Backend

Spring Boot

Referência completa de Spring Boot — controllers, JPA, Security, Testing, WebFlux, Cache, Events, Batch e mais

1. Setup e Estrutura de Projeto

@SpringBootApplication é o ponto de entrada de toda aplicação Spring Boot. Ela combina três anotações em uma: @Configuration (classe de configuração), @ComponentScan (varre o pacote atual e sub-pacotes por beans) e @EnableAutoConfiguration (ativa a mágica do auto-configure baseado no classpath).

@SpringBootApplication
public class PedidosApplication {
    public static void main(String[] args) {
        SpringApplication.run(PedidosApplication.class, args);
    }
}

Estrutura de projeto recomendada (pacotes por feature, não por camada):

src/main/java/com/empresa/pedidos/
├── PedidosApplication.java
├── pedido/
│   ├── PedidoController.java
│   ├── PedidoService.java
│   ├── PedidoRepository.java
│   ├── Pedido.java               // @Entity
│   ├── PedidoRequest.java        // record DTO de entrada
│   └── PedidoResponse.java       // record DTO de saída
├── pagamento/
│   ├── PagamentoController.java
│   └── ...
└── shared/
    ├── GlobalExceptionHandler.java
    └── SecurityConfig.java

Maven — dependência mínima:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.4</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</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-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

Gradle (build.gradle.kts):

plugins {
    id("org.springframework.boot") version "3.3.4"
    id("io.spring.dependency-management") version "1.1.6"
    kotlin("jvm") version "1.9.25"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

2. application.yml — Propriedades Comuns

O application.yml é o arquivo de configuração principal. Use variáveis de ambiente com ${VAR_NAME} e defina valores padrão com ${VAR_NAME:valor_padrao}. A hierarquia de configuração, do menor para o maior precedência: defaults do Spring Boot, application.yml, application-{profile}.yml, variáveis de ambiente, argumentos de linha de comando.

spring:
  application:
    name: pedidos-service

  datasource:
    url: jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:pedidos}
    username: ${DB_USER:dev}
    password: ${DB_PASS:dev}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000

  jpa:
    hibernate:
      ddl-auto: validate        # nunca use create/update em produção
    show-sql: false
    properties:
      hibernate:
        format_sql: true
        default_schema: public
        jdbc:
          batch_size: 50        # inserções em lote

  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=300s

server:
  port: ${PORT:8080}
  shutdown: graceful             # aguarda requests em andamento antes de parar
  compression:
    enabled: true
    min-response-size: 1024

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized

logging:
  level:
    com.empresa.pedidos: DEBUG
    org.hibernate.SQL: DEBUG

3. @ConfigurationProperties — Configuração Tipada

Em vez de @Value espalhado, agrupe propriedades relacionadas em uma classe ou record tipado. O @ConfigurationProperties faz o binding automático e o @Validated aplica Bean Validation nas propriedades.

// Classe de propriedades tipada — preferir record (Java 16+)
@ConfigurationProperties(prefix = "pedidos.pagamento")
@Validated
public record PagamentoProperties(
    @NotBlank  String gatewayUrl,
    @NotBlank  String apiKey,
    @Min(1000) @Max(30000) int timeoutMs,
    @Min(1)    int maxRetries,
    boolean    sandboxMode
) {}
# application.yml
pedidos:
  pagamento:
    gateway-url: https://gateway.pagamento.com/v2
    api-key: ${PAYMENT_API_KEY}
    timeout-ms: 5000
    max-retries: 3
    sandbox-mode: false
// Habilitar e injetar
@SpringBootApplication
@EnableConfigurationProperties(PagamentoProperties.class)
public class PedidosApplication { ... }

@Service
public class PagamentoService {
    private final PagamentoProperties props;
    public PagamentoService(PagamentoProperties props) { this.props = props; }

    public void processar(Pagamento p) {
        if (props.sandboxMode()) {
            log.info("Sandbox: simulando pagamento para {}", p.id());
            return;
        }
        // usa props.gatewayUrl(), props.apiKey() etc.
    }
}

4. Profiles — Configuração por Ambiente

Profiles permitem ativar beans e configurações específicos por ambiente (dev, test, prod). Use @Profile nos beans e arquivos application-{profile}.yml para sobrescrever propriedades.

# application-dev.yml
spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop      # recria o schema a cada reinício em dev
  datasource:
    url: jdbc:h2:mem:devdb       # banco em memória em dev

logging:
  level:
    com.empresa: TRACE
# application-prod.yml
spring:
  datasource:
    url: jdbc:postgresql://prod-db:5432/pedidos
  jpa:
    show-sql: false
logging:
  level:
    root: WARN
// Bean ativo apenas em dev — ex.: dados de seed
@Component
@Profile("dev")
public class DevDataLoader implements CommandLineRunner {
    private final PedidoRepository repo;
    public DevDataLoader(PedidoRepository repo) { this.repo = repo; }

    @Override
    public void run(String... args) {
        repo.saveAll(PedidoFixtures.sample());
        log.info("Dev data carregado: {} pedidos", repo.count());
    }
}

// Ativar perfil em testes
@SpringBootTest
@ActiveProfiles("test")
class PedidoServiceTest { ... }

5. @RestController Completo

Cada método de um controller mapeia um endpoint HTTP. Use injeção por construtor (sem @Autowired), que é mais testável. ResponseEntity dá controle total sobre status HTTP e headers.

@RestController
@RequestMapping("/api/pedidos")
@Tag(name = "Pedidos", description = "CRUD de pedidos")  // OpenAPI
public class PedidoController {

    private final PedidoService service;

    public PedidoController(PedidoService service) {
        this.service = service;
    }

    // GET com paginação e filtro
    @GetMapping
    public ResponseEntity<Page<PedidoResponse>> listar(
            @RequestParam(defaultValue = "0")  int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false)    String status,
            Pageable pageable) {
        return ResponseEntity.ok(service.listar(status, pageable));
    }

    // GET por ID
    @GetMapping("/{id}")
    public ResponseEntity<PedidoResponse> buscarPorId(@PathVariable UUID id) {
        return ResponseEntity.ok(service.buscarPorId(id));
    }

    // POST — cria recurso e retorna 201 com Location header
    @PostMapping
    public ResponseEntity<PedidoResponse> criar(@RequestBody @Valid CriarPedidoRequest req) {
        PedidoResponse criado = service.criar(req);
        URI location = URI.create("/api/pedidos/" + criado.id());
        return ResponseEntity.created(location).body(criado);
    }

    // PUT — substitui o recurso completo
    @PutMapping("/{id}")
    public ResponseEntity<PedidoResponse> atualizar(
            @PathVariable UUID id,
            @RequestBody @Valid AtualizarPedidoRequest req) {
        return ResponseEntity.ok(service.atualizar(id, req));
    }

    // PATCH — atualização parcial
    @PatchMapping("/{id}/status")
    public ResponseEntity<Void> atualizarStatus(
            @PathVariable UUID id,
            @RequestBody @Valid AtualizarStatusRequest req) {
        service.atualizarStatus(id, req.status());
        return ResponseEntity.noContent().build();  // 204
    }

    // DELETE
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deletar(@PathVariable UUID id) {
        service.deletar(id);
        return ResponseEntity.noContent().build();  // 204
    }

    // Busca com múltiplos parâmetros
    @GetMapping("/busca")
    public ResponseEntity<List<PedidoResponse>> buscar(
            @RequestParam String clienteId,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate de,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate ate) {
        return ResponseEntity.ok(service.buscarPorClienteEPeriodo(clienteId, de, ate));
    }
}

