Pular para conteúdo

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 ⧉.

  1. Controller: Responsável pela entrada e saída das requisições rest.
  2. Service: Responsável pela lógica de negócios.
  3. 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.

spring-io-filled.png

  • 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 arquivo MySpringBootApplication.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):

spring:
  application:
    name: myapp

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:

java -jar myapp.jar --spring.profiles.active=dev

Via application.yml:

spring:
  profiles:
    active: dev

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 em resources executa os comandos sql presentes.
  • A configuração jpa.hibernate.ddl-auto recebe os valores validate, update, create, create-drop. O valor padrão é none.
  • As configurações jpa.hibernate.properties.hibernate.default_schema e jpa.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.

scripts-iniciais.png

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.

server:
  port: 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:

log-inicializacao.png

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 no plural.
  • 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 o Body da requisição, como no método createCinema, 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 classe NovoCinemaDTO.
  • A anotação @Valid define que o Json de entrada deve ser validado. No nomento da serialização o Spring Boot utiliza o jakarta-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ão 404 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 o Construtor para encontrar e injetar as suas dependencias.
  • Um construtor para fieldsdo 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 Factorypara 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:

@Repository
public interface CinemaRepository extends JpaRepository<Cinema, Long> {
    //
}

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.
    public record CinemaSummaryVO(
            Long id,
            String nome
    ) {
    }
    
  • Da mesma forma a interface permite a projeção performatica, apenas com menos especialização.
    public interface CinemaDetailsVO {
    
     String getId();
     String getNome();
    
    }
    

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:

  1. Complexidade: Escrever consultas com CriteriaBuilder pode resultar em código complexo e difícil de ler, especialmente para consultas grandes e complicadas.
  2. 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.
  3. 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 e nulidade 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.

  1. 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 de WebClient.
  2. Construa a Instância de WebClient Usando WebClient.Builder:

    • Use o método build() para criar a instância final do WebClient.
@Configuration
public class ViaCepClientConfig {

    @Bean
    WebClient viaCepWebClient(
        @Value("${application.properties.client.viacep.base-url}") String baseUrl
    ){
        return WebClient.builder()
            .baseUrl(baseUrl)
            .build();
    }

}
  1. Classe WebClientConfig:

    • Anotada com @Configuration, indicando que ela define beans Spring.
  2. Método webClient:

    • Anotado com @Bean, para que o Spring trate a instância retornada como um bean gerenciado.
    • Utiliza WebClient.builder() para criar um WebClient.Builder.
    • Configura o WebClient com um baseUrl padrão e um header padrão Content-Type: application/json.
    • Chama build() para criar a instância final do WebClient.

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:

http://localhost:3002/swagger-ui.html

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

  1. 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.
  2. 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.
  3. 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:

./mvnw clean package

Então deve-se usar o comando docker build para criar uma imagem Docker a partir do Dockerfile.

docker build -t my-spring-boot-app .

Assim é possível iniciar um contêiner a partir da imagem Docker criada.

docker run -p 3002:3002 my-spring-boot-app

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.