Documento de Referência Arquitetural¶
Introdução¶
Este documento descreve a arquitetura proposta para o desenvolvimento de uma API Web utilizando Java.
O código está disponível em: http://git.magnasistemas.com.br/mith/labs/projeto-referencia-java.git
Este código implementa uma api rest para controlar um cadastro de cinemas.
Arquitetura Geral¶
A arquitetura do aplicativo seguirá os princípios de separação de responsabilidades, modularidade e escalabilidade.
Utilizaremos como base a Arquitetura em Camadas
(Layered Archtecture) que pode ser estudada em mais detalhes em Arquitetura em Camadas ⧉.
- Controller: Responsável pela entrada e saída das requisições rest.
- Service: Responsável pela lógica de negócios.
- Repository: Responsável pela conexão com o banco de dados.
Bibliotecas, Frameworks e Design Patterns¶
- Spring Boot 3: Para uma experiência de desenvolvimento web orgânica e padronizada.
- Spring Core: Para injeção de dependencia e configuração do projeto web;
- Spring Web: Para construir endpoints rest.
- Spring Webflux: Para executar chamadas a endpoints rest externos.
- Rest: Para uma integração moderna com a web por meio de um protocolo de comunicação abrangente.
- Jakarta Validation: Para padronização de validação na entrada dos dados.
- Lombok: Para eliminar o código boiller plate do java.
- Jpa: Para uma conexão gerenciada com o banco de dados.
- Spring data: Para abstrair as integrações com o banco de dados.
- Driver H2: Para um banco de dados local, ideal para prototipação do banco.
- Driver PostgreSql: Para fornecer a conexão correta de uma dada instancia de banco de dados com a aplicação java.
- Juni: Para realizar os testes automatizados
- Test Containers: Para realizar testes de integração o mais próximo dos cenários reais possível.
Iniciando¶
Para criar a estrutura inicial do projeto, após a instalação correta do java (conforme descrito em preparacao-ambiente ⧉), basta acessar o spring-io ⧉, preencher os detalhes do projeto e clicar em gerar.
- Project: Esta seção define qual é o empacotador do projeto, escolha Maven;
- Language: Esta seção define qual a linguagem de programação utilizada no spring boot, utilize Java;
- Spring Boot: Esta seção define qual é a versão do spring boot a ser utilizada, escolha sempre a versão Release;
- Project Metadata: Esta seção define as informações de identificação do projeto e versão do java;
- Deve ser sempre preenchido com as informações padrão da organização e do cliente;
- O empacotamento correto para um deploy containerizado é o Jar;
- A versão do Java deve ser sempre LTS ⧉
- Dependencies: Esta seção define os componentes do spring boot que serão adicionados ao projeto;
- Devem respeitar a arquitetura definida para o projeto;
- Tem alto impacto no desenvolvimento;
Ao gerar o projeto, um download iniciará contendo um Zip com o esqueleto do projeto.
Deve ser desempacotado e inserido no diretório versionado do projeto, conforme descrito em obtendo-codigo-fonte ⧉.
A estrutura gerada para o projeto spring boot deve se semelhante a esta:¶
my-spring-boot-project/
│
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── myproject/
│ │ │ ├── MySpringBootApplication.java
│ │ │
│ │ ├── resources/
│ │ ├── static/
│ │ ├── templates/
│ │ ├── application.properties
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── myproject/
│ │ └── MySpringBootApplicationTests.java
│ └── resources/
│
├── .gitignore
├── mvnw
├── mvnw.cmd
├── pom.xml
└── README.md
No exemplo acima:
- O desenvolvimento acontece dentro da pasta
myproject
, sendo o arquivoMySpringBootApplication.java
a porta de entrada da aplicação. - O arquivo
pom.xml
guarda a configuração do projeto, no que diz respeito a compilação, as suas dependencias e estruturas de testes. - O arquivo
application.propertiers
guarda as configurações dos componentes do spring boot. - Os diretórios
resources
são diretórios especiais que guardam arquivos estáticos a serem utilizados durante a execução do projeto. - Podem ser utilizados para guardar quaisquer dados estáticos como imagens, estilos, etc.
- O diretório test guarda os arquivos de automação de testes. Este diretório geralmente espelha os diretórios de desenvolvimento por questão de padronização.
A configuração¶
Estrutura básica de dependencias gerada pelo spring-io contendo:
- O nome do projeto, que será o nome final do arquivo compilado, uma configuração de publicação e sua versão atual.
- A definição da versão do Java a ser utilizada.
- A declaração das dependencias utilizadas pelo projeto final e as dependencias utilizadas apenas durante o desenvolvimento, como os testes.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>br.com.magna.mith</groupId>
<artifactId>projeto-referencia-java-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>projeto-referencia-java-api</name>
<description>Projeto para servir de referência para apis java</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
</project>
Porta de entrada¶
Por padrão o spring-io cria uma classe com o nome do projeto como por exemplo MySpringBootApplicationTests.java
para ser a porta de entrada da aplicação.
Esta classe contém a anotação @SpringBootApplication
e o método main com retorno void e por meio desses dois itens é capaz de inicializar todo o Contexto Spring
.
A porta de entrada de uma aplicação spring boot seria:
package br.com.magna.mith.projetoreferenciajavaapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ProjetoReferenciaJavaApiApplication {
public static void main(String[] args) {
SpringApplication.run(ProjetoReferenciaJavaApiApplication.class, args);
}
}
O contexto spring é iniciado aqui¶
O Contexto Spring
é a estrutura de objetos, carregada em memória, que oferece além das configurações iniciais do projeto, a injeção de dependencias.
- Ao iniciar o projeto, o spring escaneia todas as classes, por meio das suas anotações padronizadas, carregando suas instâncias no contexto e inserindo-as onde são solicitadas.
- Por meio desse mecanismo é possível para o spring validar se algo está faltando no contexto ou se alguma configuração não atende os requisitos necessários.
O aplication properties¶
O arquivo application.properties
é o principal arquivo de configuração de uma aplicação Spring Boot.
Nele, você pode definir várias propriedades que controlam o comportamento da aplicação, como configurações de banco de dados, portas de servidor, credenciais de segurança, entre outras.
Exemplo de application.properties
:
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=rootpassword
Embora application.properties
seja amplamente utilizado, o formato YAML (application.yml
) é frequentemente considerado melhor devido à sua estrutura mais legível e organizada.
YAML permite uma hierarquia clara e evita a repetição de prefixos comuns.
Exemplo de application.yml
:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: rootpassword
O Spring Boot suporta perfis de configuração, permitindo definir diferentes configurações para diferentes ambientes (por exemplo, desenvolvimento, teste, produção). Isso é feito através do uso de perfis, que podem ser ativados em tempo de execução.
Motivação para o uso de perfis:¶
- Flexibilidade: Facilita a gestão de configurações específicas para cada ambiente.
- Segurança: Permite a definição de configurações seguras para ambientes de produção.
- Manutenção: Simplifica a manutenção e a atualização das configurações, centralizando-as de acordo com o ambiente.
application.yml
(Configurações comuns):
application-dev.yml
(Configurações para o perfil de desenvolvimento):
spring:
profiles: dev
datasource:
url: jdbc:mysql://localhost:3306/devdb
username: devuser
password: devpassword
application-prod.yml
(Configurações para o perfil de produção):
spring:
profiles: prod
datasource:
url: jdbc:mysql://prod-db-server:3306/proddb
username: produser
password: prodpassword
Os perfis podem ser ativados através de variáveis de ambiente, parâmetros da linha de comando ou no próprio application.yml
.
Via linha de comando:
Via application.yml
:
Nosso application.yaml
para o profile local
está configurado desta forma:¶
server:
port: 3002
spring:
application:
name: projeto-referencia-java-api
devtools:
add-properties: true
servlet:
multipart:
max-file-size: 2MB
enabled: true
file-size-threshold: 2KB
location: /tmp/uploaded-files
security:
user:
name: admin
password: admin
main:
banner-mode: OFF
cache:
type: redis
data:
web:
pageable:
default-page-size: 10
max-page-size: 50
page-parameter: page
size-parameter: size
sort:
sort-parameter: sort
jackson:
# property-naming-strategy: SNAKE_CASE
default-property-inclusion: non-null
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:projeto_referencia_java_api;DB_CLOSE_DELAY=-1
username: sa
password: password
hikari:
connection-test-query: SELECT 1
schema: classpath:schema-local.sql
jpa:
show-sql: true
open-in-view: false
hibernate:
ddl-auto: create-drop
properties:
hibernate:
transaction:
jta:
platform: none
jdbc:
time_zone: America/Sao_Paulo
lob.non_contextual_creation: true
format_sql: true
generate_statistics: true
use_sql_comments: false
default_schema: JAVA_API
management:
endpoints:
web:
exposure:
include: '*'
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
springdoc:
api-docs:
enabled: true
path: /api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
operationsSorter: method
show-actuator: true
logging:
level:
root: INFO
org:
hibernate:
orm:
jdbc:
bind: TRACE
extract: TRACE
SQL: TRACE
springframework:
web: TRACE
Por vezes são necessárias propriedades adicionais para comportamentos específicos da aplicação.
Utilizando a dependencia spring-boot-configuration-processor
e configurações dentro do arquivo resources/META-INF/additional-spring-configuration-metadata.json
é possível definir propriedades como:
{"properties": [
{
"name": "application.properties",
"type": "java.lang.String",
"description": "A description for 'application.properties'"
},
{
"name": "application.properties.client",
"type": "java.lang.String",
"description": "A description for 'application.properties.client'"
},
{
"name": "application.properties.client.viacep",
"type": "java.lang.String",
"description": "A description for 'application.properties.client.viacep'"
},
{
"name": "application.properties.client.viacep.base-url",
"type": "java.lang.String",
"description": "A description for 'application.properties.client.viacep.base-url'"
},
{
"name": "application.properties.client.themoviedb",
"type": "java.lang.String",
"description": "A description for 'application.properties.client.themoviedb'"
},
{
"name": "application.properties.client.themoviedb.base-url",
"type": "java.lang.String",
"description": "A description for 'application.properties.client.themoviedb.base-url'"
}
]}
Assim, propriedades extras definidas no arquivo application.yaml recebem auto-complete e documentação.
application:
properties:
client:
viacep:
base-url: 'https://viacep.com.br/ws'
themoviedb:
base-url: 'https://api.themoviedb.org/3'
Importante! A configuração presente em additional-spring-configuration-metadata.json é opcional.
As propriedades adicionais podem ser utilizadas sem os metadados, entretanto é altamente recomendado o seu uso.
Configurações obrigatórias¶
Algumas configurações do spring boot são obrigatórias, como a configuração de conexão com o banco de dados.
Ao definir a dependencia spring-boot-starter-data-jpa
é necessário definir um data-source
.
- Uma conexão com o banco de dados por meio da aplicação jáva se dá por meio do driver
- Cada banco de dados como o MySql, PostgreSql e Oracle, tem seu próprio driver
- Para uma aplicação que utiliza o maven o driver deve ser definido como uma dependência
- Além do driver, são definidas as informações de conexão como url, usuário e senha.
Para o exemplo de implementação do projeto de referência, utilizaremos o banco de dados H2 ⧉, que é um banco de dados embutido, pela facilidade e independencia de execução local. A definição de uma conexão com o banco de dados H2 no application.yaml fica assim:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:projeto_referencia_java_api;DB_CLOSE_DELAY=-1
username: sa
password: password
Com uma conexão de banco de dados gerenciada pelo Spring Boot é possível utilizar diversas funcionalidades nativas. As funcionalidades de definir scripts iniciais e criar scripts a partir das entidades ORM definidas é perticularmente útil.
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:projeto_referencia_java_api;DB_CLOSE_DELAY=-1
username: sa
password: password
hikari:
connection-test-query: SELECT 1
schema: classpath:schema-local.sql
jpa:
show-sql: true
open-in-view: false
hibernate:
ddl-auto: create-drop
properties:
hibernate:
transaction:
jta:
platform: none
format_sql: true
generate_statistics: true
use_sql_comments: false
default_schema: JAVA_API
- A configuração
datasource.schema
recebe o nome de um arquivo que deve estar presente emresources
executa os comandos sql presentes. - A configuração
jpa.hibernate.ddl-auto
recebe os valoresvalidate
,update
,create
,create-drop
. O valor padrão énone
. - As configurações
jpa.hibernate.properties.hibernate.default_schema
ejpa.hibernate.properties.hibernate.default_catalog
permitem que, na criação dos scripts iniciais por meio de entidades ORM, sejam criados no schema e catalog definidos.
No nosso exemplo definimos dois scripts iniciais, um para cada profile.
Iniciando a aplicação¶
Estamos prontos para iniciar a aplicação.
De acordo com o que definimos ao executar o método main da classe principal, o servidor web será disponibilizado em localhost, na porta 3002
.
No console da aplicação vários logs serão disponibilizados, esclarecendo os detalhes da da inicialização. Se tudo estiver correto, a mensagem a seguir será exibida:
Neste momento não há nenhuma definição de api, mas é possível verificar se o servidor está ativo e escutando acessando http://localhost:3002 ⧉.
O controller¶
No Spring Boot, os REST Controllers são usados para criar APIs RESTful, permitindo que a aplicação lide com requisições HTTP e responda com dados no formato JSON.
Esses controladores são classes Java anotadas com @RestController
e @RequestMapping
.
- A anotação
@RestController
é usada para definir uma classe como um controlador REST. Ela combina@Controller
e@ResponseBody
, o que significa que cada método da classe retornará diretamente o corpo da resposta HTTP. - A anotação
@RequestMapping
ou suas especializações (@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
, etc.) são usadas para mapear URLs específicas para métodos do controlador. - As anotações
@GetMapping
,@PutMapping
,@PostMapping
,@DeleteMapping
e@PatchMapping
definem a nível de método recursos rest no servidor a serem recebidos pelo servidor.
Neste exemplo:
@RestController
@RequestMapping(value = "/v1/cinemas")
@RequiredArgsConstructor
@Tag(name = "Cinemas", description = "Conjunto de endpoints para gerenciar os dados dos cinemas.")
public class CinemaController {
private final ConsultaCinemaService consultaCinemaService;
private final CadastroCinemaService cadastroCinemaService;
private final AlteracaoCinemaService alteracaoCinemaService;
@Operation(
summary = "Consulta todos os cinemas cadastrados"
)
@GetMapping(
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Page<CinemaSummaryVO>> getCinemas(
Pageable pageable
) {
return ResponseEntity.ok(
this.consultaCinemaService.getAll(pageable)
);
}
@Operation(
summary = "Detalha o cinema pelo identificador"
)
@GetMapping(
path = "/{idCinema}",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<CinemaDetailsVO> getCinema(
@PathVariable Long idCinema
) {
return ResponseEntity.of(
this.consultaCinemaService.detail(idCinema)
);
}
@Operation(
summary = "Cria um novo cinema"
)
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Long> createCinema(
@Valid @RequestBody NovoCinemaDTO novoCinemaDTO,
ServletUriComponentsBuilder uriBuilder
) {
Long newCinemaId = this.cadastroCinemaService.create(novoCinemaDTO);
URI location = uriBuilder.path("/cinemas/{idCinema}")
.buildAndExpand(newCinemaId)
.toUri();
return ResponseEntity.created(location)
.build();
}
@Operation(
summary = "Altera um cinema existente"
)
@PutMapping(
path = "/{idCinema}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Long> updateCinema(
@PathVariable Long idCinema,
@Valid @RequestBody AlteracaoCinemaDTO alteracaoCinemaDTO
) {
return ResponseEntity.of(
this.alteracaoCinemaService.update(
idCinema,
alteracaoCinemaDTO
)
);
}
@Operation(
summary = "Altera um cinema existente"
)
@DeleteMapping(
path = "/{idCinema}",
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Long> deleteCinema(
@PathVariable Long idCinema
) {
return ResponseEntity.of(
this.alteracaoCinemaService.delete(idCinema)
);
}
}
- É boa pratica definir versão para os recursos Rest, como em
@RequestMapping(value = "/v1/cinemas")
, para que mudanças necessárias não impactem diretamente os clientes que já utilizam. - O padrão de nomenclatura de recursos Rest determina que endpoints que são a raiz de outros recursos, como
@RequestMapping(value = "/v1/cinemas")
, devem ser declarados noplural
. - As anotações que definem os métodos
Http
devem declarar o tipo de serialização de entrada e/ou saida, como em@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
, onde o produces define que apenas respostas Json são produzidas. - Os recursos no plural devem devolver listagens sem filtro e são encorajadas a usar paginação.
- Os recuros unitários, identificaveis dentro do servidor por um identificador único devem ser obtidos utilizando o caminho Rest, como em
@GetMapping(path = "/{idCinema}", produces = MediaType.APPLICATION_JSON_VALUE)
. - As entradas de dados por meio do método
Post
devem ser serializadas utilizado oBody
da requisição, como no métodocreateCinema
, onde são utilizadas as anotações@Valid @RequestBody NovoCinemaDTO novoCinemaDTO
. - A anotação
@RequestBody
define que o Json de entrada deve atender ao protótipo definido na classeNovoCinemaDTO
. - A anotação
@Valid
define que o Json de entrada deve ser validado. No nomento da serialização o Spring Boot utiliza ojakarta-validation
para executar as regras de validação. - As saidas de dados de todos os endpoints devem estar encapsuladas pelo objeto
ResponseEntity
, que prepara uma resposta padrão com status e corpo da resposta. - O método
ResponseEntity.of()
, que utiliza a interface Optional, é especialmente util pois retorna uma resposta padrão404
quando o recurso opcional não é encontrado.
Mais detalhes¶
Validação¶
A estrutura da validação é parte importante da consistencia das funcionalidades como um todo. As anotações jakarta são simples e devem ser utilizadas obrigatoriamente:
@Data
@AllArgsConstructor
public class NovoCinemaDTO {
@NotBlank
@Length(max = 100)
private String nome;
@PositiveOrZero
@Max(10)
private Integer quantidadeSala;
@NotBlank
@Pattern(regexp = "^\\d+$", message = "Somente números são permitidos")
@Length(max = 8, min = 8)
private String cep;
@Positive
private Integer numeroEndereco;
@Length(max = 200)
private String complemento;
}
Importante! É responsabilidade do controller garantir que apenas dados válidos adentrem as outras camadas do sistema.
Injeção de dependencia¶
A responsabilidade do controller é lidar com entradas e saídas de dados. Regras de negócio são responsabilidades de outra camada, portantanto a controller apenas delega essa responsabilidade utilizando suas dependencias.
@RequiredArgsConstructor
@Tag(name = "Cinemas", description = "Conjunto de endpoints para gerenciar os dados dos cinemas.")
public class CinemaController {
private final ConsultaCinemaService consultaCinemaService;
private final CadastroCinemaService cadastroCinemaService;
private final AlteracaoCinemaService alteracaoCinemaService;
//
}
- A estratégia de injeção de dependencia utilizada é a de
Construtor
. - É boa pratica utilizar a estratégia de Construtor para facilitar o posterior
Teste
da classe. - No momento da instanciação desse objeto pelo
Contexto Spring
, ele utiliza oConstrutor
para encontrar e injetar as suas dependencias. - Um construtor para
fields
do tipo final é declarado por meio da anotação@RequiredArgsConstructor
do lombok.
O Service¶
No Spring Boot, a camada de serviço é usada para encapsular a lógica de negócios da aplicação. Os services são componentes que contêm métodos para realizar operações de negócios, geralmente interagindo com a camada de persistência e a camada de apresentação.
Importante! A camada de serviço é fundamental para a organização das regras de negócio do sistema.
Devem ser granulares e modulares.
Os services no Spring Boot são definidos como classes Java e anotados com @Service
.
Essa anotação indica ao Spring que a classe é um componente de serviço, permitindo que seja detectada e registrada no contexto da aplicação durante a varredura de componentes.
@Service
@RequiredArgsConstructor
public class ConsultaCinemaService {
private final CinemaRepository cinemaRepository;
public Page<CinemaSummaryVO> getAll(Pageable pageable) {
return this.cinemaRepository.findAllSummary(pageable);
}
public Optional<CinemaDetailsVO> detail(Long idCinema) {
return this.cinemaRepository.findDetailsBy(idCinema);
}
}
- Neste serviço estão concentrados os métodos de negócio relacionados a consulta.
- Note que são consultas simples, entretanto com o avançar do desenvolvimento tende a crescer.
- Ter estes métodos misturados a outros métodos que não tem a ver com consulta pode levar a classes muito grandes e difíceis de testar.
Outro exemplo de serviço:
@Service
@RequiredArgsConstructor
public class CadastroCinemaService {
private final CinemaRepository cinemaRepository;
private final CinemaFactory cinemaFactory;
private final EnderecoService enderecoService;
@Transactional
public Long create(NovoCinemaDTO novoCinemaDTO) {
validarCinemaJaExistente(novoCinemaDTO.getNome());
return this.cinemaRepository.save(
this.cinemaFactory.createFrom(
this.enderecoService.create(
novoCinemaDTO.getCep(),
novoCinemaDTO.getNumeroEndereco(),
novoCinemaDTO.getComplemento()
),
novoCinemaDTO
)
).getId();
}
private void validarCinemaJaExistente(String nomeCinema) {
if(
this.cinemaRepository.existsByNome(nomeCinema)
){
throw new CinemaJaExisteException(nomeCinema);
}
}
}
- Neste serviço estão concentrados os métodos de negócio relacionados ao cadastro de um cinema.
- O objeto de entrada já está validado em sua consistencia.
- Regras mais complexas são validadas dentro do método.
- A anotação
@Transactional
garante que em caso de erro durante uma iteração com o banco de dados, qualquer alteração será desconsiderada. - O retorno do método é simples, mas com grande significado, representa o dado novo que foi criado e dispensa serialização ou mapeamento mais complexo.
- É utilizado o padrão de projeto
Factory
para a abstrair a complexidade de criar um novo objeto do tipo Cinema.
O repository¶
O Spring Data JPA simplifica o acesso a dados em aplicações Spring, fornecendo uma abstração de repositório que reduz a quantidade de código boilerplate necessário para interagir com bancos de dados.
Um repositório spring é definido como uma interface, assim:
O Spring Data JPA permite a criação de métodos de consulta personalizados apenas definindo o nome do método na interface do repositório.
Exemplo:
@Repository
public interface CinemaRepository extends JpaRepository<Cinema, Long> {
boolean existsByNome(String nome);
}
O spring se encarrega de construir a query final, baseada no padrão de nomenclatura utilizado no método.
Para consultas mais complexas, você pode usar a anotação @Query
e escrever a consulta JPQL ⧉.
Exemplo:
@Query(
value = """
SELECT
new br.com.magna.mith.projetoreferenciajavaapi.model.vo.CinemaSummaryVO(
cinema.id,
cinema.nome
)
FROM
Cinema cinema
"""
)
Page<CinemaSummaryVO> findAllSummary(Pageable pageable);
@Query(
value = """
SELECT
cinema.id AS id,
cinema.nome AS nome
FROM
Cinema cinema
WHERE
cinema.id = :idCinema
"""
)
Optional<CinemaDetailsVO> findDetailsBy(Long idCinema);
@Query(
value = """
SELECT
new br.com.magna.mith.projetoreferenciajavaapi.model.vo.CinemaSummaryVO(
cinema.id,
cinema.nome
)
FROM
Cinema cinema
JOIN
cinema.endereco endereco
JOIN
endereco.cep cep
WHERE
(:nomeCinema IS NULL OR cinema.nome = :nomeCinema)
AND
(:cepCinema IS NULL OR cep.numeroCep = :cepCinema)
"""
)
Page<CinemaSummaryVO> findAllBy(
String nomeCinema,
String cepCinema,
Pageable pageable
);
- As queries JPQL devem ser construídas utilizando Text Block ⧉ para uma identação padronizada.
- Para realizar uma projeção performatica, ou seja, garantindo que a query final traga apenas as colunas solicitadas, deve-se utilizar VO ⧉ ou Interfaces.
- O VO servirá como uma representação de uma porção da entidade ORM, garantindo imutabilidade e permitindo especialização durante a criação, por meio do construtor.
- Da mesma forma a interface permite a projeção performatica, apenas com menos especialização.
Importante! O CriteriaBuilder do JPA é uma API poderosa para construir consultas de forma programática.
No entanto, ela pode ser evitada devido aos seguintes motivos:
- Complexidade: Escrever consultas com
CriteriaBuilder
pode resultar em código complexo e difícil de ler, especialmente para consultas grandes e complicadas. - Manutenção: O código gerado por
CriteriaBuilder
pode ser difícil de manter e modificar em comparação com as consultas declarativas usando JPQL ou@Query
. - Legibilidade: As consultas construídas com
CriteriaBuilder
tendem a ser menos legíveis do que aquelas escritas diretamente em JPQL ou SQL, tornando mais difícil para outros desenvolvedores entenderem o que a consulta está fazendo.
O model¶
As entidades JPA são classes Java que representam tabelas no banco de dados. Elas são a base para o mapeamento objeto-relacional (ORM) que o JPA (Java Persistence API) fornece.
Uma entidade é anotada com @Entity
e geralmente mapeada para uma tabela de banco de dados com @Table
.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Entity
@Table(
name = "TB_CINEMA",
uniqueConstraints = @UniqueConstraint(name = "UK_ID_ENDERECO", columnNames = "ID_ENDERECO"),
indexes = {
@Index(name = "IDX_CINEMA_DS_NOME", columnList = "DS_NOME")
}
)
public class Cinema {
@Id
@Comment("Identificador sequencial único")
@Column(name = "ID", nullable = false, unique = true)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Length(max = 100)
@Comment("Nome do cinema")
@Column(name = "DS_NOME", nullable = false, unique = true, length = 100)
private String nome;
@Min(1L)
@Max(10L)
@Comment("Quantidade de salas do cinema")
@Column(name = "QT_SALA", nullable = false)
private Integer quantidadeSala;
@Comment("Identificador único para o endereço")
@JoinColumn(name = "ID_ENDERECO", foreignKey = @ForeignKey(name = "FK_CINEMA_ID_ENDERECO"), nullable = false, unique = true)
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Endereco endereco;
}
É uma prática comum usar nomes de tabela e coluna em caixa alta para manter a consistência com a convenção SQL. Os padrões de nomenclatura estão disponíveis em banco-de-dados ⧉.
Importante! Para uma relação coerente com o banco de dados é importante declarar as estruturas completas no objeto ORM, como indices, chaves estrangeiras e tabelas de relação.
- É importante utilizar o
jakarta-validation
também na camada de persistencia, para que as regras do banco de dados sejam respeitadas. - As configurações de
tipagem
,tamanho
enulidade
das colunas devem sempre ser declaradas, para evitar discrepancias entre o banco e a modelagem orm.
As exceções¶
Para aplicações web tratamento de erros tem um papel importante já que impactam diretamente a experiência do usuário.
Exceções podem ser utilizadas para definir fluxos alternativos de forma transparente por meio do @ControllerAdvice
.
@ControllerAdvice
é uma anotação no Spring Boot usada para tratar exceções e fornecer manipulação global de exceções em todos os controladores.
Ela permite centralizar a lógica de tratamento de erros, tornando o código mais limpo e organizado.
- A anotação
@ControllerAdvice
é usada em uma classe para designá-la como um componente de conselho que pode interceptar exceções lançadas por controladores. - Dentro de uma classe anotada com
@ControllerAdvice
, você define métodos anotados com@ExceptionHandler
para tratar exceções específicas ou genéricas.
@Slf4j
@ControllerAdvice
public class DefaultControllerAdvice extends DefaultErrorAttributes {
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<Map<String, Object>> handle(BadRequestException exception, WebRequest webRequest) {
log.warn(exception.getMessage(), exception);
return ResponseEntity.status(BAD_REQUEST)
.body(defaultErrorAttributes(webRequest));
}
@ExceptionHandler(BindException.class)
public ResponseEntity<Map<String, Object>> handle(BindException exception, WebRequest webRequest) {
log.warn(exception.getMessage(), exception);
return ResponseEntity.status(BAD_REQUEST).
body(defaultErrorAttributes(webRequest));
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handle(ConstraintViolationException exception, WebRequest webRequest) {
log.warn(exception.getMessage(), exception);
return ResponseEntity.status(BAD_REQUEST).
body(defaultErrorAttributes(webRequest));
}
@ExceptionHandler(ServletRequestBindingException.class)
public ResponseEntity<Map<String, Object>> handle(ServletRequestBindingException exception, WebRequest webRequest) {
log.warn(exception.getMessage(), exception);
return ResponseEntity.status(BAD_REQUEST).
body(defaultErrorAttributes(webRequest));
}
@ExceptionHandler(InternalServerErrorException.class)
public ResponseEntity<Map<String, Object>> handle(InternalServerErrorException exception, WebRequest webRequest) {
log.error(exception.getMessage(), exception);
return ResponseEntity.status(INTERNAL_SERVER_ERROR)
.body(defaultErrorAttributes(webRequest));
}
private Map<String, Object> defaultErrorAttributes(WebRequest webRequest) {
Map<String, Object> errorAttributes = super.getErrorAttributes(
webRequest,
ErrorAttributeOptions.of(
ErrorAttributeOptions.Include.MESSAGE,
ErrorAttributeOptions.Include.EXCEPTION,
ErrorAttributeOptions.Include.BINDING_ERRORS
)
);
errorAttributes.remove("status");
errorAttributes.put(
"path",
webRequest.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST)
);
return errorAttributes;
}
@ExceptionHandler(WebClientIntegrationException.class)
public ResponseEntity<Map<String, Object>> handle(WebClientIntegrationException exception, WebRequest webRequest) {
log.warn(exception.getMessage(), exception);
return ResponseEntity.status(INTERNAL_SERVER_ERROR)
.body(defaultErrorAttributes(webRequest));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handle(Exception exception, WebRequest webRequest) {
log.error(exception.getMessage(), exception);
return ResponseEntity.status(INTERNAL_SERVER_ERROR)
.body(defaultErrorAttributes(webRequest));
}
}
Os exemplos acima definem excelentes formatos de tratamento, do mais específico para o mais genérico.
Como boa prática é util definir famílias de exceções para centralizar os tipos de retorno, como no exemplo acima, BadRequestException
e InternalServerErrorException
possuem retorno tratado com os status 400
e 500
respectivamente.
public class CinemaJaExisteException extends BadRequestException {
public CinemaJaExisteException(@NotBlank String nome) {
super("Um cinema com o nome %s já existe.".formatted(nome));
}
}
No exemplo acima, esta exceção será capturada pelo handler e apresentará a mensagem corretamente encapsulada em um status 400.
A integração externa¶
O Spring WebFlux WebClient é uma ferramenta reativa e não bloqueante para realizar requisições HTTP no Spring Framework. Ele substitui o RestTemplate em aplicações reativas e é ideal para situações onde a escalabilidade e a performance são críticas.
- Reatividade: Suporta programação reativa, permitindo um alto nível de paralelismo e uso eficiente dos recursos.
- Não Bloqueante: As operações HTTP não bloqueiam a thread que as invoca, permitindo melhor desempenho em aplicações com alta concorrência.
- Versatilidade: Pode ser utilizado para realizar chamadas síncronas e assíncronas a APIs REST.
Para configurar o WebClient em uma aplicação Spring Boot, você pode criar uma classe de configuração anotada com @Configuration
e definir um bean WebClient
usando @Bean
.
-
Crie a Classe de Configuração:
- Anote a classe com
@Configuration
para indicar que é uma classe de configuração Spring. - Defina um método anotado com
@Bean
que retorna uma instância deWebClient
.
- Anote a classe com
-
Construa a Instância de WebClient Usando
WebClient.Builder
:- Use o método
build()
para criar a instância final doWebClient
.
- Use o método
@Configuration
public class ViaCepClientConfig {
@Bean
WebClient viaCepWebClient(
@Value("${application.properties.client.viacep.base-url}") String baseUrl
){
return WebClient.builder()
.baseUrl(baseUrl)
.build();
}
}
-
Classe
WebClientConfig
:- Anotada com
@Configuration
, indicando que ela define beans Spring.
- Anotada com
-
Método
webClient
:- Anotado com
@Bean
, para que o Spring trate a instância retornada como um bean gerenciado. - Utiliza
WebClient.builder()
para criar umWebClient.Builder
. - Configura o
WebClient
com umbaseUrl
padrão e um header padrãoContent-Type: application/json
. - Chama
build()
para criar a instância final doWebClient
.
- Anotado com
Depois de configurado, você pode injetar o WebClient
em seus componetes e utilizá-lo para realizar requisições HTTP.
@Component
@RequiredArgsConstructor
class ViaCepClient {
private final WebClient viaCepWebClient;
public ViaCepResponse get(String cep){
return this.viaCepWebClient.get()
.uri("/%s/json/".formatted(cep))
.retrieve()
.bodyToMono(ViaCepResponse.class)
.block();
}
}
!Importante. Neste exemplo a injeção de dependencia oriunda de uma construção com o @Bean faz uso de uma facilidade do Spring que permite utilizar o nome do método anotado como Qualifier.
Este formato de injeção deve ser utilizado em detrimento a anotação análoga @Qualifier
A documentação das APIs¶
O Swagger é uma ferramenta popular para a documentação e teste de APIs RESTful. Ele fornece uma interface gráfica para explorar e testar endpoints, tornando a API mais acessível para desenvolvedores e integradores.
É necessário adicionar a dependência ao projeto Maven:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
A configuração básica do Swagger no Spring Boot é bastante simples. Na maioria dos casos, você não é necessária uma configuração adicional, pois a integração é automática. No entanto, é possível personalizar o comportamento do Swagger criando uma classe de configuração.
@Configuration
public class OpenApiConfig {
@Bean
OpenAPI openAPI() {
Contact contact = new Contact()
.name("Arquitetura MITH")
.email("mith@magnasistemas.com.br")
.url("http://git.magnasistemas.com.br/mith/arquitetura-mith");
Info info = new Info()
.title("Starter Java REST API")
.version("1.0.0")
.contact(contact)
.description("Inventário de serviços disponibilizados pelo time de Arquitetura MITH.");
return new OpenAPI()
.info(info);
}
}
Após configurar o Swagger, é possível acessar a interface de documentação da API em:
Também é possível adicionar informações à documentação da API usando a anotação @Operation
e outras anotações relacionadas do OpenAPI.
@Operation(
summary = "Consulta todos os cinemas cadastrados"
)
@GetMapping(
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Page<CinemaSummaryVO>> getCinemas(
Pageable pageable
) {
return ResponseEntity.ok(
this.consultaCinemaService.getAll(pageable)
);
}
A segurança¶
O Spring Boot Security é um módulo do Spring Framework que fornece funcionalidades de segurança para aplicações Spring Boot, como autenticação, autorização e proteção contra ataques comuns. Ele integra facilmente várias estratégias de segurança, como login baseado em formulários, OAuth2, JWT, etc.
Funcionalidades Principais¶
-
Autenticação:
- Verifica a identidade de um usuário.
- Suporta múltiplos métodos de autenticação, incluindo formulários, HTTP Basic, OAuth2, e JWT.
-
Autorização:
- Controla o acesso a recursos com base nas permissões do usuário.
- Define políticas de segurança para URLs e métodos de serviço.
-
Proteção Contra Ameaças:
- Proteção contra ataques CSRF (Cross-Site Request Forgery).
- Segurança de sessão para prevenir sequestro de sessão.
- Proteção contra ataques de clickjacking.
É necessário dicionar a dependência do Spring Security ao projeto Maven.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
O Spring Boot Security aplica uma configuração de segurança padrão quando a dependência é adicionada. Isso inclui a exigência de autenticação para todas as requisições e a criação de uma página de login padrão.
É possível personalizar a configuração de segurança.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public InMemoryUserDetailsManager userDetailsService(
@Value("${spring.security.user.name}") String username,
@Value("${spring.security.user.password}") String password,
PasswordEncoder passwordEncoder
) {
return new InMemoryUserDetailsManager(
User.withUsername(username)
.password(passwordEncoder.encode(password))
.roles("USER", "ADMIN")
.build()
);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.requestMatchers("/v3/api-docs/**",
"/swagger-ui/**", "/swagger-ui.html", "/h2-console/**")
.permitAll()
.anyRequest()
.authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
Importante! É possível definir um provedor de usuários em memória ou baseado em banco de dados para armazenar e autenticar usuários.
Uma configuração básica de segurança geralmente é suficiente para a maioria dos casos, entretanto é importante ressaltar que para alguns projetos a autenticação do tipo OAuth pode ser mais adequada.
A conteinerização¶
A conteinerização de uma aplicação Spring Boot envolve empacotar a aplicação e suas dependências em um contêiner, como um Docker, para facilitar a distribuição, escalabilidade e execução consistente em diferentes ambientes.
O Dockerfile
define o ambiente necessário para executar a aplicação Spring Boot.
# Usando a imagem base do JDK 11
FROM openjdk:21
# Adiciona um argumento para o JAR da aplicação
ARG JAR_FILE=target/*.jar
# Copia o JAR da aplicação para o contêiner
COPY ${JAR_FILE} app.jar
# Expõe a porta que a aplicação irá rodar
EXPOSE 3002
# Comando para executar a aplicação
ENTRYPOINT ["java", "-jar", "/app.jar"]
Para Compilar a aplicação Spring Boot para gerar o arquivo JAR, basta:
Então deve-se usar o comando docker build
para criar uma imagem Docker a partir do Dockerfile
.
Assim é possível iniciar um contêiner a partir da imagem Docker criada.
Os testes¶
Testes unitários e testes de integração são componentes essenciais no desenvolvimento de software para garantir a qualidade e a confiabilidade do código.
Testes Unitários¶
- Objetivo: Verificar a funcionalidade de componentes individuais, como métodos ou classes, isolados de suas dependências.
- Ferramentas Comuns: JUnit, Mockito.
- Características:
- Isolados: Testam uma unidade de código de forma independente.
- Rápidos: Executam rapidamente.
- Focados: Cada teste cobre um aspecto específico da funcionalidade.
Exemplo de Teste Unitário¶
class CadastroCinemaServiceTest {
@Mock
private CinemaRepository cinemaRepository;
@Mock
private CinemaFactory cinemaFactory;
@Mock
private EnderecoService enderecoService;
@InjectMocks
private CadastroCinemaService cadastroCinemaService;
@BeforeEach
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testCreateExistingCinema() {
NovoCinemaDTO novoCinemaDTO = new NovoCinemaDTO("Nome Cinema", 10, "123", 101, "complemento");
when(cinemaRepository.existsByNome("Nome Cinema")).thenReturn(true);
CinemaJaExisteException exception = assertThrows(CinemaJaExisteException.class, () -> {
cadastroCinemaService.create(novoCinemaDTO);
});
}
}
Testes de Integração¶
- Objetivo: Verificar a funcionalidade de vários componentes ou módulos integrados, incluindo interações com bancos de dados, sistemas externos, etc.
- Ferramentas Comuns: Spring Boot Test, Testcontainers.
- Características:
- Integrados: Testam a interação entre múltiplos componentes.
- Mais lentos: Podem envolver operações de E/S ou interações de rede.
- Abrangentes: Cobre cenários de uso mais complexos.
Exemplo de Teste de Integração¶
@SpringBootTest
@Transactional
public class CadastroCinemaServiceIntegrationTest {
@Autowired
private CadastroCinemaService cadastroCinemaService;
@Autowired
private CinemaRepository cinemaRepository;
@Test
public void testCreateNewCinema() {
NovoCinemaDTO novoCinemaDTO = new NovoCinemaDTO("Nome Cinema", 10, "04671160", 101, "");
Long cinemaId = cadastroCinemaService.create(novoCinemaDTO);
assertNotNull(cinemaId);
assertTrue(cinemaRepository.findById(cinemaId).isPresent());
}
@Test
public void testCreateExistingCinema() {
NovoCinemaDTO novoCinemaDTO = new NovoCinemaDTO("Nome Cinema", 10, "04671160", 102, "");
cadastroCinemaService.create(novoCinemaDTO);
CinemaJaExisteException exception = assertThrows(CinemaJaExisteException.class, () -> cadastroCinemaService.create(novoCinemaDTO));
}
}
Para identificar e organizar os testes deve-se separa-los em diretórios distintos, como em:
- Testes Unitários: Colocados no diretório
src/test/java
. - Testes de Integração: Podem ser colocados em um subdiretório separado, como
src/test-integration/java
.
Estrutura de Diretórios¶
src/
main/
java/
com/
example/
myapp/
MyService.java
test/
java/
com/
example/
myapp/
MyServiceTest.java
test-integration/
java/
com/
example/
myapp/
MyIntegrationTest.java
Para separar a execução de testes unitários e de integração no Maven, você pode configurar os plugins de surefire e failsafe.
Configuração do Maven (pom.xml
)¶
<build>
<plugins>
<!-- Plugin para Testes Unitários -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<!-- Plugin para Testes de Integração -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>**/*IntegrationTest.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
Por fim para execução dos testes de forma separada é possível utilizar os comandos:
- Testes Unitários: Executados com
mvn test
. - Testes de Integração: Executados com
mvn verify
.