6. Bean Validation — Validação de Entrada

As anotações da Jakarta Validation marcam as restrições nos DTOs. O @Valid no parâmetro do controller dispara a validação; em caso de falha, lança MethodArgumentNotValidException. Use @Valid em objetos aninhados para validação recursiva.

public record CriarPedidoRequest(
    @NotBlank(message = "clienteId é obrigatório")
    String clienteId,

    @NotEmpty
    @Size(min = 1, max = 50, message = "Pedido deve ter entre 1 e 50 itens")
    List<@Valid ItemPedidoRequest> itens,

    @Email(message = "Email de contato inválido")
    String emailContato,

    @DecimalMin(value = "0.0", inclusive = true)
    @DecimalMax(value = "100.0")
    BigDecimal descontoPercent,

    @Future(message = "Data de entrega deve ser no futuro")
    LocalDate dataEntrega
) {}

public record ItemPedidoRequest(
    @NotBlank               String sku,
    @Min(1) @Max(100)       int quantidade,
    @NotNull @Positive      BigDecimal precoUnitario
) {}

Constraint customizada:

// 1. Definir a anotação
@Documented
@Constraint(validatedBy = CpfCnpjValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CpfCnpj {
    String message() default "CPF ou CNPJ inválido";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. Implementar o validador
public class CpfCnpjValidator implements ConstraintValidator<CpfCnpj, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext ctx) {
        if (value == null) return true; // use @NotNull separado
        String digits = value.replaceAll("[^0-9]", "");
        return digits.length() == 11 ? validarCpf(digits) : validarCnpj(digits);
    }
}

// 3. Usar
public record ClienteRequest(
    @NotBlank @CpfCnpj String documento
) {}

7. ResponseEntity e Respostas HTTP

ResponseEntity<T> encapsula o status, os headers e o corpo. Use os métodos estáticos do builder para expressar intenção claramente.

// Respostas mais comuns
ResponseEntity.ok(body)                          // 200 com corpo
ResponseEntity.ok().build()                      // 200 sem corpo
ResponseEntity.created(location).body(body)      // 201 com Location
ResponseEntity.accepted().body(body)             // 202 Accepted (operação assíncrona)
ResponseEntity.noContent().build()               // 204 sem corpo
ResponseEntity.notFound().build()                // 404 sem corpo
ResponseEntity.badRequest().body(problemDetail)  // 400 com ProblemDetail

// Customizando headers
return ResponseEntity.ok()
    .header("X-Total-Count", String.valueOf(total))
    .header("X-Correlation-Id", correlationId)
    .contentType(MediaType.APPLICATION_JSON)
    .body(lista);

// Stream de arquivo
@GetMapping("/relatorio/{id}/download")
public ResponseEntity<Resource> downloadRelatorio(@PathVariable UUID id) {
    Resource arquivo = service.gerarRelatorio(id);
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"relatorio-" + id + ".pdf\"")
        .contentType(MediaType.APPLICATION_PDF)
        .body(arquivo);
}

8. @RestControllerAdvice — Tratamento Global de Erros

@RestControllerAdvice intercepta exceções de qualquer controller. A partir do Spring 6 (Boot 3+), use ProblemDetail que implementa o padrão RFC 9457 (antigo RFC 7807).

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 404 — recurso não encontrado
    @ExceptionHandler(RecursoNaoEncontradoException.class)
    public ProblemDetail handleNotFound(RecursoNaoEncontradoException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        pd.setTitle("Recurso não encontrado");
        pd.setProperty("recursoId", ex.getRecursoId());
        return pd;
    }

    // 422 — validação de entrada
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.UNPROCESSABLE_ENTITY);
        pd.setTitle("Dados de entrada inválidos");
        List<String> erros = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        pd.setProperty("erros", erros);
        return pd;
    }

    // 409 — conflito de estado de negócio
    @ExceptionHandler(ConflictoDeNegocioException.class)
    public ProblemDetail handleConflict(ConflictoDeNegocioException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
        pd.setTitle("Conflito de negócio");
        return pd;
    }

    // 400 — parâmetro de URL inválido (ex: UUID mal formatado)
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ProblemDetail handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
        String msg = "Parâmetro '%s' com valor '%s' é inválido".formatted(ex.getName(), ex.getValue());
        return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, msg);
    }

    // 500 — fallback genérico
    @ExceptionHandler(Exception.class)
    public ProblemDetail handleGeneric(Exception ex, HttpServletRequest req) {
        log.error("Erro não tratado em {}", req.getRequestURI(), ex);
        return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Erro interno do servidor");
    }
}

9. Spring Data JPA — Entities e Repositórios

@Entity mapeia a classe para uma tabela. JpaRepository fornece CRUD, paginação e ordenação prontos. Query methods derivam SQL do nome do método; @Query permite JPQL ou SQL nativo para casos mais complexos.

@Entity
@Table(name = "pedidos",
       indexes = @Index(name = "idx_pedidos_cliente_id", columnList = "cliente_id"))
@EntityListeners(AuditingEntityListener.class)  // auditoria automática
public class Pedido {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "cliente_id", nullable = false)
    private String clienteId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private StatusPedido status;

    @OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<ItemPedido> itens = new ArrayList<>();

    @Column(precision = 12, scale = 2)
    private BigDecimal valorTotal;

    @CreatedDate
    @Column(updatable = false)
    private Instant criadoEm;

    @LastModifiedDate
    private Instant atualizadoEm;

    // getters/setters ou use Lombok @Getter @Setter
}

@Entity
@Table(name = "itens_pedido")
public class ItemPedido {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "pedido_id", nullable = false)
    private Pedido pedido;

    @Column(nullable = false)
    private String sku;

    @Min(1)
    private int quantidade;

    @Column(precision = 10, scale = 2)
    private BigDecimal precoUnitario;
}
public interface PedidoRepository extends JpaRepository<Pedido, UUID> {

    // Derivação de nome de método — Spring gera o JPQL automaticamente
    List<Pedido> findByClienteId(String clienteId);
    List<Pedido> findByStatusAndClienteId(StatusPedido status, String clienteId);
    Page<Pedido> findByStatus(StatusPedido status, Pageable pageable);
    boolean existsByClienteIdAndStatus(String clienteId, StatusPedido status);
    long countByStatus(StatusPedido status);

    // JPQL — para queries com joins ou agregações
    @Query("SELECT p FROM Pedido p JOIN FETCH p.itens WHERE p.id = :id")
    Optional<Pedido> findByIdComItens(@Param("id") UUID id);

    // JPQL com projeção — retorna apenas os campos necessários
    @Query("SELECT new com.empresa.pedidos.PedidoResumo(p.id, p.status, p.valorTotal) " +
           "FROM Pedido p WHERE p.clienteId = :clienteId ORDER BY p.criadoEm DESC")
    List<PedidoResumo> findResumosByClienteId(@Param("clienteId") String clienteId);

    // SQL nativo — quando JPQL não é suficiente
    @Query(value = """
        SELECT p.* FROM pedidos p
        WHERE p.criado_em BETWEEN :de AND :ate
          AND p.status = :status
        ORDER BY p.valor_total DESC
        LIMIT :limite
        """, nativeQuery = true)
    List<Pedido> findTopPedidosPorPeriodo(
        @Param("de") Instant de,
        @Param("ate") Instant ate,
        @Param("status") String status,
        @Param("limite") int limite
    );

    // Atualização em massa com @Modifying
    @Modifying
    @Query("UPDATE Pedido p SET p.status = :novoStatus WHERE p.status = :statusAtual AND p.criadoEm < :antes")
    int cancelarPedidosAbandonados(
        @Param("statusAtual") StatusPedido statusAtual,
        @Param("novoStatus") StatusPedido novoStatus,
        @Param("antes") Instant antes
    );
}

10. Relacionamentos JPA — Fetch Strategies e N+1

O problema N+1 ocorre quando você carrega uma lista de N entidades e, para cada uma, dispara uma query adicional para buscar a associação. A solução é usar JOIN FETCH na query ou @EntityGraph.

// ERRADO — LAZY por padrão, cada pedido dispara uma query para buscar itens
List<Pedido> pedidos = pedidoRepo.findAll();
for (Pedido p : pedidos) {
    p.getItens().size(); // dispara SELECT para cada pedido! N+1!
}

// CERTO — JOIN FETCH carrega tudo em uma única query
@Query("SELECT DISTINCT p FROM Pedido p JOIN FETCH p.itens WHERE p.clienteId = :clienteId")
List<Pedido> findByClienteIdComItens(@Param("clienteId") String clienteId);

// CERTO — @EntityGraph para reutilizar sem escrever JPQL
@EntityGraph(attributePaths = {"itens", "pagamento"})
List<Pedido> findByClienteId(String clienteId);

Mapeamentos completos:

// @OneToMany — Pedido tem vários Itens
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemPedido> itens = new ArrayList<>();

// @ManyToOne — Item pertence a um Pedido
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pedido_id")
private Pedido pedido;

// @ManyToMany — Pedido tem vários Cupons e vice-versa
@ManyToMany
@JoinTable(name = "pedidos_cupons",
    joinColumns = @JoinColumn(name = "pedido_id"),
    inverseJoinColumns = @JoinColumn(name = "cupom_id"))
private Set<Cupom> cupons = new HashSet<>();

// @OneToOne
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "pagamento_id", unique = true)
private Pagamento pagamento;

11. @Transactional — Propagação e Isolamento

@Transactional demarca uma transação de banco. Por padrão, faz rollback automático em RuntimeException. Cuidado: só funciona em chamadas vindas de fora da classe (proxy Spring) — uma chamada interna não ativa a transação.

@Service
@Transactional(readOnly = true)  // padrão read-only para queries (melhor performance)
public class PedidoService {

    // Sobrescreve para operações de escrita
    @Transactional
    public PedidoResponse criar(CriarPedidoRequest req) {
        Pedido pedido = new Pedido(req);
        pedidoRepo.save(pedido);
        eventPublisher.publishEvent(new PedidoCriadoEvent(pedido.getId()));
        return PedidoResponse.from(pedido);
    }

    // Rollback em checked exception — padrão é apenas RuntimeException
    @Transactional(rollbackFor = PagamentoException.class)
    public void confirmarPagamento(UUID pedidoId, DadosPagamento dados) throws PagamentoException {
        Pedido pedido = pedidoRepo.findById(pedidoId).orElseThrow();
        Pagamento pag = pagamentoGateway.cobrar(dados);  // throws PagamentoException
        pedido.marcarComoPago(pag.getId());
        pedidoRepo.save(pedido);
    }

    // REQUIRES_NEW — abre nova transação, não interfere na transação pai
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void registrarTentativaPagamento(UUID pedidoId, String resultado) {
        // se isso falhar, não faz rollback da transação do chamador
        auditRepo.save(new AuditLog(pedidoId, resultado));
    }
}

Propagações mais usadas:

PropagaçãoComportamento
REQUIRED (padrão)Usa transação existente ou cria uma nova
REQUIRES_NEWSempre cria nova transação, suspende a existente
SUPPORTSUsa transação se existir, senão executa sem
NOT_SUPPORTEDSempre executa sem transação, suspende a existente
NEVERLança exceção se houver transação ativa
MANDATORYLança exceção se não houver transação ativa

Isolamentos:

IsolamentoProtege contra
READ_UNCOMMITTEDNada (mais rápido, menos seguro)
READ_COMMITTED (padrão PostgreSQL)Dirty reads
REPEATABLE_READDirty reads + Non-repeatable reads
SERIALIZABLETudo (mais lento, mais seguro)

12. Spring Security — SecurityFilterChain e JWT

A partir do Spring Security 6 (Boot 3+), a configuração usa SecurityFilterChain como bean. Não há mais WebSecurityConfigurerAdapter.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // habilita @PreAuthorize, @PostAuthorize etc.
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;
    private final UserDetailsService userDetailsService;

    public SecurityConfig(JwtAuthFilter jwtAuthFilter, UserDetailsService uds) {
        this.jwtAuthFilter = jwtAuthFilter;
        this.userDetailsService = uds;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())               // desabilita CSRF para APIs stateless
            .sessionManagement(sm -> sm
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**", "/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/produtos/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    @Bean
    public AuthenticationManager authManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

Filtro JWT:

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String authHeader = req.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(req, res);
            return;
        }

        String token = authHeader.substring(7);
        String username = jwtService.extractUsername(token);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails user = userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(token, user)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(req, res);
    }
}

UserDetails customizado:

@Entity
@Table(name = "usuarios")
public class Usuario implements UserDetails {
    @Id @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    private String email;
    private String senha;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "usuario_roles")
    @Enumerated(EnumType.STRING)
    private Set<Role> roles;

    @Override public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream().map(r -> new SimpleGrantedAuthority("ROLE_" + r.name())).toList();
    }
    @Override public String getPassword() { return senha; }
    @Override public String getUsername() { return email; }
    @Override public boolean isAccountNonExpired()    { return true; }
    @Override public boolean isAccountNonLocked()     { return true; }
    @Override public boolean isCredentialsNonExpired(){ return true; }
    @Override public boolean isEnabled()              { return true; }
}

@PreAuthorize nos métodos:

@Service
public class PedidoService {

    @PreAuthorize("hasRole('ADMIN') or #clienteId == authentication.name")
    public List<PedidoResponse> listarPorCliente(String clienteId) { ... }

    @PreAuthorize("hasRole('ADMIN')")
    public void cancelar(UUID id) { ... }

    @PostAuthorize("returnObject.clienteId == authentication.name or hasRole('ADMIN')")
    public PedidoResponse buscarPorId(UUID id) { ... }
}

13. Spring Security — OAuth2 Resource Server com JWT

Quando há um Authorization Server externo (Keycloak, Auth0, Okta), configure o Spring Boot apenas como Resource Server — ele valida os tokens mas não os emite.

# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://keycloak.empresa.com/realms/pedidos
          # ou: jwk-set-uri: https://keycloak.empresa.com/realms/pedidos/protocol/openid-connect/certs
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter())))
            .build();
    }

    // Extrai roles do claim aninhado "realm_access.roles" do Keycloak
    // ATENÇÃO: JwtGrantedAuthoritiesConverter só suporta claims de nível raiz.
    // Para o Keycloak (claim aninhado), é necessário um Converter customizado.
    @Bean
    public JwtAuthenticationConverter jwtConverter() {
        // Converter customizado para extrair roles aninhadas do Keycloak
        Converter<Jwt, Collection<GrantedAuthority>> realmRolesConverter = jwt -> {
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            if (realmAccess == null) return List.of();
            @SuppressWarnings("unchecked")
            List<String> roles = (List<String>) realmAccess.getOrDefault("roles", List.of());
            return roles.stream()
                .map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role))
                .toList();
        };

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(realmRolesConverter);
        return converter;
    }

    // Alternativa: usar claim plano "scp" ou "scope" (padrão OAuth2/OIDC)
    // — JwtGrantedAuthoritiesConverter funciona diretamente aqui
    @Bean
    public JwtAuthenticationConverter scopeBasedConverter() {
        JwtGrantedAuthoritiesConverter grantedConverter = new JwtGrantedAuthoritiesConverter();
        grantedConverter.setAuthoritiesClaimName("scp");  // claim plano — funciona
        grantedConverter.setAuthorityPrefix("SCOPE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(grantedConverter);
        return converter;
    }
}

14. Testing — @WebMvcTest, @DataJpaTest, @SpringBootTest

O Spring Boot oferece três camadas de teste com contextos progressivamente maiores.

// @WebMvcTest — testa apenas a camada de controller; mais rápido
@WebMvcTest(PedidoController.class)
class PedidoControllerTest {

    @Autowired
    private MockMvcTester mvc;   // MockMvcTester (Boot 3.4+) ou @Autowired MockMvc

    @MockitoBean                 // substitui @MockBean no Boot 3.4+
    private PedidoService service;

    @Test
    void deveRetornar201AoCriarPedido() {
        var req = new CriarPedidoRequest("cliente-1", List.of(new ItemPedidoRequest("SKU-1", 2, new BigDecimal("50.00"))), null, null, null);
        var resp = new PedidoResponse(UUID.randomUUID(), "cliente-1", StatusPedido.ABERTO, new BigDecimal("100.00"));

        given(service.criar(any())).willReturn(resp);

        assertThat(mvc.post().uri("/api/pedidos")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""{"clienteId":"cliente-1","itens":[{"sku":"SKU-1","quantidade":2,"precoUnitario":50.00}]}"""))
            .hasStatus(HttpStatus.CREATED)
            .bodyJson().extractingPath("$.status").isEqualTo("ABERTO");
    }

    @Test
    void deveRetornar422ComCampoInvalido() {
        assertThat(mvc.post().uri("/api/pedidos")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""{"clienteId":"","itens":[]}"""))
            .hasStatus(HttpStatus.UNPROCESSABLE_ENTITY);
    }
}
// @DataJpaTest — testa apenas a camada JPA; usa H2 em memória por padrão
@DataJpaTest
@ActiveProfiles("test")
class PedidoRepositoryTest {

    @Autowired
    private PedidoRepository repo;

    @Autowired
    private TestEntityManager em;

    @Test
    void deveEncontrarPedidosPorCliente() {
        var pedido = new Pedido("cliente-42", StatusPedido.ABERTO);
        em.persistAndFlush(pedido);

        List<Pedido> resultado = repo.findByClienteId("cliente-42");

        assertThat(resultado).hasSize(1);
        assertThat(resultado.get(0).getClienteId()).isEqualTo("cliente-42");
    }
}
// Testcontainers — banco real em container Docker
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
class PedidoIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private PedidoService service;

    @Test
    void deveProcessarFluxoCompletoDeCompra() {
        var req = CriarPedidoRequest.builder()
            .clienteId("cliente-1")
            .itens(List.of(new ItemPedidoRequest("SKU-ABC", 3, new BigDecimal("29.90"))))
            .build();

        PedidoResponse resp = service.criar(req);

        assertThat(resp.status()).isEqualTo(StatusPedido.ABERTO);
        assertThat(resp.valorTotal()).isEqualByComparingTo("89.70");
    }
}

15. Spring Cache — @Cacheable, @CacheEvict, @CachePut

Cache declarativo com anotações. Suporta vários backends: Caffeine (in-process), Redis (distribuído), EhCache etc. A chave é gerada a partir dos parâmetros por padrão, mas pode ser customizada via SpEL.

@Service
@CacheConfig(cacheNames = "pedidos")  // nome do cache padrão para a classe
public class PedidoService {

    // Cache na leitura — executa o método apenas se não houver cache
    @Cacheable(key = "#id", unless = "#result == null")
    public PedidoResponse buscarPorId(UUID id) {
        return pedidoRepo.findById(id)
            .map(PedidoResponse::from)
            .orElseThrow(() -> new RecursoNaoEncontradoException(id));
    }

    // Atualiza o cache após salvar
    @CachePut(key = "#result.id")
    @Transactional
    public PedidoResponse atualizar(UUID id, AtualizarPedidoRequest req) {
        Pedido pedido = pedidoRepo.findById(id).orElseThrow();
        pedido.atualizar(req);
        return PedidoResponse.from(pedidoRepo.save(pedido));
    }

    // Invalida o cache ao deletar
    @CacheEvict(key = "#id")
    @Transactional
    public void deletar(UUID id) {
        pedidoRepo.deleteById(id);
    }

    // Invalida todo o cache de uma vez
    @CacheEvict(allEntries = true)
    public void invalidarTodosOsPedidos() {}
}
# Caffeine (in-process — recomendado para instância única)
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=5000,expireAfterWrite=600s,recordStats

# Redis (distribuído — para múltiplas instâncias)
spring:
  cache:
    type: redis
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: 6379

16. Spring Events — ApplicationEvent e @TransactionalEventListener

Events desacoplam o publicador do consumidor dentro da mesma JVM. @TransactionalEventListener garante que o handler só executa após o commit da transação, evitando processar eventos de transações que possam fazer rollback.

// 1. Definir o evento (record Java — imutável por padrão)
public record PedidoCriadoEvent(UUID pedidoId, String clienteId, BigDecimal valorTotal) {}

// 2. Publicar o evento no serviço
@Service
@RequiredArgsConstructor
public class PedidoService {
    private final ApplicationEventPublisher eventPublisher;
    private final PedidoRepository repo;

    @Transactional
    public PedidoResponse criar(CriarPedidoRequest req) {
        Pedido pedido = repo.save(new Pedido(req));
        // O evento só será processado após o commit da transação
        eventPublisher.publishEvent(new PedidoCriadoEvent(pedido.getId(), pedido.getClienteId(), pedido.getValorTotal()));
        return PedidoResponse.from(pedido);
    }
}

// 3. Consumir o evento
@Component
@Slf4j
public class NotificacaoListener {

    private final EmailService emailService;
    private final EstoqueService estoqueService;

    // Executa APÓS o commit — seguro para side effects
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onPedidoCriado(PedidoCriadoEvent event) {
        emailService.enviarConfirmacao(event.clienteId(), event.pedidoId());
        log.info("Email de confirmação enviado para pedido {}", event.pedidoId());
    }

    // Outro listener para o mesmo evento — executa de forma independente
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Async  // em thread separada para não bloquear
    public void atualizarEstoque(PedidoCriadoEvent event) {
        estoqueService.reservar(event.pedidoId());
    }
}

17. @Scheduled — Tarefas Agendadas

@EnableScheduling habilita o agendamento. @Scheduled marca o método que será executado. Use cron para horários precisos, fixedDelay quando quer aguardar o fim da execução anterior, e fixedRate para intervalos fixos independentes.

@SpringBootApplication
@EnableScheduling
public class PedidosApplication { ... }

@Component
@Slf4j
public class PedidoScheduler {

    private final PedidoService service;
    private final RelatorioService relatorioService;

    // Cron — executa todo dia às 02:00
    @Scheduled(cron = "0 0 2 * * *")
    public void cancelarPedidosAbandonados() {
        int cancelados = service.cancelarAbandonados(Duration.ofHours(48));
        log.info("Pedidos cancelados por abandono: {}", cancelados);
    }

    // fixedDelay — aguarda 30s após o fim da execução anterior
    @Scheduled(fixedDelay = 30_000, initialDelay = 10_000)
    public void sincronizarStatusPagamento() {
        service.sincronizarPagamentosPendentes();
    }

    // fixedRate — executa a cada 5 minutos, independente de quanto durou
    @Scheduled(fixedRate = 300_000)
    public void atualizarMetricas() {
        relatorioService.atualizarCache();
    }

    // Cron com zona de horário — importante para servidores em UTC
    @Scheduled(cron = "0 0 8 * * MON-FRI", zone = "America/Sao_Paulo")
    public void enviarRelatorioDiario() {
        relatorioService.enviarEmailDiario();
    }
}

Referência rápida de expressão cron (6 campos):

Segundo Minuto Hora DiaMes Mes DiaSemana
  0       0     2    *     *      *       → todo dia às 02:00:00
  0      */15   *    *     *      *       → a cada 15 minutos
  0       0    8-18  *     *   MON-FRI    → horário comercial, de hora em hora
  0       0     0    1     *      *       → primeiro dia do mês à meia-noite

18. Spring Actuator — Monitoramento e Health Indicators

O Actuator expõe endpoints HTTP para monitorar e gerenciar a aplicação em produção. Integra-se nativamente com Prometheus/Grafana via Micrometer.

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,loggers,env
      base-path: /internal      # prefixo para proteger com firewall/gateway
  endpoint:
    health:
      show-details: when-authorized
      show-components: when-authorized
  info:
    env:
      enabled: true
  metrics:
    tags:
      application: ${spring.application.name}  # tag em todas as métricas
      environment: ${spring.profiles.active:default}

Health Indicator customizado:

@Component
public class GatewayPagamentoHealthIndicator implements HealthIndicator {

    private final PagamentoGatewayClient client;

    @Override
    public Health health() {
        try {
            PingResponse ping = client.ping();
            if (ping.isOk()) {
                return Health.up()
                    .withDetail("latencyMs", ping.getLatencyMs())
                    .withDetail("version", ping.getVersion())
                    .build();
            }
            return Health.down()
                .withDetail("motivo", "Gateway retornou status " + ping.getStatus())
                .build();
        } catch (Exception ex) {
            return Health.down(ex)
                .withDetail("motivo", "Timeout ou erro de conexão")
                .build();
        }
    }
}

Métricas customizadas com Micrometer:

@Service
@RequiredArgsConstructor
public class PedidoService {

    private final MeterRegistry meterRegistry;

    @Transactional
    public PedidoResponse criar(CriarPedidoRequest req) {
        PedidoResponse resp = // ...

        // Contador com tags
        meterRegistry.counter("pedidos.criados",
            "status", "sucesso",
            "origem", req.origem()).increment();

        // Timer — mede duração
        Timer timer = meterRegistry.timer("pedidos.processamento",
            "tipo", req.tipo().name());
        // use timer.record() em volta do processamento

        return resp;
    }
}

19. WebFlux — Programação Reativa

WebFlux é a alternativa reativa ao Spring MVC. Usa Reactor (Mono/Flux) e funciona com servidores não-bloqueantes (Netty por padrão). Ideal quando há muita I/O concorrente.

@RestController
@RequestMapping("/api/pedidos")
public class PedidoReativoController {

    private final PedidoReativoService service;

    @GetMapping("/{id}")
    public Mono<PedidoResponse> buscar(@PathVariable UUID id) {
        return service.buscar(id)
            .switchIfEmpty(Mono.error(new RecursoNaoEncontradoException(id)));
    }

    @GetMapping
    public Flux<PedidoResponse> listar(@RequestParam(defaultValue = "0") int page) {
        return service.listar(page);
    }

    @PostMapping
    public Mono<ResponseEntity<PedidoResponse>> criar(@RequestBody @Valid Mono<CriarPedidoRequest> req) {
        return req.flatMap(service::criar)
            .map(criado -> ResponseEntity.created(URI.create("/api/pedidos/" + criado.id())).body(criado));
    }
}

Operadores essenciais do Reactor:

// map — transformação síncrona (sem IO)
Mono<PedidoResponse> resp = mono.map(PedidoResponse::from);

// flatMap — transformação assíncrona (retorna outro Mono/Flux)
Mono<Pagamento> pag = pedidoMono.flatMap(p -> pagamentoService.cobrar(p));

// zip / zipWith — combina dois Monos em paralelo
Mono<DetalhePedido> detalhe = Mono.zip(
    pedidoRepo.findById(id),
    clienteRepo.findById(clienteId),
    (pedido, cliente) -> new DetalhePedido(pedido, cliente)
);

// onErrorResume — fallback quando falha
Mono<Pedido> seguro = pedidoRepo.findById(id)
    .onErrorResume(TimeoutException.class, ex -> cacheRepo.findById(id));

// retry com backoff exponencial
Mono<Order> resiliente = externalApi.fetch(id)
    .retryWhen(Retry.backoff(3, Duration.ofMillis(200)).maxBackoff(Duration.ofSeconds(2)));

// filter + switchIfEmpty
Flux<Pedido> ativos = pedidoRepo.findAll()
    .filter(p -> p.getStatus() == StatusPedido.ATIVO)
    .switchIfEmpty(Flux.error(new RuntimeException("Nenhum pedido ativo")));

// collectList — converte Flux para Mono<List>
Mono<List<PedidoResponse>> lista = service.listarTodos()
    .map(PedidoResponse::from)
    .collectList();

R2DBC — JPA reativo:

public interface PedidoR2dbcRepository extends R2dbcRepository<Pedido, UUID> {
    Flux<Pedido> findByClienteId(String clienteId);
    Mono<Pedido> findByIdAndClienteId(UUID id, String clienteId);
}

20. Spring Batch — Job, Step, Reader/Processor/Writer

Spring Batch é ideal para processamento em lote: importação de CSVs, relatórios, migrações. A unidade de execução é o Job composto de Steps. Cada Step tem um ItemReader, ItemProcessor e ItemWriter.

@Configuration
@EnableBatchProcessing
public class ImportacaoPedidosBatch {

    @Bean
    public Job importarPedidosJob(JobRepository jobRepo, Step importarStep) {
        return new JobBuilder("importar-pedidos", jobRepo)
            .start(importarStep)
            .build();
    }

    @Bean
    public Step importarStep(JobRepository jobRepo,
                              PlatformTransactionManager txManager,
                              ItemReader<PedidoCsv> reader,
                              ItemProcessor<PedidoCsv, Pedido> processor,
                              ItemWriter<Pedido> writer) {
        return new StepBuilder("importar-step", jobRepo)
            .<PedidoCsv, Pedido>chunk(100, txManager)  // processa em lotes de 100
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .faultTolerant()
            .skip(ValidationException.class).skipLimit(50)  // pula até 50 erros
            .build();
    }

    @Bean
    @StepScope  // scope de step — permite injetar parâmetros do Job
    public FlatFileItemReader<PedidoCsv> csvReader(
            @Value("#{jobParameters['arquivo']}") String arquivo) {
        return new FlatFileItemReaderBuilder<PedidoCsv>()
            .name("pedido-csv-reader")
            .resource(new FileSystemResource(arquivo))
            .delimited().delimiter(",")
            .names("clienteId", "sku", "quantidade", "preco")
            .targetType(PedidoCsv.class)
            .linesToSkip(1)  // pula header
            .build();
    }

    @Bean
    public ItemProcessor<PedidoCsv, Pedido> pedidoProcessor(PedidoValidator validator) {
        return csv -> {
            validator.validate(csv);
            return Pedido.fromCsv(csv);
        };
    }

    @Bean
    public JpaItemWriter<Pedido> pedidoWriter(EntityManagerFactory emf) {
        return new JpaItemWriterBuilder<Pedido>()
            .entityManagerFactory(emf)
            .build();
    }
}

21. Anotações — Referência Rápida

AnotaçãoPara que serve
@SpringBootApplicationEntry point — @Configuration + @ComponentScan + @EnableAutoConfiguration
@RestController@Controller + @ResponseBody — retorna JSON por padrão
@RequestMappingPrefixo de rota para a classe inteira
@GetMapping / @PostMapping / @PutMapping / @PatchMapping / @DeleteMappingMapeamento de verbos HTTP
@PathVariableExtrai variável da URL (ex: /{id})
@RequestParamExtrai parâmetro de query string
@RequestBodyDesserializa o corpo JSON da request
@ValidDispara Bean Validation no argumento
@ServiceBean de lógica de negócio
@RepositoryBean de acesso a dados + tradução de exceções JPA
@ComponentBean genérico gerenciado pelo Spring
@ConfigurationClasse de configuração com @Bean
@BeanDeclara um bean dentro de uma @Configuration
@TransactionalDemarca transação; rollback em RuntimeException
@CacheableArmazena resultado em cache
@CacheEvictRemove entrada do cache
@ScheduledExecuta em agendamento (cron, fixedRate, fixedDelay)
@AsyncExecuta em thread pool separado
@EventListenerConsome ApplicationEvent
@TransactionalEventListenerConsome evento apenas após commit da transação
@PreAuthorizeAutorização por SpEL antes de executar o método
@ProfileBean ativo apenas em determinado perfil
@Value("${prop}")Injeta valor de properties/environment
@ConfigurationProperties(prefix)Binding tipado de grupo de propriedades
@ConditionalOnPropertyAtiva bean somente se a propriedade tiver valor específico
@MockitoBeanSubstitui bean por mock em testes (@MockBean até Boot 3.3)

22. Tabela de Versões

VersãoAnoPrincipais novidades
2.7.x2022Última linha 2.x; Spring Framework 5; Java 8+ suportado; namespace javax.*; Spring Security 5; spring.security.oauth2.client melhorado
3.02022Spring Framework 6; Java 17 mínimo; javax.*jakarta.* (breaking change); suporte a GraalVM native image; @HttpExchange para HTTP clients declarativos; Spring Security 6
3.22023Suporte nativo a Virtual Threads (Project Loom); RestClient — novo HTTP client síncrono fluente substituto do RestTemplate; JdbcClient — JDBC fluente; @ConditionalOnThreading
3.32024CDS (Class Data Sharing) out-of-the-box para startup mais rápido; Service Connections melhorado com Testcontainers; integração automática com Docker Compose em dev; @MockitoBean substitui @MockBean
3.42024MockMvcTester (API fluente AssertJ para testes MVC); estruturação aprimorada de logs; @ConditionalOnMissingBean melhorado; melhorias no Actuator; suporte a ConnectionDetails para mais providers
4.02025Java 17 mínimo confirmado + suporte Java 21; Spring Framework 7; suporte completo a Jakarta EE 11; revisão da API de auto-configuration; melhorias em observability (OTEL nativo)

23. WebFlux — RouterFunction, WebClient e Server-Sent Events

Spring WebFlux é o framework web reativo do Spring, introduzido no Spring Framework 5. Diferentemente do Spring MVC (que bloqueia threads em I/O), o WebFlux usa o modelo de programação reativo baseado no Reactor (Project Reactor) — sem bloqueio, com back-pressure. A dependência troca spring-boot-starter-web por spring-boot-starter-webflux.

Dependência (Gradle):

implementation("org.springframework.boot:spring-boot-starter-webflux")

@RestController reativo com Mono<T> e Flux<T>:

Mono<T> representa 0 ou 1 resultado; Flux<T> representa 0 ou N resultados. O Spring WebFlux suporta exatamente as mesmas anotações MVC — a diferença está nos tipos de retorno.

@RestController
@RequestMapping("/produtos")
public class ProdutoController {

    private final ProdutoRepository repository;

    public ProdutoController(ProdutoRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public Flux<Produto> listarTodos() {
        return repository.findAll();
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<Produto>> buscarPorId(@PathVariable Long id) {
        return repository.findById(id)
            .map(ResponseEntity::ok)
            .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Produto> criar(@RequestBody @Valid Mono<ProdutoRequest> request) {
        return request
            .map(req -> new Produto(req.nome(), req.preco()))
            .flatMap(repository::save);
    }
}

RouterFunction — alternativa funcional ao MVC:

Em vez de anotações, você define rotas como funções puras. Útil quando quer evitar reflexão ou prefere composição funcional.

@Configuration
public class ProdutoRouter {

    @Bean
    public RouterFunction<ServerResponse> routes(ProdutoHandler handler) {
        return RouterFunctions.route()
            .GET("/produtos", handler::listarTodos)
            .GET("/produtos/{id}", handler::buscarPorId)
            .POST("/produtos", handler::criar)
            .build();
    }
}

@Component
public class ProdutoHandler {

    private final ProdutoRepository repository;

    public ProdutoHandler(ProdutoRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listarTodos(ServerRequest request) {
        return ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(repository.findAll(), Produto.class);
    }

    public Mono<ServerResponse> buscarPorId(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return repository.findById(id)
            .flatMap(produto -> ServerResponse.ok().bodyValue(produto))
            .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> criar(ServerRequest request) {
        return request.bodyToMono(ProdutoRequest.class)
            .map(req -> new Produto(req.nome(), req.preco()))
            .flatMap(repository::save)
            .flatMap(salvo -> ServerResponse.status(HttpStatus.CREATED).bodyValue(salvo));
    }
}

WebClient — substituto reativo do RestTemplate:

O WebClient é não-bloqueante, fluente e totalmente integrado com o Reactor. O Spring Boot auto-configura um WebClient.Builder para injeção.

@Service
public class PagamentoClient {

    private final WebClient webClient;

    public PagamentoClient(WebClient.Builder builder) {
        this.webClient = builder
            .baseUrl("https://api.pagamentos.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }

    public Mono<PagamentoResponse> processar(PagamentoRequest request) {
        return webClient.post()
            .uri("/processar")
            .bodyValue(request)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, resp ->
                resp.bodyToMono(String.class).map(body -> new PagamentoException(body)))
            .bodyToMono(PagamentoResponse.class)
            .timeout(Duration.ofSeconds(10))
            .retryWhen(Retry.backoff(3, Duration.ofMillis(200)));
    }

    public Flux<Produto> listarProdutos() {
        return webClient.get()
            .uri("/produtos")
            .retrieve()
            .bodyToFlux(Produto.class);
    }
}

Server-Sent Events com Flux:

SSE (Server-Sent Events) permite que o servidor empurre dados para o cliente continuamente. Com WebFlux, basta retornar Flux<T> com o media type correto.

@GetMapping(value = "/eventos", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamEventos() {
    return Flux.interval(Duration.ofSeconds(1))
        .map(seq -> ServerSentEvent.<String>builder()
            .id(String.valueOf(seq))
            .event("mensagem")
            .data("Evento número " + seq)
            .build());
}

// Exemplo com dados de negócio — stream de cotações
@GetMapping(value = "/cotacoes/{ativo}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<CotacaoEvent> streamCotacoes(@PathVariable String ativo) {
    return cotacaoService.streamPorAtivo(ativo)
        .delayElements(Duration.ofMillis(500));
}

Quando usar WebFlux vs MVC:

SituaçãoRecomendação
Muitas conexões simultâneas com I/O (APIs externas, WebSockets, SSE)WebFlux — threads não ficam bloqueadas
CRUD simples com banco relacional (JDBC)MVC — JDBC é bloqueante; WebFlux não ajuda
Banco reativo (R2DBC, MongoDB reativo)WebFlux — aproveita o pipeline reativo de ponta a ponta
Equipe com experiência em programação reativaWebFlux
Integração com libs que bloqueiam threadsMVC — misturar bloqueante com reativo é perigoso

24. Spring Security — JWT Avançado: @PreAuthorize e Claims do Token

Com spring-boot-starter-oauth2-resource-server, o Spring Boot configura automaticamente sua API para validar tokens JWT emitidos por um Authorization Server (Keycloak, Okta, Auth0, etc.). A configuração mínima é apenas uma propriedade no application.yml.

Dependência:

implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")

Configuração JWT via application.yml:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: https://auth.empresa.com/realms/app/protocol/openid-connect/certs
          # Alternativa: issuer-uri (faz discovery automático do jwk-set-uri)
          # issuer-uri: https://auth.empresa.com/realms/app

SecurityConfig com JWT:

@Configuration
@EnableMethodSecurity   // habilita @PreAuthorize, @PostAuthorize, @Secured
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/produtos/**").hasAuthority("SCOPE_produtos:read")
                .requestMatchers(HttpMethod.POST, "/produtos").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))
            )
            .build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter rolesConverter = new JwtGrantedAuthoritiesConverter();
        rolesConverter.setAuthoritiesClaimName("realm_access.roles");  // claim do Keycloak
        rolesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            Collection<GrantedAuthority> roles = rolesConverter.convert(jwt);
            // combina roles + scopes do token
            Collection<GrantedAuthority> scopes = new JwtGrantedAuthoritiesConverter().convert(jwt);
            List<GrantedAuthority> all = new ArrayList<>(roles);
            all.addAll(scopes);
            return all;
        });
        return converter;
    }
}

@PreAuthorize com claims do token:

@RestController
@RequestMapping("/pedidos")
public class PedidoController {

    // Verifica role do Keycloak
    @GetMapping
    @PreAuthorize("hasRole('USER')")
    public List<PedidoResponse> listar(JwtAuthenticationToken auth) {
        String userId = auth.getToken().getSubject();  // sub do JWT
        return pedidoService.listarPorUsuario(userId);
    }

    // Verifica scope OAuth2
    @PostMapping
    @PreAuthorize("hasAuthority('SCOPE_pedidos:write')")
    public ResponseEntity<PedidoResponse> criar(@RequestBody @Valid PedidoRequest req) {
        return ResponseEntity.status(HttpStatus.CREATED).body(pedidoService.criar(req));
    }

    // Acessa claim customizado do token
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @pedidoService.pertenceAoUsuario(#id, authentication.token.subject)")
    public void deletar(@PathVariable Long id) {
        pedidoService.deletar(id);
    }

    // Injeta dados do token diretamente
    @GetMapping("/perfil")
    public Map<String, Object> perfil(@AuthenticationPrincipal Jwt jwt) {
        return Map.of(
            "sub", jwt.getSubject(),
            "email", jwt.getClaimAsString("email"),
            "roles", jwt.getClaimAsStringList("realm_access")
        );
    }
}

25. Spring Cloud — Essencial

Spring Cloud fornece padrões de microserviços (configuração centralizada, service discovery, circuit breaker, load balancing) sobre o ecossistema Spring Boot. Cada componente é uma dependência independente dentro do BOM spring-cloud-dependencies.

BOM do Spring Cloud (Gradle):

extra["springCloudVersion"] = "2024.0.1"

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

Config Server — configuração centralizada:

O Config Server serve arquivos de configuração (YAML/properties) para outros microserviços a partir de um repositório Git.

// Config Server — dependência
implementation("org.springframework.cloud:spring-cloud-config-server")
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
# application.yml do Config Server
server:
  port: 8888

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/empresa/config-repo
          default-label: main
          search-paths: "{application}"  # pasta por serviço
# bootstrap.yml dos microserviços clientes (ou application.yml com spring.config.import)
spring:
  application:
    name: pedidos-service
  config:
    import: "configserver:http://localhost:8888"

Eureka — Service Discovery:

O Eureka é um registro de serviços: microserviços se registram e descobrem outros pelo nome, sem endereços fixos.

// Servidor Eureka
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")

// Cliente Eureka (microserviços)
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
# application.yml dos microserviços clientes
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

OpenFeign — HTTP client declarativo:

O Feign gera implementações de clientes HTTP em tempo de compilação a partir de interfaces anotadas. Integra automaticamente com Eureka (usa o nome do serviço como host).

implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
@SpringBootApplication
@EnableFeignClients
public class PedidosApplication { ... }

@FeignClient(name = "estoque-service", path = "/estoque")
public interface EstoqueClient {

    @GetMapping("/produtos/{sku}/disponibilidade")
    DisponibilidadeResponse verificarDisponibilidade(@PathVariable String sku);

    @PostMapping("/reservas")
    ReservaResponse reservar(@RequestBody ReservaRequest request);
}

// Uso — injeção direta como qualquer bean Spring
@Service
public class PedidoService {

    private final EstoqueClient estoque;

    public PedidoService(EstoqueClient estoque) {
        this.estoque = estoque;
    }

    public void criar(PedidoRequest req) {
        DisponibilidadeResponse disp = estoque.verificarDisponibilidade(req.sku());
        if (!disp.disponivel()) throw new EstoqueInsuficienteException(req.sku());
        // ...
    }
}

Circuit Breaker com Resilience4j:

O Circuit Breaker evita que falhas em serviços dependentes cascateiem pelo sistema. Resilience4j é a implementação recomendada no Spring Cloud 2022+.

implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
@Service
public class EstoqueService {

    private final CircuitBreakerFactory cbFactory;
    private final EstoqueClient client;

    public EstoqueService(CircuitBreakerFactory cbFactory, EstoqueClient client) {
        this.cbFactory = cbFactory;
        this.client = client;
    }

    public DisponibilidadeResponse verificar(String sku) {
        CircuitBreaker cb = cbFactory.create("estoque");
        return cb.run(
            () -> client.verificarDisponibilidade(sku),
            throwable -> new DisponibilidadeResponse(false, "Serviço indisponível")  // fallback
        );
    }
}
# Configuração do Resilience4j
resilience4j:
  circuitbreaker:
    instances:
      estoque:
        sliding-window-size: 10
        failure-rate-threshold: 50           # abre o circuito se 50% das chamadas falharem
        wait-duration-in-open-state: 10s     # tempo em que o circuito fica aberto
        permitted-number-of-calls-in-half-open-state: 3

26. Actuator — Endpoints Customizados

O Spring Boot Actuator expõe informações de saúde e métricas da aplicação. Além dos endpoints embutidos (/health, /info, /metrics), você pode criar seus próprios endpoints e health indicators para necessidades específicas do negócio.

Criar um @Endpoint customizado:

Um @Endpoint pode ser acessado via HTTP, JMX ou ambos. @ReadOperation → GET, @WriteOperation → POST, @DeleteOperation → DELETE.

@Component
@Endpoint(id = "feature-flags")   // acessível em /actuator/feature-flags
public class FeatureFlagsEndpoint {

    private final FeatureFlagService service;

    public FeatureFlagsEndpoint(FeatureFlagService service) {
        this.service = service;
    }

    @ReadOperation
    public Map<String, Boolean> listar() {
        return service.getAll();
    }

    @ReadOperation
    public FeatureFlag buscar(@Selector String nome) {
        return service.getByName(nome);
    }

    @WriteOperation
    public void ativar(@Selector String nome, boolean ativo) {
        service.setAtivo(nome, ativo);
    }
}

Expor via application.yml:

management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, prometheus, feature-flags
  endpoint:
    health:
      show-details: when-authorized
    feature-flags:
      cache:
        time-to-live: 10s

HealthIndicator customizado:

@Component
public class ServicoExternoHealthIndicator implements HealthIndicator {

    private final ServicoExternoClient client;

    public ServicoExternoHealthIndicator(ServicoExternoClient client) {
        this.client = client;
    }

    @Override
    public Health health() {
        try {
            StatusResponse status = client.status();
            if (status.ok()) {
                return Health.up()
                    .withDetail("latencia_ms", status.latencyMs())
                    .withDetail("versao", status.version())
                    .build();
            }
            return Health.down()
                .withDetail("motivo", status.errorMessage())
                .build();
        } catch (Exception e) {
            return Health.down(e)
                .withDetail("motivo", "Timeout ou conexão recusada")
                .build();
        }
    }
}

InfoContributor customizado:

O endpoint /actuator/info agrega contribuições. Útil para expor versão do build, branch do Git, ou qualquer metadado da aplicação.

@Component
public class AppInfoContributor implements InfoContributor {

    private final BuildProperties buildProperties;
    private final GitProperties gitProperties;

    public AppInfoContributor(BuildProperties build, GitProperties git) {
        this.buildProperties = build;
        this.gitProperties = git;
    }

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("build", Map.of(
            "versao", buildProperties.getVersion(),
            "artefato", buildProperties.getArtifact(),
            "timestamp", buildProperties.getTime()
        ));
        builder.withDetail("git", Map.of(
            "branch", gitProperties.getBranch(),
            "commit", gitProperties.getShortCommitId(),
            "data", gitProperties.getCommitTime()
        ));
    }
}
# Habilitar informações de build e git no /actuator/info
management:
  info:
    build:
      enabled: true
    git:
      enabled: true
      mode: full    # simples ou full

27. AOP — Aspect Oriented Programming

AOP (Programação Orientada a Aspectos) permite separar preocupações transversais — logging, segurança, auditoria, métricas — do código de negócio. O Spring AOP usa proxies JDK ou CGLIB; é baseado em tempo de execução (não compile-time como AspectJ). Para habilitá-lo, adicione a dependência abaixo (já inclusa no spring-boot-starter).

implementation("org.springframework.boot:spring-boot-starter-aop")

Conceitos fundamentais:

TermoSignificado
AspectClasse que encapsula o comportamento transversal (@Aspect)
Join PointPonto de execução onde o aspecto pode intervir (em Spring: sempre uma chamada de método)
PointcutExpressão que seleciona quais join points o aspecto intercepta
AdviceCódigo executado no join point (@Before, @After, @Around, etc.)
WeavingProcesso de aplicar o aspecto ao código alvo

Anotações de advice:

@Aspect
@Component
public class AuditoriaAspect {

    private static final Logger log = LoggerFactory.getLogger(AuditoriaAspect.class);

    // Executa ANTES do método — não pode alterar o retorno
    @Before("execution(* com.empresa.pedidos.service.*.*(..))")
    public void antes(JoinPoint jp) {
        log.info("Chamando: {}.{}",
            jp.getTarget().getClass().getSimpleName(),
            jp.getSignature().getName());
    }

    // Executa DEPOIS — sempre, mesmo se lançar exceção
    @After("execution(* com.empresa.pedidos.service.*.*(..))")
    public void depois(JoinPoint jp) {
        log.debug("Concluído: {}", jp.getSignature().toShortString());
    }

    // Executa somente se o método retornar normalmente — acessa o valor de retorno
    @AfterReturning(
        pointcut = "execution(* com.empresa.pedidos.service.PedidoService.criar(..))",
        returning = "resultado"
    )
    public void aposRetorno(JoinPoint jp, Object resultado) {
        log.info("Pedido criado com sucesso: {}", resultado);
    }

    // Executa somente se o método lançar exceção — acessa a exceção
    @AfterThrowing(
        pointcut = "execution(* com.empresa.pedidos.service.*.*(..))",
        throwing = "ex"
    )
    public void aposExcecao(JoinPoint jp, Exception ex) {
        log.error("Falha em {}: {}", jp.getSignature().toShortString(), ex.getMessage());
    }
}

@Around — controle total sobre o join point:

@Around é o advice mais poderoso: você decide se o método original será chamado, pode alterar os argumentos e o valor de retorno, e medir o tempo de execução.

@Aspect
@Component
public class TempoExecucaoAspect {

    private static final Logger log = LoggerFactory.getLogger(TempoExecucaoAspect.class);

    // Intercepta métodos anotados com @MedirTempo (anotação customizada)
    @Around("@annotation(com.empresa.pedidos.shared.aop.MedirTempo)")
    public Object medirTempo(ProceedingJoinPoint pjp) throws Throwable {
        long inicio = System.currentTimeMillis();
        String metodo = pjp.getSignature().toShortString();
        try {
            Object resultado = pjp.proceed();  // executa o método original
            long duracao = System.currentTimeMillis() - inicio;
            log.info("[TEMPO] {} completou em {}ms", metodo, duracao);
            return resultado;
        } catch (Throwable ex) {
            long duracao = System.currentTimeMillis() - inicio;
            log.warn("[TEMPO] {} falhou após {}ms", metodo, duracao);
            throw ex;
        }
    }
}

// Anotação marcadora
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MedirTempo {}

// Uso
@Service
public class RelatorioService {

    @MedirTempo
    public RelatorioResponse gerarRelatorio(FiltroRequest filtro) {
        // lógica demorada
    }
}

Pointcut expressions — referência rápida:

@Aspect
@Component
public class ExemplosPointcut {

    // Qualquer método público em qualquer classe
    @Pointcut("execution(public * *(..))")
    public void qualquerMetodoPublico() {}

    // Qualquer método no pacote service (um nível)
    @Pointcut("execution(* com.empresa.pedidos.service.*.*(..))")
    public void qualquerServico() {}

    // Qualquer método no pacote service e sub-pacotes (..)
    @Pointcut("execution(* com.empresa.pedidos.service..*.*(..))")
    public void qualquerServicoRecursivo() {}

    // Métodos anotados com @Transactional
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void metodosTransacionais() {}

    // Beans anotados com @Service
    @Pointcut("within(@org.springframework.stereotype.Service *)")
    public void classesService() {}

    // Combinando com operadores lógicos
    @Pointcut("classesService() && !@annotation(com.empresa.pedidos.shared.aop.SemAuditoria)")
    public void servicosSemExcecao() {}

    @Before("servicosSemExcecao()")
    public void auditarServicos(JoinPoint jp) {
        // aplica somente em @Service que não têm @SemAuditoria
    }
}