DFCODE

Tecnologia e Inovação

DFCODE — Escola de Tecnologia

Spring na Prática

Do Zero aos Microsserviços com Spring Boot, Spring MVC,
Spring Data JPA e Spring Security
31 CapítulosBásico ao Avançado
Exemplos ReaisProjeto EduSpring
Exercícios Práticos1 por capítulo
Versão 1.4Abril / 2026
📚 Como usar este ebook

Use o menu lateral para navegar entre os capítulos. Cada capítulo traz conceitos, exemplos de código comentados, diagramas de sequência e um exercício prático. Todos os exemplos pertencem ao projeto EduSpring — um sistema de gestão escolar que cresce a cada capítulo.

Parte 1 — Alicerces do Spring
Capítulo 1

O Ecossistema Spring

Antes de escrever a primeira linha de código, é fundamental entender o território. O Spring não é apenas um framework — é um ecossistema completo que cobre desde a criação de APIs até segurança, persistência e mensageria.

O que é o Spring Framework?

O Spring Framework é uma plataforma open-source para desenvolvimento de aplicações Java criada em 2003 por Rod Johnson. Seu objetivo original era simples, porém ambicioso: tornar o desenvolvimento Java corporativo menos doloroso, eliminando a complexidade do antigo Java EE (Enterprise Edition).

Naquela época, criar uma aplicação corporativa Java exigia configurações XML gigantescas, servidores de aplicação pesados e muito código repetitivo. O Spring introduziu um conceito central chamado Inversão de Controle (IoC), que transformou a forma como os desenvolvedores pensam sobre seus objetos — em vez de criar e gerenciar instâncias manualmente, você declara o que precisa e o Spring cuida do resto.

Os Módulos do Ecossistema

Hoje o Spring é um ecossistema com dezenas de projetos. Os mais relevantes para você, desenvolvedor backend Java, são:

MóduloO que fazQuando usar
Spring FrameworkO núcleo: IoC, DI, AOP, MVCBase de tudo, sempre presente
Spring BootConfiguração automática e embedded serverTodo projeto novo
Spring MVCCamada web: controllers, REST, serializaçãoAPIs e aplicações web
Spring Data JPAPersistência com JPA/Hibernate simplificadaProjetos com banco relacional
Spring SecurityAutenticação, autorização, JWT, OAuth2Qualquer app com controle de acesso
Spring CloudConfig, Discovery, Gateway, Circuit BreakerArquiteturas de microsserviços
Spring AMQPMensageria com RabbitMQComunicação assíncrona entre serviços
Diagrama 1.1 — Ecossistema Spring e suas camadas
graph TD A["Spring Framework
(Core: IoC, DI, AOP)"] B["Spring Boot
(Auto-config, Starters, Embedded Server)"] C["Spring MVC
(REST Controllers, DispatcherServlet)"] D["Spring Data JPA
(Repositories, JPA, Hibernate)"] E["Spring Security
(Auth, JWT, OAuth2)"] F["Spring Cloud
(Config, Eureka, Gateway)"] G["Spring AMQP
(RabbitMQ)"] A --> B B --> C B --> D B --> E B --> F B --> G

De Monolito a Microsserviços

Historicamente, as aplicações Java eram construídas como monolitos: um único sistema executável com todos os módulos (usuários, produtos, pedidos, relatórios) dentro do mesmo deployment. Isso funcionava bem até a escala crescer.

Com o crescimento das plataformas digitais, surgiu a necessidade de escalar partes específicas do sistema de forma independente. Assim nasceu a arquitetura de microsserviços: cada domínio de negócio é um serviço independente, com seu próprio banco de dados, ciclo de deploy e escalabilidade.

O Spring Boot tornou-se a ferramenta padrão do mercado para construir microsserviços em Java justamente por eliminar a burocracia de configuração e permitir criar um serviço funcional em minutos.

💡 Sobre este ebook

Ao longo de todos os capítulos, você vai construir o EduSpring: um sistema de gestão escolar com cadastro de alunos, cursos, matrículas e professores. A aplicação começa simples e evolui até uma arquitetura com múltiplos serviços, mensageria e resiliência.

1
Exercício — Mapeando o mercado

Objetivo

Pesquisar o uso do Spring no mercado brasileiro.

Tarefa

  • Acesse o LinkedIn Jobs e pesquise "Spring Boot" no Brasil.
  • Identifique 5 vagas e anote quais módulos do Spring cada vaga exige (Boot, Security, Data, Cloud etc.).
  • Monte uma tabela comparando os módulos mais pedidos.

Critério de aceite

Tabela com pelo menos 5 vagas e os módulos identificados, com conclusão própria sobre quais módulos priorizar no estudo.

✓ O que aprendemos
  • O Spring Framework é o núcleo de um ecossistema amplo de projetos open-source.
  • O Spring Boot simplifica a configuração e é o ponto de entrada padrão para novos projetos.
  • Spring MVC, Data, Security e Cloud resolvem camadas específicas da aplicação.
  • A arquitetura evoluiu de monolitos para microsserviços, e o Spring acompanhou essa evolução.
Parte 1 — Alicerces do Spring
Capítulo 2

Configurando o Ambiente

Um bom ambiente de desenvolvimento economiza horas de trabalho. Neste capítulo vamos configurar tudo do zero: JDK, IDE, Maven e criar nosso primeiro projeto Spring Boot usando o Spring Initializr.

Pré-requisitos

JDK 21 (LTS)

Recomendamos o JDK 21, versão LTS (Long-Term Support) lançada em setembro de 2023. Use o Eclipse Temurin (distribuição gratuita e open-source) ou o SDKMAN! para gerenciar múltiplas versões no Linux/macOS:

terminalbash
# Instalar SDKMAN e JDK 21
curl -s "https://get.sdkman.io" | bash
sdk install java 21.0.3-tem
java -version   # Confirma: openjdk 21...

Maven ou Gradle?

Maven usa XML (pom.xml) e é mais verboso, porém com convenções bem estabelecidas. Gradle usa Groovy/Kotlin DSL (build.gradle) e é mais conciso e flexível. Neste ebook usamos Maven por ser o padrão mais encontrado em projetos legados e vagas de emprego.

IDE: IntelliJ IDEA ou VS Code

A IntelliJ IDEA Community (gratuita) é a IDE mais produtiva para Spring. Instale o plugin Spring Boot Assistant para autocomplete de propriedades. Alternativa: VS Code com o Extension Pack for Java e Spring Boot Tools.

Criando o Projeto com Spring Initializr

Acesse start.spring.io e configure:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.3.x (ou versão estável mais recente)
  • Group: br.com.dfcode
  • Artifact: eduspring
  • Java: 21
  • Dependências: Spring Web, Spring Boot DevTools, Actuator

Clique em Generate, extraia o ZIP e abra na sua IDE.

Estrutura do Projeto

Estrutura de pastastext
eduspring/
├── src/
│   ├── main/
│   │   ├── java/br/com/dfcode/eduspring/
│   │   │   └── EduspringApplication.java   ← ponto de entrada
│   │   └── resources/
│   │       ├── application.properties      ← configurações
│   │       └── static/                     ← arquivos estáticos
│   └── test/
│       └── java/br/com/dfcode/eduspring/   ← testes
└── pom.xml                                 ← dependências Maven

O Ponto de Entrada: @SpringBootApplication

EduspringApplication.javajava
package br.com.dfcode.eduspring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// @SpringBootApplication é um atalho para três anotações:
// @SpringBootConfiguration  → define esta classe como configuração
// @EnableAutoConfiguration  → ativa a mágica do auto-configure
// @ComponentScan            → escaneia beans no pacote atual e subpacotes
@SpringBootApplication
public class EduspringApplication {

    public static void main(String[] args) {
        // SpringApplication.run inicializa o container IoC,
        // aplica auto-configurações e sobe o servidor Tomcat embutido
        SpringApplication.run(EduspringApplication.class, args);
    }
}

O que acontece quando você roda a aplicação?

Diagrama 2.1 — Sequência de inicialização do Spring Boot
sequenceDiagram participant Dev as Desenvolvedor participant Main as metodo main participant SA as SpringApplication participant Ctx as ApplicationContext participant AC as AutoConfiguration participant Tomcat as Tomcat Embutido Dev->>Main: executa java -jar eduspring.jar Main->>SA: SpringApplication.run(...) SA->>Ctx: cria ApplicationContext Ctx->>AC: carrega spring.factories AC->>Ctx: registra beans automáticos Ctx->>Ctx: instancia e injeta todos os beans Ctx->>Tomcat: inicializa servidor na porta 8080 Tomcat-->>Dev: "Started EduspringApplication in 2.3s"
💡 Boas Práticas

Mantenha a classe EduspringApplication no pacote raiz (br.com.dfcode.eduspring). O @ComponentScan escaneia este pacote e todos os subpacotes automaticamente. Se mover para um subpacote, seus beans podem não ser encontrados.

2
Exercício — Primeiro projeto funcionando

Objetivo

Criar e rodar o projeto EduSpring localmente.

Passos

  • Gere o projeto no Spring Initializr com as dependências: Web, DevTools e Actuator.
  • Rode a aplicação (./mvnw spring-boot:run ou pela IDE).
  • Acesse http://localhost:8080/actuator/health no navegador.

Critério de aceite

O endpoint /actuator/health retorna {"status":"UP"} no navegador.

✓ O que aprendemos
  • O Spring Initializr gera a estrutura completa do projeto com dependências configuradas.
  • @SpringBootApplication combina configuração, auto-configure e component scan.
  • O Spring Boot inicia um servidor Tomcat embutido — sem necessidade de servidor externo.
  • A sequência de boot vai de main() até o servidor ouvindo na porta 8080.
Parte 1 — Alicerces do Spring
Capítulo 3

IoC e Injeção de Dependências

Inversão de Controle (IoC) e Injeção de Dependências (DI) são os pilares do Spring. Entender esses conceitos é obrigatório — eles explicam por que o Spring funciona do jeito que funciona e por que seu código fica mais fácil de testar e manter.

O Problema: Acoplamento Forte

Imagine o código sem Spring. Para criar um MatriculaService que depende de AlunoRepository e CursoRepository, você instanciaria tudo manualmente:

Sem Spring — acoplamento forte ❌java
public class MatriculaService {

    // Problema 1: instanciação manual — quem cria o Repository?
    // Problema 2: se AlunoRepository mudar o construtor, tudo quebra
    // Problema 3: impossível substituir por mock nos testes
    private AlunoRepository alunoRepository = new AlunoRepository();
    private CursoRepository cursoRepository = new CursoRepository();

    public void matricular(Long alunoId, Long cursoId) {
        Aluno aluno = alunoRepository.findById(alunoId);
        Curso curso = cursoRepository.findById(cursoId);
        // lógica de matrícula...
    }
}

A Solução: Inversão de Controle

Com IoC, você inverte a responsabilidade de criação. Em vez de MatriculaService criar suas dependências, o container Spring as cria e as injeta. Você apenas declara o que precisa:

Com Spring — injeção por construtor ✅java
package br.com.dfcode.eduspring.service;

import org.springframework.stereotype.Service;

// @Service marca esta classe como um bean gerenciado pelo Spring
// Semanticamente indica que contém regras de negócio
@Service
public class MatriculaService {

    private final AlunoRepository alunoRepository;
    private final CursoRepository cursoRepository;

    // Injeção por construtor: preferida!
    // - Imutável (campos final)
    // - Facilita testes (basta chamar new MatriculaService(mockRepo1, mockRepo2))
    // - Deixa dependências explícitas
    // Com Spring Boot 2.2+, @Autowired no construtor é opcional
    public MatriculaService(AlunoRepository alunoRepository,
                            CursoRepository cursoRepository) {
        this.alunoRepository = alunoRepository;
        this.cursoRepository = cursoRepository;
    }

    public void matricular(Long alunoId, Long cursoId) {
        // O Spring já injetou as dependências antes desta linha
        Aluno aluno = alunoRepository.findById(alunoId).orElseThrow();
        Curso curso  = cursoRepository.findById(cursoId).orElseThrow();
        // lógica de matrícula...
    }
}

Estereótipos: @Component e seus filhos

O Spring oferece anotações especializadas para marcar beans segundo sua responsabilidade. Todas são especializações de @Component:

AnotaçãoCamadaUso
@ComponentGenéricoQualquer bean que não se encaixa nas outras
@ServiceNegócioClasses com regras de negócio
@RepositoryDadosAcesso ao banco; converte exceções JPA em DataAccessException
@ControllerWebRecebe requisições HTTP; retorna views
@RestControllerWeb@Controller + @ResponseBody; retorna JSON

@Bean e @Configuration para dependências externas

Quando você precisa configurar um bean de uma biblioteca de terceiros (que você não pode anotar com @Service), usa-se @Bean dentro de uma classe @Configuration:

InfraConfig.javajava
package br.com.dfcode.eduspring.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// @Configuration indica que esta classe declara beans
@Configuration
public class InfraConfig {

    // @Bean diz ao Spring: "chame este método e registre o retorno como bean"
    // Ideal para configurar objetos de bibliotecas externas
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // fator de custo 12
    }
}

Escopos de Beans

Por padrão, todo bean Spring é Singleton: uma única instância é criada e compartilhada. Para casos especiais, outros escopos estão disponíveis:

EscopoComportamentoExemplo de uso
singletonUma instância para toda a aplicação (padrão)Services, Repositories
prototypeNova instância a cada injeçãoBuilders, objetos com estado temporário
requestUma instância por requisição HTTPObjetos que guardam dados da requisição
sessionUma instância por sessão HTTPRascunho multi-etapas, preferências da sessão, dados do usuário logado

Ambiguidade de beans: @Primary, @Qualifier e listas

Se existirem duas implementações da mesma interface (ex.: dois NotificacaoService), o Spring não sabe qual injetar e falha no startup com NoUniqueBeanDefinitionException. Três saídas comuns:

  • @Primary — marca o bean padrão quando há ambiguidade.
  • @Qualifier("nomeDoBean") — escolhe o bean pelo identificador (nome do método @Bean ou nome da classe em camelCase).
  • Anotação customizada — composta com @Qualifier, deixa o código mais legível que strings soltas.
Duas implementações + @Primary e @Qualifierjava
public interface RelatorioExporter { byte[] exportar(List<Matricula> dados); }

@Component("exporterCsv")
public class RelatorioExporterCsv implements RelatorioExporter { /* ... */ }

@Primary
@Component("exporterExcel")
public class RelatorioExporterExcel implements RelatorioExporter { /* ... */ }

@Service
public class RelatorioService {
    private final RelatorioExporter exporterPadrao; // Excel por causa de @Primary

    public RelatorioService(RelatorioExporter exporterPadrao) {
        this.exporterPadrao = exporterPadrao;
    }
}

@Service
public class IntegracaoLegadoService {
    private final RelatorioExporter csvExporter;

    public IntegracaoLegadoService(@Qualifier("exporterCsv") RelatorioExporter csvExporter) {
        this.csvExporter = csvExporter;
    }
}
Qualifier customizado + meta-anotaçãojava
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface CsvExporter { }

// Uso na implementação:
@Component
@CsvExporter
public class RelatorioExporterCsv implements RelatorioExporter { }

// Uso na injeção:
public IntegracaoLegadoService(@CsvExporter RelatorioExporter csv) { }
Injetar todas as implementações (lista de beans)java
@Service
public class PipelineValidacaoAluno {

    private final List<ValidadorAluno> validadores;

    // Spring injeta TODOS os beans que implementam ValidadorAluno (opcional: @Order nos validadores)
    public PipelineValidacaoAluno(List<ValidadorAluno> validadores) {
        this.validadores = validadores;
    }

    public void validar(Aluno a) {
        validadores.forEach(v -> v.validar(a));
    }
}
Diagrama 3.1 — Como o container IoC injeta dependências no startup
sequenceDiagram participant Ctx as Container IoC participant AR as AlunoRepository participant CR as CursoRepository participant MS as MatriculaService participant MC as MatriculaController Note over Ctx: Fase de inicialização Ctx->>AR: instancia AlunoRepository Ctx->>CR: instancia CursoRepository Ctx->>MS: instancia MatriculaService(alunoRepo, cursoRepo) Note over MS: dependências injetadas pelo construtor Ctx->>MC: instancia MatriculaController(matriculaService) Note over MC: pronto para receber requisições HTTP
⚠ Atenção — Evite @Autowired em campo

A injeção direta em campo (@Autowired private AlunoRepository repo;) funciona, mas é considerada má prática:

  • Impossibilita testes unitários sem o contexto Spring.
  • Esconde dependências (você não sabe o que a classe precisa sem ler os campos).
  • Permite criar objetos com estado incompleto.

Prefira sempre injeção por construtor.

3
Exercício — Serviço com dois profiles

Contexto

O EduSpring precisa de um serviço de data/hora que em desenvolvimento retorna uma data fixada (para testes determinísticos) e em produção retorna a hora real.

Objetivo

  • Criar a interface RelogioService com o método LocalDateTime agora().
  • Criar RelogioServiceDev (anotado com @Profile("dev")) que retorna LocalDateTime.of(2024, 1, 1, 8, 0).
  • Criar RelogioServiceProd (anotado com @Profile("prod")) que retorna LocalDateTime.now().
  • Injetar RelogioService em um controller e expor endpoint GET /hora.

Critério de aceite

  • Com spring.profiles.active=dev: GET /hora retorna 2024-01-01T08:00:00.
  • Com spring.profiles.active=prod: retorna a hora atual do sistema.

Dica

Adicione spring.profiles.active=dev no application.properties para testar localmente.

✓ O que aprendemos
  • IoC inverte a responsabilidade de criação de objetos do seu código para o container Spring.
  • Injeção por construtor é a forma preferida: imutável, testável e explícita.
  • @Service, @Repository e @Controller são especializações semânticas de @Component.
  • @Bean + @Configuration servem para configurar beans de bibliotecas externas.
  • O escopo padrão é Singleton — uma instância compartilhada para toda a aplicação.
  • @Primary, @Qualifier e qualifiers customizados resolvem ambiguidade entre implementações.
  • List<Interface> no construtor recebe todas as implementações registradas.
Parte 2 — Spring MVC: APIs REST
Capítulo 4

Arquitetura MVC e o DispatcherServlet

O Spring MVC é o módulo responsável por receber requisições HTTP e encaminhá-las ao código correto. Entender seu fluxo interno é essencial para depurar problemas e construir APIs bem estruturadas.

O Padrão MVC

MVC (Model-View-Controller) separa a aplicação em três responsabilidades:

  • Model: os dados e a lógica de negócio (entidades, serviços).
  • View: a apresentação (no nosso caso, JSON serializado).
  • Controller: o intermediário que recebe a requisição, chama o serviço e devolve a resposta.

No contexto de APIs REST, a "View" é simplesmente a serialização JSON do objeto retornado pelo controller.

O DispatcherServlet

O DispatcherServlet é o Front Controller do Spring MVC: um único servlet que recebe todas as requisições HTTP e delega para o controller correto. Ele consulta o HandlerMapping para descobrir qual método deve tratar a requisição.

Diagrama 4.1 — Ciclo de vida de uma requisição no Spring MVC
sequenceDiagram participant C as Cliente participant DS as DispatcherServlet participant HM as HandlerMapping participant Ctrl as CursoController participant Svc as CursoService participant DB as Banco de Dados C->>DS: GET /cursos/1 DS->>HM: qual handler trata GET /cursos/1? HM-->>DS: CursoController.buscarPorId(1) DS->>Ctrl: buscarPorId(1) Ctrl->>Svc: buscarPorId(1) Svc->>DB: SELECT * FROM curso WHERE id=1 DB-->>Svc: Curso{id=1, nome="Java"} Svc-->>Ctrl: CursoResponse{...} Ctrl-->>DS: ResponseEntity 200 OK + JSON DS-->>C: HTTP 200 {"id":1,"nome":"Java"}

@RestController e Mapeamentos

@RestController combina @Controller + @ResponseBody. O @ResponseBody instrui o Spring a serializar o retorno do método diretamente para o body da resposta HTTP (JSON por padrão, via Jackson).

CursoController.javajava
package br.com.dfcode.eduspring.controller;

import br.com.dfcode.eduspring.dto.CursoRequest;
import br.com.dfcode.eduspring.dto.CursoResponse;
import br.com.dfcode.eduspring.service.CursoService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;

// @RestController = @Controller + @ResponseBody
// Todo retorno é serializado automaticamente para JSON
@RestController
// Prefixo de todas as rotas deste controller
@RequestMapping("/cursos")
public class CursoController {

    private final CursoService cursoService;

    public CursoController(CursoService cursoService) {
        this.cursoService = cursoService;
    }

    // GET /cursos → lista todos os cursos
    @GetMapping
    public List<CursoResponse> listar() {
        return cursoService.listarTodos();
    }

    // GET /cursos/1 → busca curso por ID
    // @PathVariable extrai o {id} da URL
    @GetMapping("/{id}")
    public CursoResponse buscarPorId(@PathVariable Long id) {
        return cursoService.buscarPorId(id);
    }

    // GET /cursos/buscar?nome=java → filtra por nome
    // @RequestParam lê o parâmetro de query string
    @GetMapping("/buscar")
    public List<CursoResponse> buscarPorNome(@RequestParam String nome) {
        return cursoService.buscarPorNome(nome);
    }

    // POST /cursos → cria novo curso
    // @RequestBody deserializa o JSON do body para CursoRequest
    @PostMapping
    public ResponseEntity<CursoResponse> criar(@Valid @RequestBody CursoRequest request) {
        CursoResponse criado = cursoService.criar(request);
        // Retorna 201 Created com Location header apontando para o novo recurso
        URI location = URI.create("/cursos/" + criado.id());
        return ResponseEntity.created(location).body(criado);
    }

    // PUT /cursos/1 → atualiza completamente o curso
    @PutMapping("/{id}")
    public CursoResponse atualizar(@PathVariable Long id,
                                   @Valid @RequestBody CursoRequest request) {
        return cursoService.atualizar(id, request);
    }

    // DELETE /cursos/1 → remove o curso
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> remover(@PathVariable Long id) {
        cursoService.remover(id);
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}
💡 Boas Práticas — Nomes de rotas REST
  • Use substantivos no plural para recursos: /cursos, /alunos, não /getCurso.
  • Use verbos HTTP para ações: GET (ler), POST (criar), PUT (substituir), PATCH (atualizar parcialmente), DELETE (remover).
  • Recursos aninhados: GET /cursos/1/alunos para listar alunos de um curso.
4
Exercício — Endpoint de busca com filtro

Objetivo

Adicionar endpoint de busca com múltiplos filtros opcionais.

Tarefa

  • Criar endpoint GET /alunos/buscar que aceite @RequestParam(required = false) para nome e email.
  • Se nenhum parâmetro for informado, retornar todos os alunos.
  • Se nome for informado, filtrar por nome (contém, ignorando case).

Critério de aceite

  • GET /alunos/buscar → retorna todos.
  • GET /alunos/buscar?nome=jo → retorna alunos cujo nome contém "jo".
  • Resposta sempre em JSON com status 200.
✓ O que aprendemos
  • O DispatcherServlet é o Front Controller que orquestra todas as requisições HTTP.
  • @RestController serializa automaticamente os retornos para JSON.
  • @PathVariable extrai segmentos da URL; @RequestParam lê query strings; @RequestBody deserializa o body.
  • Use substantivos no plural para rotas e verbos HTTP para ações.
Parte 2 — Spring MVC: APIs REST
Capítulo 5

Validação de Dados

Nunca confie nos dados que chegam pela API. A validação na camada de entrada protege sua aplicação de dados inválidos antes mesmo de chegarem ao banco de dados. O Spring integra nativamente a especificação Bean Validation (Jakarta Validation).

Por que validar na entrada?

Sem validação, dados inválidos percorrem todas as camadas: chegam ao service, passam para o repository e só falham no banco de dados — gerando exceções genéricas difíceis de depurar. Com Bean Validation, você rejeita dados inválidos imediatamente no controller com uma resposta clara para o cliente.

Bean Validation — As anotações principais

CriarAlunoRequest.javajava
package br.com.dfcode.eduspring.dto;

import jakarta.validation.constraints.*;

// Record Java (Java 16+): imutável, sem boilerplate, ideal para DTOs de entrada
public record CriarAlunoRequest(

    // Campo obrigatório, não pode ser vazio ou só espaços
    @NotBlank(message = "O nome é obrigatório")
    @Size(min = 2, max = 100, message = "Nome deve ter entre 2 e 100 caracteres")
    String nome,

    // Valida formato de e-mail
    @NotBlank(message = "O e-mail é obrigatório")
    @Email(message = "Formato de e-mail inválido")
    String email,

    // Matrícula deve ser positiva
    @NotNull(message = "A matrícula é obrigatória")
    @Positive(message = "A matrícula deve ser um número positivo")
    Integer matricula,

    // Idade entre 14 e 120 anos
    @Min(value = 14, message = "Idade mínima permitida é 14 anos")
    @Max(value = 120, message = "Idade máxima permitida é 120 anos")
    Integer idade
) {}

Ativando a validação no Controller

AlunoController.java (trecho)java
// @Valid ativa a validação do objeto anotado com Bean Validation
// Se a validação falhar, Spring lança MethodArgumentNotValidException
// antes mesmo de entrar no método — o código dentro nunca é executado
@PostMapping
public ResponseEntity<AlunoResponse> criar(@Valid @RequestBody CriarAlunoRequest request) {
    AlunoResponse criado = alunoService.criar(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(criado);
}

Retornando erros em JSON padronizado

Sem configuração adicional, o Spring retorna um JSON verboso e difícil de consumir. Vamos criar um @ControllerAdvice que intercepta as exceções de validação e retorna um JSON limpo seguindo o padrão RFC 7807 (Problem Details):

GlobalExceptionHandler.javajava
package br.com.dfcode.eduspring.exception;

import org.springframework.http.*;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;

// @ControllerAdvice intercepta exceções de qualquer controller
@RestControllerAdvice
public class GlobalExceptionHandler {

    // Intercepta falhas de validação (@Valid)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)  // 422
    public Map<String, Object> handleValidationErrors(
            MethodArgumentNotValidException ex) {

        // Coleta todos os erros de campo
        List<Map<String, String>> erros = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> Map.of(
                "campo", e.getField(),
                "mensagem", e.getDefaultMessage()
            ))
            .toList();

        return Map.of(
            "status", 422,
            "titulo", "Dados inválidos",
            "erros", erros
        );
    }

    // Intercepta recursos não encontrados (lançado nos services)
    @ExceptionHandler(RecursoNaoEncontradoException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)  // 404
    public Map<String, Object> handleNaoEncontrado(
            RecursoNaoEncontradoException ex) {
        return Map.of(
            "status", 404,
            "titulo", "Recurso não encontrado",
            "detalhe", ex.getMessage()
        );
    }
}

Com essa configuração, um POST com dados inválidos retornará:

Resposta de erro — 422 Unprocessable Entityjson
{
  "status": 422,
  "titulo": "Dados inválidos",
  "erros": [
    { "campo": "email", "mensagem": "Formato de e-mail inválido" },
    { "campo": "nome",  "mensagem": "O nome é obrigatório" }
  ]
}

RFC 7807 com ProblemDetail (Spring 6+)

O Spring MVC oferece o tipo org.springframework.http.ProblemDetail, alinhado à RFC 7807 (Problem Details for HTTP APIs): respostas de erro com type, title, status, detail, instance e extensões em properties.

GlobalExceptionHandler — ProblemDetail + erros de campojava
import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;

import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ProblemDetail> validacao(MethodArgumentNotValidException ex, WebRequest req) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(
            HttpStatus.UNPROCESSABLE_ENTITY, "Um ou mais campos são inválidos");
        pd.setTitle("Dados inválidos");
        pd.setType(URI.create("https://api.eduspring.local/problems/validation-error"));
        pd.setInstance(URI.create(req.getDescription(false).replace("uri=", "")));

        Map<String, Object> props = new LinkedHashMap<>();
        props.put("erros", ex.getBindingResult().getFieldErrors().stream()
            .map(e -> Map.of("campo", e.getField(), "mensagem", e.getDefaultMessage()))
            .toList());
        pd.setProperties(props);
        return ResponseEntity.unprocessableEntity().body(pd);
    }

    @ExceptionHandler(RecursoNaoEncontradoException.class)
    public ProblemDetail naoEncontrado(RecursoNaoEncontradoException ex, WebRequest req) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        pd.setTitle("Recurso não encontrado");
        pd.setType(URI.create("https://api.eduspring.local/problems/not-found"));
        return pd;
    }
}

ResponseEntityExceptionHandler e erros do Spring MVC

Estenda ResponseEntityExceptionHandler para sobrescrever o tratamento padrão de exceções já mapeadas pelo Spring (MethodArgumentTypeMismatchException, HttpRequestMethodNotSupportedException, etc.) e devolver ProblemDetail no mesmo formato da sua API.

RestExceptionHandler.java — base reutilizáveljava
import org.springframework.http.*;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import org.springframework.web.context.request.WebRequest;

@RestControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
            Exception ex, Object body, HttpHeaders headers, HttpStatusCode status, WebRequest req) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
        pd.setTitle(ex.getClass().getSimpleName());
        return ResponseEntity.status(status).headers(headers).body(pd);
    }
}
📋 Uma classe só na prática

Em projetos reais, costuma-se ter um único @RestControllerAdvice que estende ResponseEntityExceptionHandler e concentra todos os @ExceptionHandler customizados (ProblemDetail, validação, negócio). Os exemplos anteriores foram separados para didática; ao implementar, una os métodos em uma classe (ex.: ApiExceptionHandler extends ResponseEntityExceptionHandler) e remova duplicidade de beans de advice.

Erro na desserialização: InvalidFormatException

Quando o JSON envia um valor incompatível com o tipo do DTO (ex.: string onde esperava número), o Jackson lança InvalidFormatException, encapsulada em HttpMessageNotReadableException. Trate-a para retornar 400 com mensagem clara em vez de stack genérica.

Tratando JSON malformado / tipo erradojava
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import org.springframework.http.converter.HttpMessageNotReadableException;

@ExceptionHandler(HttpMessageNotReadableException.class)
public ProblemDetail jsonInvalido(HttpMessageNotReadableException ex) {
    Throwable causa = ex.getMostSpecificCause();
    String detalhe = (causa instanceof InvalidFormatException ife)
        ? "Valor inválido para o campo '%s': '%s'".formatted(ife.getPathReference(), ife.getValue())
        : "Corpo da requisição não pôde ser lido";
    ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, detalhe);
    pd.setTitle("Payload inválido");
    return pd;
}

@ExceptionHandler no próprio controller

Você pode declarar métodos @ExceptionHandler dentro de um controller — eles só valem para aquele controller. Use para casos muito específicos; regras globais ficam no @RestControllerAdvice.

Diagrama 5.1 — Fluxo de uma requisição com dados inválidos
sequenceDiagram participant C as Cliente participant DS as DispatcherServlet participant V as Validator participant Ctrl as Controller participant EH as GlobalExceptionHandler C->>DS: POST /alunos {"email":"invalido"} DS->>V: @Valid CriarAlunoRequest V-->>DS: MethodArgumentNotValidException Note over Ctrl: nunca é chamado DS->>EH: handleValidationErrors(ex) EH-->>C: HTTP 422 {"status":422,"erros":[...]}

Criando uma validação customizada

MatriculaUnica.java + MatriculaUnicaValidator.javajava
// 1. Definindo a anotação
@Documented
@Constraint(validatedBy = MatriculaUnicaValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MatriculaUnica {
    String message() default "Matrícula já cadastrada";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. Implementando a lógica de validação
@Component
public class MatriculaUnicaValidator
        implements ConstraintValidator<MatriculaUnica, Integer> {

    private final AlunoRepository alunoRepository;

    public MatriculaUnicaValidator(AlunoRepository alunoRepository) {
        this.alunoRepository = alunoRepository;
    }

    @Override
    public boolean isValid(Integer matricula, ConstraintValidatorContext ctx) {
        if (matricula == null) return true; // deixa o @NotNull tratar
        // Retorna true se NÃO existir aluno com esta matrícula
        return !alunoRepository.existsByMatricula(matricula);
    }
}
5
Exercício — Validação de criação de curso

Objetivo

Aplicar validações no DTO de criação de curso.

Tarefa

  • Criar CriarCursoRequest com campos: nome (obrigatório, 3-150 chars), cargaHoraria (min 4, max 400 horas), descricao (opcional, max 500 chars).
  • Configurar o GlobalExceptionHandler para retornar os erros em JSON.
  • Testar no Postman enviando dados inválidos e verificar a resposta 422.

Critério de aceite

POST com nome vazio retorna 422 com "campo": "nome" e a mensagem de erro correspondente.

✓ O que aprendemos
  • Bean Validation valida dados de entrada antes de chegarem ao service.
  • @Valid no controller ativa a validação; falhas lançam MethodArgumentNotValidException.
  • @RestControllerAdvice centraliza o tratamento de erros de toda a aplicação.
  • ProblemDetail padroniza erros no formato RFC 7807.
  • ResponseEntityExceptionHandler customiza erros já tratados pelo Spring MVC.
  • HttpMessageNotReadableException + causa InvalidFormatException cobre JSON com tipos errados.
  • Validações customizadas são criadas implementando ConstraintValidator.
Parte 2 — Spring MVC: APIs REST
Capítulo 6

APIs REST Profissionais

Uma API bem construída vai além de retornar JSON: verbos e status HTTP corretos, negociação de conteúdo (JSON/XML), atualização parcial (PATCH), projeções com @JsonView e filtros de propriedade, mapeamento DTO↔entidade, cache HTTP (Cache-Control, ETags), CORS e documentação OpenAPI — tudo com foco em boas práticas para o dia a dia em produção.

Os 4 Níveis de Maturidade REST

Leonard Richardson definiu quatro níveis de maturidade para APIs REST. A maioria das APIs de mercado opera no Nível 2:

NívelCaracterísticaExemplo
0 — POXHTTP como transporte apenasPOST /api com action no body
1 — RecursosURLs representam recursosPOST /cursos/criar
2 — Verbos HTTPVerbos HTTP para ações + Status codes corretosPOST /cursos → 201 Created
3 — HATEOASRespostas incluem links para ações relacionadasResponse com _links

ResponseEntity — Controle total da resposta

Exemplos de ResponseEntityjava
// 200 OK com body
return ResponseEntity.ok(cursoResponse);

// 201 Created com Location header
URI location = URI.create("/cursos/" + curso.getId());
return ResponseEntity.created(location).body(cursoResponse);

// 204 No Content (sem body)
return ResponseEntity.noContent().build();

// 404 Not Found com mensagem
return ResponseEntity.notFound().build();

// Status customizado + headers
return ResponseEntity
    .status(HttpStatus.ACCEPTED)        // 202
    .header("X-Request-Id", requestId)
    .body(resultado);

DTOs com Java Records

Nunca exponha suas entidades JPA diretamente na API. Use DTOs (Data Transfer Objects) para desacoplar o modelo de banco do contrato da API. Java Records (Java 16+) são perfeitos para isso: imutáveis, concisos e sem boilerplate.

CursoResponse.javajava
package br.com.dfcode.eduspring.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDate;

// Record: construtor, getters, equals, hashCode e toString automáticos
public record CursoResponse(
    Long id,
    String nome,
    String descricao,
    Integer cargaHoraria,

    // @JsonFormat controla a serialização da data para o padrão ISO
    @JsonFormat(pattern = "dd/MM/yyyy")
    LocalDate dataInicio,

    Boolean ativo
) {
    // Método de fábrica estático: converte Entidade → DTO
    public static CursoResponse de(Curso curso) {
        return new CursoResponse(
            curso.getId(),
            curso.getNome(),
            curso.getDescricao(),
            curso.getCargaHoraria(),
            curso.getDataInicio(),
            curso.getAtivo()
        );
    }
}

Hierarquia de Exceções de Negócio

Hierarquia de exceçõesjava
// Exceção base para todas as regras de negócio
public class NegocioException extends RuntimeException {
    public NegocioException(String mensagem) { super(mensagem); }
}

// Lançada quando um recurso não é encontrado → mapeia para 404
public class RecursoNaoEncontradoException extends NegocioException {
    public RecursoNaoEncontradoException(String recurso, Long id) {
        super("%s com id %d não encontrado".formatted(recurso, id));
    }
}

// Lançada quando uma regra de negócio é violada → mapeia para 422
public class RegraVioladaException extends NegocioException {
    public RegraVioladaException(String mensagem) { super(mensagem); }
}

// Uso no service:
public CursoResponse buscarPorId(Long id) {
    return cursoRepository.findById(id)
        .map(CursoResponse::de)
        .orElseThrow(() -> new RecursoNaoEncontradoException("Curso", id));
}

Documentação com Springdoc OpenAPI (Swagger)

Adicione a dependência no pom.xml e a documentação é gerada automaticamente:

pom.xmlxml
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.5.0</version>
</dependency>

Acesse http://localhost:8080/swagger-ui.html para ver a documentação interativa. Enriqueça com anotações:

Anotações OpenAPI no controllerjava
@Operation(summary = "Cria um novo curso")
@ApiResponse(responseCode = "201", description = "Curso criado com sucesso")
@ApiResponse(responseCode = "422", description = "Dados inválidos")
@PostMapping
public ResponseEntity<CursoResponse> criar(@Valid @RequestBody CriarCursoRequest request) {
    // ...
}

Identificando recursos REST

Recursos são substantivos (nomes), não verbos. A URI identifica o quê; o método HTTP indica a ação.

TipoURI exemploSignificado
CollectionGET /cursosConjunto de cursos (lista paginada ou completa).
SingletonGET /cursos/{id}Um curso específico — use @PathVariable Long id.
Sub-recursoGET /cursos/{id}/matriculasRelacionamento explícito na URL.

Métodos HTTP — quando usar cada um

MétodoUsoIdempotente?Body típico
GETLer recurso ou coleçãoSimNão
POSTCriar recurso (servidor define URI/id)NãoSim
PUTSubstituição completa do recursoSimSim
PATCHAtualização parcial (só campos enviados)Não*Sim
DELETERemover recursoSimNão

*PATCH pode ser tratado como idempotente na prática, dependendo da semântica do recurso; não confundir com garantia de rede segura.

Códigos de status e @ResponseStatus

Alguns exemplos recorrentes: 200 OK, 201 Created + Location, 204 No Content, 400 Bad Request, 404 Not Found, 409 Conflict, 422 Unprocessable Entity (validação de negócio). Você pode anotar exceções com @ResponseStatus(HttpStatus.NOT_FOUND) para mapeamento declarativo, mas o @RestControllerAdvice costuma dar corpo JSON/ProblemDetail mais rico.

Collection vazia: para GET /cursos, o mais comum é 200 OK com [] — o cliente distingue “nenhum item” de erro. 204 No Content sem corpo é raro em APIs JSON de listagem.

Negociação de conteúdo (Accept e Content-Type)

O cliente envia Accept: application/json ou application/xml; o servidor escolhe o HttpMessageConverter adequado. No controller, produces e consumes restringem o contrato.

CursoController — produces / consumesjava
@GetMapping(value = "/{id}", produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public CursoResponse buscar(@PathVariable Long id) { ... }

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CursoResponse> criar(@Valid @RequestBody CriarCursoRequest req) { ... }
pom.xml — XML com Jacksonxml
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
DTO — anotações Jackson JSON/XMLjava
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;

@JsonRootName("curso") // nome do elemento raiz no XML
public record CursoResponse(
    Long id,
    String nome,
    @JsonProperty("desc") String descricao,
    @JsonIgnore Long versaoInterna // nunca serializa
) {}

// Coleções em XML: @JacksonXmlElementWrapper(name = "cursos") + @JacksonXmlProperty em listas
// (requer jackson-dataformat-xml e módulo com.fasterxml.jackson.dataformat.xml.annotation)

PATCH — atualização parcial

Use um DTO só com campos opcionais (Optional<T> ou tipos nullable) ou Map<String, Object> + merge manual. Em APIs mais rígidas, JsonMergePatch (RFC 7396) com ObjectMapper também é uma opção.

AtualizarCursoParcialRequest.java + PATCHjava
public record AtualizarCursoParcialRequest(
    @Size(max = 150) String nome,
    @Min(1) Integer cargaHoraria,
    Boolean ativo   // null = não alterar; false/true = atualizar
) {}

@PatchMapping("/{id}")
public CursoResponse patch(@PathVariable Long id,
        @Valid @RequestBody AtualizarCursoParcialRequest patch) {
    return cursoService.aplicarPatch(id, patch);
}

// No service: copiar apenas campos não-nulos para a entidade
public CursoResponse aplicarPatch(Long id, AtualizarCursoParcialRequest p) {
    Curso c = cursoRepository.findById(id).orElseThrow(...);
    if (p.nome() != null) c.setNome(p.nome());
    if (p.cargaHoraria() != null) c.setCargaHoraria(p.cargaHoraria());
    if (p.ativo() != null) c.setAtivo(p.ativo());
    return CursoResponse.de(cursoRepository.save(c));
}

Projeções com @JsonView

Defina “visões” (resumo vs detalhe) sem multiplicar DTOs: anote propriedades com @JsonView e no controller use MappingJacksonValue ou @JsonView no retorno.

Views + CursoResponse + controllerjava
public class Views {
    public static class Resumo {}
    public static class Detalhe extends Resumo {}
}

// Mesmo conceito do CursoResponse do projeto — aqui com visões para listagem vs detalhe
public record CursoJsonViewDto(
    @JsonView(Views.Resumo.class) Long id,
    @JsonView(Views.Resumo.class) String nome,
    @JsonView(Views.Detalhe.class) String descricaoCompleta,
    @JsonView(Views.Detalhe.class) Integer cargaHoraria
) {}

@GetMapping
@JsonView(Views.Resumo.class)
public List<CursoJsonViewDto> listar() { ... }

@GetMapping("/{id}")
@JsonView(Views.Detalhe.class)
public CursoJsonViewDto buscar(@PathVariable Long id) { ... }

Filtrar propriedades na resposta (Jackson)

Property filter: use @JsonFilter no DTO e um FilterProvider por requisição para incluir/excluir campos dinamicamente (ex.: perfil do usuário).

Filtro dinâmico com SimpleFilterProviderjava
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@JsonFilter("cursoFilter")
public record CursoFiltravel(Long id, String nome, String descricao, String codigoInterno) {}

// Em um @ControllerAdvice ou serviço: serializar só id e nome
SimpleFilterProvider fp = new SimpleFilterProvider()
    .addFilter("cursoFilter", SimpleBeanPropertyFilter.filterOutAllExcept("id", "nome"));
ObjectMapper om = new ObjectMapper().setFilterProvider(fp);
📚 Squiggly (filtro por query string)

A biblioteca Squiggly Filter for Jackson permite algo como GET /cursos?fields=id,nome para limitar o JSON retornado. Útil em integrações legadas; avalie manutenção do projeto e combine com paginação/cache.

Object mapping com ModelMapper

O ModelMapper reduz boilerplate ao copiar campos entre entidade e DTO (convenções de nome). Alternativas modernas: MapStruct (geração em compile time, preferido em muitos times).

ModelMapper bean + usojava
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ModelMapperConfig {
    @Bean
    public ModelMapper modelMapper() {
        ModelMapper mm = new ModelMapper();
        mm.getConfiguration().setAmbiguityIgnored(true);
        return mm;
    }
}

@Service
public class CursoService {
    private final ModelMapper modelMapper;

    public CursoResponse criar(CriarCursoRequest req) {
        Curso entidade = modelMapper.map(req, Curso.class);
        return CursoResponse.de(cursoRepository.save(entidade));
    }
}

pom.xml: org.modelmapper:modelmapper (versão estável compatível com seu Spring Boot).

Cache HTTP — por quê e quando evitar

Cache reduz latência e carga no servidor para recursos seguros para ler de cache (GET idempotentes). Evite cache agressivo em dados personalizados ou que mudam a cada segundo. Use Cache-Control (ex.: max-age, private, no-store para dados sensíveis).

ETag: o servidor envia um identificador da versão do recurso; o cliente reenvia If-None-Match e recebe 304 Not Modified sem body. Shallow ETag (ex.: ShallowEtagHeaderFilter do Spring) calcula hash do conteúdo serializado da resposta — simples, mas ainda executa o controller. Deep ETag na prática costuma ser um ETag derivado de versão/timestamp no banco, comparado antes de montar o payload (mais eficiente para payloads grandes).

Cache-Control + ETag com ResponseEntityjava
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;

import java.util.concurrent.TimeUnit;

@GetMapping("/{id}")
public ResponseEntity<CursoResponse> buscarComCache(@PathVariable Long id, WebRequest req) {
    CursoResponse body = cursoService.buscarPorId(id);
    String etag = "\"curso-" + id + "-v" + body.hashCode() + "\""; // produção: use version/timestamp do banco

    if (req.checkNotModified(etag)) {
        return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
    }
    return ResponseEntity.ok()
        .eTag(etag)
        .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic())
        .body(body);
}
ShallowEtagHeaderFilter (global)java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.ShallowEtagHeaderFilter;

@Configuration
public class EtagFilterConfig {
    @Bean
    public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
        return new ShallowEtagHeaderFilter();
    }
}

CORS e consumo da API (JavaScript e Java)

Browsers bloqueiam chamadas a outro domínio/porta sem cabeçalhos CORS corretos. No Spring, configure CorsRegistry ou @CrossOrigin em desenvolvimento; em produção, restrinja allowedOrigins.

WebMvcConfigurer — CORSjava
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("http://localhost:3000")
            .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true);
    }
}
fetch (JavaScript) + RestClient (Java)javascript
const r = await fetch('http://localhost:8080/api/cursos', {
  headers: { 'Accept': 'application/json' }
});
const cursos = await r.json();
Cliente Java (RestClient)java
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;

List<CursoResponse> lista = restClient.get()
    .uri("/cursos")
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .body(new ParameterizedTypeReference<>() {});
Diagrama 6.1 — Fluxo de erro de negócio com GlobalExceptionHandler
sequenceDiagram participant C as Cliente participant Ctrl as CursoController participant Svc as CursoService participant EH as GlobalExceptionHandler C->>Ctrl: DELETE /cursos/99 Ctrl->>Svc: remover(99) Svc-->>Ctrl: RecursoNaoEncontradoException("Curso", 99) Note over Ctrl: exceção propaga Ctrl-->>EH: intercepta a exceção EH-->>C: HTTP 404 {"status":404,"detalhe":"Curso com id 99 não encontrado"}
6
Exercício — PATCH de status do curso

Objetivo

Implementar atualização parcial com regra de negócio.

Tarefa

  • Criar endpoint PATCH /cursos/{id}/status que recebe {"ativo": false}.
  • Se o curso tiver alunos matriculados ativos, lançar RegraVioladaException com mensagem descritiva.
  • O GlobalExceptionHandler deve mapear essa exceção para HTTP 422.

Critério de aceite

  • PATCH em curso sem alunos → 200 OK com curso atualizado.
  • PATCH em curso com alunos → 422 com mensagem "Não é possível desativar um curso com matrículas ativas".
6b
Desafio — Negociação, PATCH e cache HTTP

Objetivo

Consolidar três tópicos do capítulo num fluxo único sobre o recurso Curso.

Tarefa

  • Content negotiation: expor GET /cursos/{id} com produces JSON e XML; testar com Accept: application/xml e application/json.
  • PATCH: criar PATCH /cursos/{id} com DTO parcial (ex.: só nome ou só cargaHoraria); garantir que campos omitidos não zerem a entidade.
  • Cache: no mesmo GET, enviar ETag + Cache-Control: max-age=60; validar 304 Not Modified com If-None-Match no Postman ou curl.

Critério de aceite

Três testes manuais documentados (prints ou coleção HTTP): resposta XML válida; PATCH altera só o enviado; segunda requisição GET com ETag retorna 304.

✓ O que aprendemos
  • Recursos REST são substantivos na URI; métodos HTTP definem a operação.
  • Negociação de conteúdo: Accept/Content-Type, produces/consumes, JSON e XML com Jackson.
  • PATCH com DTO parcial; @JsonView e filtros Jackson para projeções e campos dinâmicos.
  • ModelMapper (ou MapStruct) reduz mapeamento manual entre DTO e entidade.
  • Cache HTTP: Cache-Control, ETag, 304; Shallow ETag vs ETag “profundo” ligado à versão do dado.
  • CORS no WebMvcConfigurer; consumo com fetch e RestClient.
  • ResponseEntity controla status, headers e corpo; collection vazia costuma ser 200 + [].
  • Springdoc/OpenAPI documenta a API para times e integradores.
Parte 3 — Spring Boot
Capítulo 7

Apache Maven: build, dependências e escopos

O Spring Boot roda em cima de um projeto Java bem empacotado — e o Apache Maven é a ferramenta mais comum para declarar dependências, compilar, testar e gerar o JAR. Entender o pom.xml evita conflitos de versão, dependências “fantasma” em produção e builds lentos.

O que o Maven faz na prática

Maven é um automação de build baseado em convenções: pastas fixas (src/main/java, src/test/java), artefato publicado no repositório local (~/.m2/repository) e descritor central pom.xml (Project Object Model). Cada biblioteca é identificada por coordenadas GAV: groupId (organização), artifactId (nome do jar), version.

pom.xml — esqueleto mínimo Spring Bootxml
<?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.4.4</version>
    <relativePath/>
  </parent>

  <groupId>br.com.dfcode</groupId>
  <artifactId>eduspring</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <name>EduSpring</name>
  <description>Gestão escolar — exemplo do ebook</description>

  <properties>
    <java.version>21</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

O spring-boot-starter-parent importa o BOM (dependencyManagement) com dezenas de versões alinhadas — por isso muitos starters não precisam de <version>.

Ciclo de vida: comandos que você usa todo dia

Fase / comandoEfeito
mvn validateVerifica se o POM está íntegro.
mvn compileCompila main para target/classes.
mvn testRoda testes (Surefire); usa dependências com escopo test.
mvn packageGera o JAR/WAR (sem instalar no repositório local).
mvn verifyInclui verificações pós-package (ex.: integração).
mvn installInstala o artefato no ~/.m2 para outros módulos consumirem.
mvn clean package -DskipTestsLimpa target, empacota e pula testes (CI com cuidado).

Escopos (<scope>): o que entra no classpath

EscopoCompileRuntimeTestUso típico
compile (padrão)SimSimSimSua API e libs usadas no código principal (Spring Web, JPA).
providedSimNãoSimO container já oferece em runtime (ex.: servlet API em WAR no Tomcat externo).
runtimeNãoSimSimImplementação trocável não referenciada no código-fonte (driver JDBC, Logback impl).
testNãoNãoSimJUnit, Mockito, AssertJ — não vão para o JAR de produção.
importSó em <dependencyManagement> (BOMs); não adiciona dependência sozinha.
systemEvite: jar local com systemPath — frágil e não portável.
Confundir escopo é bug em produção

Colocar uma lib de teste sem scope test faz ela ir parar no artefato final. Sempre revise com mvn dependency:tree -Dscope=runtime antes de liberar release.

optional e dependências transitivas

Se o módulo A depende de B e B declara C como <optional>true</optional>, o projeto A não puxa C automaticamente — útil para recursos “plugáveis”. Já uma dependência normal de B é transitiva: Maven resolve a árvore inteira (com regras de “nearest definition” e ordem do POM). Comando essencial: mvn dependency:tree e, em caso de conflito, mvn dependency:tree -Dverbose.

Plugins que importam no Spring Boot

  • spring-boot-maven-plugin — empacota fat JAR executável (repackage) e objetivos como spring-boot:run.
  • maven-surefire-plugin — executa testes unitários (configurado pelo parent do Boot).
  • maven-failsafe-plugin — testes de integração na fase verify (*IT.java).

Perfis Maven (<profile>)

Diferente do Spring Profile: perfis Maven ativam trechos do POM (mvn package -Pprod), por exemplo trocar URL de repositório Nexus ou desligar plugin em CI. Combine com propriedades: <properties><env>dev</env></properties> dentro do profile.

7
Exercício — Árvore de dependências e escopo

Objetivo

Enxergar o que realmente entra no classpath de runtime do EduSpring.

Tarefa

  • Executar ./mvnw dependency:tree e localizar de onde vem o Hibernate (transitivo de spring-boot-starter-data-jpa).
  • Adicionar propositalmente uma dependência de teste sem scope test, rodar package e inspecionar o JAR com jar tf target/*.jar | findstr junit (Windows) ou jar tf ... | grep junit — depois corrigir com scope>test</scope>.
  • Listar apenas dependências de teste: mvn dependency:list -DincludeScope=test.
✓ O que aprendemos
  • Maven modela o projeto no pom.xml com GAV e herança do spring-boot-starter-parent.
  • Escopos definem visibilidade em compile, runtime e test — impacto direto no tamanho e segurança do deploy.
  • Dependências transitivas e opcionais explicam “de onde veio esse jar?” — use dependency:tree.
  • Plugins Maven amarram compile, teste e empacotamento ao ciclo de vida.
Parte 3 — Spring Boot
Capítulo 8

Spring Initializr — criando o projeto

O Spring Initializr é o jeito oficial de gerar um esqueleto de aplicação Spring Boot com dependências alinhadas às versões do ecossistema. Você evita erros de BOM, versões incompatíveis e esquecimento de pastas src/main/java e src/main/resources.

O que o Initializr entrega

Um ZIP (ou estrutura gerada pela IDE) com: pom.xml ou build.gradle, classe principal anotada com @SpringBootApplication, teste de contexto vazio, application.properties e pastas padrão Maven/Gradle. A documentação do Initializr descreve metadados e extensões.

Passo a passo em start.spring.io

  1. Project: Maven ou Gradle — no ebook usamos Maven; o time pode padronizar Gradle Kotlin DSL se preferir.
  2. Language: Java (Kotlin e Groovy também são suportados).
  3. Spring Boot: escolha a linha estável suportada pelo seu time (evite SNAPSHOT em produção).
  4. Group / Artifact / Name / Package: exemplo br.com.dfcode / eduspring — o package raiz deve ser o mesmo usado no @SpringBootApplication para o component scan funcionar.
  5. Packaging: Jar para aplicações executáveis com java -jar; War apenas se for implantar em servlet container externo.
  6. Java: alinhe à versão LTS instalada (17, 21…).

Dependências recomendadas para o EduSpring

DependênciaPara quê
Spring WebREST + Tomcat embutido
Spring Data JPARepositórios e Hibernate
PostgreSQL DriverBanco usado nos exemplos
ValidationBean Validation nas APIs
Spring Boot ActuatorSaúde e métricas
Spring Boot DevToolsRestart rápido em desenvolvimento
LombokMenos boilerplate (opcional mas comum)
💡 Dica

Use Spring Configuration Processor se for criar @ConfigurationProperties — sua IDE ganha metadados para autocomplete no application.yml.

Depois de gerar o ZIP

  1. Descompacte e importe como Maven project na IDE.
  2. Execute ./mvnw spring-boot:run (Linux/macOS) ou mvnw.cmd spring-boot:run (Windows).
  3. Confirme /actuator/health se adicionou Actuator.
8
Exercício — Novo módulo via Initializr

Objetivo

Gerar um segundo projeto só com Web + Actuator e comparar o pom.xml com o do EduSpring.

Tarefa

  • Criar em start.spring.io um artefato eduspring-minimal.
  • Rodar e validar GET /actuator/health.
  • Listar no caderno quais starters aparecem no pom.xml em relação ao projeto completo.
✓ O que aprendemos
  • O Initializr gera projetos alinhados às versões do Spring Boot.
  • Group/Artifact e package devem ser consistentes com o component scan.
  • Jar + Java LTS é o caminho padrão para microserviços e APIs.
Parte 3 — Spring Boot
Capítulo 9

Magia do Spring Boot

O Spring Boot não é magia negra — é engenharia inteligente. Neste capítulo vamos desmistificar o auto-configure, entender os Starters e aprender a configurar a aplicação de forma profissional com application.yml.

O Problema que o Spring Boot resolve

Antes do Spring Boot (antes de 2014), configurar uma aplicação Spring exigia dezenas de arquivos XML, configuração manual do Tomcat, declaração explícita de cada bean de infraestrutura e gerenciamento manual de versões de dependências. Um projeto simples levava dias para sair do zero.

O Spring Boot eliminou tudo isso com três pilares: Auto-configuration, Starters e Servidor embutido.

Auto-configuration: como funciona por dentro

Quando você adiciona spring-boot-starter-web ao projeto, o Spring Boot verifica: "existe Tomcat no classpath? Então vou configurar um servidor web automaticamente." Essa decisão é tomada por classes anotadas com @Conditional:

Como o Spring Boot auto-configura o Tomcat (simplificado)java
// Esta classe existe dentro do spring-boot-autoconfigure
// Você nunca escreve isso — é gerado pela infraestrutura do Boot
@Configuration
// Só configura se Tomcat estiver no classpath
@ConditionalOnClass(Tomcat.class)
// Só configura se NÃO existir um EmbeddedServletContainerFactory definido pelo usuário
@ConditionalOnMissingBean(EmbeddedServletContainerFactory.class)
public class EmbeddedTomcatAutoConfiguration {

    @Bean
    public TomcatEmbeddedServletContainerFactory tomcat() {
        return new TomcatEmbeddedServletContainerFactory();
    }
}
💡 Dica — Veja o que foi auto-configurado

Acesse http://localhost:8080/actuator/conditions para ver todas as auto-configurações aplicadas e as que foram descartadas (e por quê). Ou use --debug ao rodar a aplicação para ver no console.

Starters: dependências pré-empacotadas

Um Starter é uma dependência que traz tudo que você precisa para uma funcionalidade específica, com versões compatíveis entre si. Você não precisa declarar 10 dependências separadas — declara apenas o Starter:

StarterO que inclui
spring-boot-starter-webSpring MVC, Jackson, Tomcat embutido, Validation
spring-boot-starter-data-jpaSpring Data JPA, Hibernate, JDBC
spring-boot-starter-securitySpring Security, filtros de autenticação
spring-boot-starter-testJUnit 5, Mockito, AssertJ, MockMvc
spring-boot-starter-amqpSpring AMQP, RabbitMQ client

application.yml — Configuração legível

src/main/resources/application.ymlyaml
spring:
  application:
    name: eduspring

  datasource:
    url: jdbc:postgresql://localhost:5432/eduspring
    username: ${DB_USER:postgres}      # lê variável de ambiente, default: postgres
    password: ${DB_PASS:postgres}

  jpa:
    hibernate:
      ddl-auto: validate               # nunca use 'create' em produção!
    show-sql: false
    properties:
      hibernate:
        format_sql: true

server:
  port: 8080
  servlet:
    context-path: /api                 # prefixo global: /api/cursos, /api/alunos

# Configurações personalizadas da aplicação
app:
  upload:
    diretorio: ${UPLOAD_DIR:/tmp/eduspring/uploads}
    tamanho-maximo-mb: 5

application.properties vs application.yml

Funcionalmente equivalentes: o Spring Boot carrega ambos. .properties é plano (chave=valor); .yml favorece hierarquia e leitura humana. Você pode misturar perfis: application-dev.properties e application-prod.yml. A precedência segue a documentação de configuração externa.

application.properties (exemplo equivalente)properties
spring.application.name=eduspring
spring.datasource.url=jdbc:postgresql://localhost:5432/eduspring
spring.datasource.username=${DB_USER:postgres}
spring.datasource.password=${DB_PASS:postgres}
server.port=8080

Sobrescrevendo propriedades: linha de comando e ambiente

Em produção, o padrão é injetar segredos e URLs via variáveis de ambiente (ex.: SPRING_DATASOURCE_URL) ou argumentos ao iniciar o JAR: java -jar app.jar --server.port=9090. Isso evita commit de credenciais e permite o mesmo artefato em vários ambientes.

Propriedades pontuais com @Value

Use quando precisar de um único valor em um bean — não para árvores grandes de configuração (aí prefira @ConfigurationProperties).

Injeção com @Valuejava
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class MatriculaPolicyService {

    private final int diasPrazoPagamento;
    private final String ambiente;

    public MatriculaPolicyService(
            @Value("${app.matricula.dias-prazo-pagamento:5}") int diasPrazoPagamento,
            @Value("${spring.profiles.active:default}") String ambiente) {
        this.diasPrazoPagamento = diasPrazoPagamento;
        this.ambiente = ambiente;
    }
}

Callbacks do ciclo de vida dos beans

O Spring permite executar código após construção e antes da destruição do bean — útil para abrir recursos, pré-carregar cache ou registrar shutdown graceful.

Ciclo de vida — anotações e interfacesjava
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmup implements InitializingBean, DisposableBean {

    @PostConstruct
    public void aposInjecao() {
        // roda depois do construtor e depois de todas as dependências injetadas
    }

    @Override
    public void afterPropertiesSet() {
        // alternativa “Spring clássica” ao @PostConstruct
    }

    @PreDestroy
    public void antesEncerrar() {
        // chamado no shutdown do contexto (exceto kill -9)
    }

    @Override
    public void destroy() {
        // alternativa a @PreDestroy
    }
}
@Bean com initMethod / destroyMethodjava
@Bean(initMethod = "abrir", destroyMethod = "fechar")
public MeuClienteGrpc meuCliente() {
    return new MeuClienteGrpc();
}

@ConfigurationProperties — Configuração tipada

UploadProperties.javajava
package br.com.dfcode.eduspring.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.nio.file.Path;

// Mapeia automaticamente as propriedades app.upload.* para este objeto
// Vantagem: tipagem, validação e autocomplete na IDE
@Component
@ConfigurationProperties(prefix = "app.upload")
public class UploadProperties {

    private Path diretorio;
    private int tamanhoMaximoMb;

    // getters e setters obrigatórios (ou use Record com @ConfigurationProperties no construtor)
    public Path getDiretorio() { return diretorio; }
    public void setDiretorio(Path diretorio) { this.diretorio = diretorio; }
    public int getTamanhoMaximoMb() { return tamanhoMaximoMb; }
    public void setTamanhoMaximoMb(int t) { this.tamanhoMaximoMb = t; }
}
Diagrama 8.1 — Precedência das fontes de configuração (maior número = maior prioridade)
flowchart TD A["1. Defaults do Spring Boot"] B["2. application.yml"] C["3. application-dev.yml (profile ativo)"] D["4. Variáveis de ambiente do SO"] E["5. Argumentos da linha de comando --porta=9090"] A --> B --> C --> D --> E style E fill:#6aaa4b,color:#fff style D fill:#4a8a2e,color:#fff
8
Exercício — @ConfigurationProperties customizado

Objetivo

Criar um bean de configuração tipado para as regras de matrícula.

Tarefa

  • Criar classe MatriculaProperties mapeando o prefixo app.matricula.
  • Adicionar no application.yml: app.matricula.vagas-padrao: 30 e app.matricula.dias-prazo-pagamento: 5.
  • Injetar MatriculaProperties no MatriculaService e usar o valor de vagas na regra de negócio.

Critério de aceite

Alterar o valor no application.yml e reiniciar — a regra de vagas deve usar o novo valor sem mudar nenhuma linha de código Java.

💬 Eventos customizados no domínio

Publicação e consumo de eventos Spring (ApplicationEventPublisher, @EventListener, @TransactionalEventListener) estão detalhados no capítulo 16, alinhados ao uso em APIs e efeitos pós-commit.

✓ O que aprendemos
  • Auto-configuration usa @Conditional para configurar beans só quando necessário.
  • Starters agrupam dependências compatíveis em uma única declaração.
  • application.yml e application.properties são equivalentes; escolha por legibilidade e padrão do time.
  • Linha de comando e variáveis de ambiente sobrescrevem o arquivo — fluxo típico de deploy.
  • @Value injeta propriedades pontuais; @ConfigurationProperties agrupa configurações tipadas.
  • @PostConstruct/@PreDestroy, InitializingBean/DisposableBean e initMethod modelam o ciclo de vida do bean.
  • Variáveis de ambiente têm prioridade alta sobre arquivos — essencial para produção.
Parte 3 — Spring Boot
Capítulo 10

Profiles e Ambientes

Uma aplicação profissional precisa se comportar de forma diferente em desenvolvimento, testes e produção — sem mudar código. Os Profiles do Spring permitem isso com elegância.

O problema sem Profiles

Sem profiles, você teria que comentar/descomentar configurações ao fazer deploy, ou pior, manter strings de conexão de produção no código-fonte. Ambas as abordagens são perigosas e trabalhosas.

Configuração por arquivo de profile

application-dev.ymlyaml
spring:
  datasource:
    url: jdbc:h2:mem:eduspring_dev     # banco em memória, zerado a cada restart
    driver-class-name: org.h2.Driver
  h2:
    console:
      enabled: true                    # acesso em /h2-console
  jpa:
    show-sql: true                     # loga todas as queries no console

logging:
  level:
    br.com.dfcode: DEBUG               # log detalhado do nosso código
application-prod.ymlyaml
spring:
  datasource:
    url: ${DATABASE_URL}               # obrigatório via variável de ambiente
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
  jpa:
    show-sql: false

logging:
  level:
    root: WARN
    br.com.dfcode: INFO

Beans condicionais por Profile

EmailConfig.javajava
// Interface comum
public interface EmailService {
    void enviar(String destinatario, String assunto, String corpo);
}

// Implementação de desenvolvimento: apenas loga, não envia
@Service
@Profile("dev")  // só ativo com spring.profiles.active=dev
public class EmailServiceFake implements EmailService {
    private static final Logger log = LoggerFactory.getLogger(EmailServiceFake.class);

    @Override
    public void enviar(String dest, String assunto, String corpo) {
        log.info("[FAKE EMAIL] Para: {} | Assunto: {}", dest, assunto);
        // não envia nada — ideal para dev e testes
    }
}

// Implementação de produção: envia de verdade via JavaMail
@Service
@Profile("prod")  // só ativo em produção
public class EmailServiceSmtp implements EmailService {
    // implementação com JavaMailSender
}

Ativando um Profile

Formas de ativar um profilebash
# 1. No application.yml (padrão para desenvolvimento)
# spring.profiles.active=dev

# 2. Variável de ambiente (recomendado para produção)
export SPRING_PROFILES_ACTIVE=prod
java -jar eduspring.jar

# 3. Argumento na linha de comando
java -jar eduspring.jar --spring.profiles.active=prod

# 4. No Docker Compose
environment:
  - SPRING_PROFILES_ACTIVE=prod
Diagrama 10.1 — Beans ativos por ambiente
flowchart LR subgraph dev ["Profile: dev"] D1["H2 em memória"] D2["EmailServiceFake"] D3["show-sql: true"] end subgraph test ["Profile: test"] T1["H2 em memória isolado"] T2["EmailServiceFake"] T3["@Sql para dados de teste"] end subgraph prod ["Profile: prod"] P1["PostgreSQL via env vars"] P2["EmailServiceSmtp"] P3["Connection Pool HikariCP"] end
9
Exercício — Três profiles para o EduSpring

Objetivo

Configurar os três ambientes do projeto EduSpring.

Tarefa

  • Criar application-dev.yml com H2 + show-sql + h2-console.
  • Criar application-test.yml com H2 sem show-sql.
  • Criar application-prod.yml com PostgreSQL via variáveis de ambiente.
  • Implementar EmailServiceFake e EmailServiceSmtp com @Profile correto.

Critério de aceite

  • Com profile dev: console H2 acessível e SQL logado.
  • Com profile prod sem variáveis de ambiente definidas: aplicação falha na inicialização com mensagem clara.
✓ O que aprendemos
  • Profiles permitem comportamentos distintos por ambiente sem alterar código.
  • Arquivos application-{profile}.yml sobrescrevem as propriedades do application.yml base.
  • @Profile("dev") em uma classe a torna ativa apenas quando o profile dev estiver ativo.
  • Credenciais de produção nunca devem estar em arquivos de configuração — use variáveis de ambiente.
Parte 3 — Spring Boot
Capítulo 11

Actuator e Monitoramento

Uma aplicação em produção precisa ser observável. O Spring Boot Actuator expõe endpoints de monitoramento que permitem checar saúde, métricas e configurações em tempo real — sem uma linha de código adicional.

O que é o Spring Boot Actuator?

O Actuator é um módulo do Spring Boot que adiciona automaticamente endpoints HTTP de gestão à sua aplicação. Com uma única dependência, você ganha visibilidade total sobre o estado da aplicação em produção.

pom.xmlxml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Endpoints principais

EndpointO que mostra
/actuator/healthStatus de saúde (UP/DOWN) + verificações customizadas
/actuator/infoInformações da aplicação (versão, ambiente, build)
/actuator/metricsMétricas: JVM, HTTP, banco de dados, custom
/actuator/envTodas as propriedades de configuração ativas
/actuator/mappingsLista de todos os endpoints HTTP registrados
/actuator/beansTodos os beans registrados no contexto Spring

Configurando o Actuator com segurança

application.yml — configuração do Actuatoryaml
management:
  endpoints:
    web:
      exposure:
        # Expõe apenas health e info publicamente
        # Em produção, nunca exponha 'env' ou 'beans' sem autenticação!
        include: health,info,metrics
  endpoint:
    health:
      show-details: when-authorized  # detalhes só para usuários autenticados

info:
  app:
    nome: EduSpring
    versao: "@project.version@"      # lido automaticamente do pom.xml
    ambiente: ${SPRING_PROFILES_ACTIVE:dev}

HealthIndicator customizado

BancoDadosHealthIndicator.javajava
package br.com.dfcode.eduspring.health;

import org.springframework.boot.actuate.health.*;
import org.springframework.stereotype.Component;

// Implementar HealthIndicator adiciona uma verificação ao /actuator/health
@Component
public class CursosAtivosHealthIndicator implements HealthIndicator {

    private final CursoRepository cursoRepository;

    public CursosAtivosHealthIndicator(CursoRepository cursoRepository) {
        this.cursoRepository = cursoRepository;
    }

    @Override
    public Health health() {
        long cursosAtivos = cursoRepository.countByAtivoTrue();

        if (cursosAtivos == 0) {
            // DOWN indica que algo está errado que precisa de atenção
            return Health.down()
                .withDetail("motivo", "Nenhum curso ativo cadastrado")
                .withDetail("cursosAtivos", 0)
                .build();
        }

        return Health.up()
            .withDetail("cursosAtivos", cursosAtivos)
            .build();
    }
}
Diagrama 11.1 — Load balancer verificando saúde da aplicação
sequenceDiagram participant LB as Load Balancer participant App as EduSpring participant DBI as CursosAtivosIndicator participant DB as PostgreSQL LB->>App: GET /actuator/health (a cada 30s) App->>DBI: health() DBI->>DB: countByAtivoTrue() DB-->>DBI: 15 DBI-->>App: Health.up com detalhes App-->>LB: 200 OK body status UP Note over LB: mantém instância no pool LB->>App: GET /actuator/health App->>DBI: health() DBI->>DB: countByAtivoTrue() DB-->>DBI: 0 DBI-->>App: Health.down App-->>LB: 503 body status DOWN Note over LB: remove instância do pool
11
Exercício — HealthIndicator de integração externa

Objetivo

Criar um health indicator que verifica a conectividade com um serviço externo.

Tarefa

  • Criar ViaCepHealthIndicator que faz um GET em https://viacep.com.br/ws/01001000/json/.
  • Se a resposta for 200, retornar Health.up().
  • Se falhar (timeout ou erro), retornar Health.down() com detalhe do erro.
  • Configurar timeout de 3 segundos para não bloquear o health check.

Critério de aceite

GET /actuator/health exibe a chave viaCep com status UP/DOWN.

✓ O que aprendemos
  • O Actuator adiciona endpoints de monitoramento com uma única dependência.
  • Em produção, exponha apenas health e info publicamente — proteja os demais.
  • HealthIndicator permite adicionar verificações customizadas ao /actuator/health.
  • Load balancers usam o health endpoint para decidir se mantêm uma instância no pool.
Parte 4 — Spring Data JPA
Capítulo 12

JPA e Hibernate com Spring

JPA (Jakarta Persistence API) é a especificação Java para mapeamento objeto-relacional. O Hibernate é a implementação mais popular. O Spring Data JPA simplifica ainda mais o uso de ambos, eliminando código repetitivo.

JPA vs Hibernate: qual a diferença?

JPA é uma especificação (conjunto de interfaces e anotações). Hibernate é uma implementação concreta dessa especificação. Você programa contra a API JPA, e o Spring Boot usa o Hibernate por baixo dos panos por padrão.

Spring Boot, JpaRepository e persistência

Com spring-boot-starter-data-jpa, o Boot configura DataSource, EntityManagerFactory e o escaneamento de repositórios no pacote da aplicação. Use @EnableJpaRepositories(basePackages = "...") apenas quando houver módulos com pacotes de repositório distintos ou mais de uma unidade de persistência — cenário comum em monólitos modulares.

O save de JpaRepository delega ao EntityManager: para instâncias consideradas novas chama-se persist; para existentes, merge. A detecção de “novo” usa propriedade de versão, id nulo ou implementação de Persistable, conforme a seção sobre persistência de entidades na referência do Spring Data JPA.

Consultas derivadas do nome do método, @Query (JPQL ou nativo), @Modifying, @EntityGraph, Specification, procedimentos e auditoria estão organizados no manual JPA do Spring Data — mantenha esse link como guia ao lado dos capítulos de repositório.

Mapeando Entidades

Aluno.javajava
package br.com.dfcode.eduspring.model;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;

// @Entity indica que esta classe é gerenciada pelo JPA
// Cada instância corresponde a uma linha na tabela
@Entity
// @Table define o nome da tabela no banco (opcional se igual ao nome da classe)
@Table(name = "alunos")
// Lombok gera getters, setters, equals, hashCode e toString
// Atenção: NÃO use @Data em entidades — causa problemas com lazy loading
@Getter @Setter
@NoArgsConstructor  // JPA exige construtor sem argumentos
@AllArgsConstructor
@Builder            // permite construção fluente: Aluno.builder().nome("...").build()
public class Aluno {

    @Id
    // IDENTITY delega a geração de ID para o banco (auto_increment / serial)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // @Column permite customizar nome, tamanho, nullable
    @Column(nullable = false, length = 100)
    private String nome;

    @Column(nullable = false, unique = true, length = 150)
    private String email;

    @Column(nullable = false, unique = true)
    private Integer matricula;

    @Column(name = "data_nascimento")
    private LocalDate dataNascimento;

    @Column(nullable = false)
    private Boolean ativo = true;
}
Diagrama 12.1 — Modelo de classes do projeto EduSpring
classDiagram class Aluno { Long id String nome String email Integer matricula LocalDate dataNascimento Boolean ativo } class Curso { Long id String nome String descricao Integer cargaHoraria LocalDate dataInicio Boolean ativo } class Matricula { Long id LocalDate dataMatricula String status } class Professor { Long id String nome String especialidade } Aluno "1" --> "*" Matricula : realiza Curso "1" --> "*" Matricula : recebe Professor "1" --> "*" Curso : ministra

Configuração JPA no application.yml

application-dev.yml (trecho JPA)yaml
spring:
  jpa:
    hibernate:
      # validate: valida o schema sem modificar — use em produção com Flyway
      # create-drop: recria o banco ao iniciar/parar — use apenas em testes
      # update: atualiza o schema — NUNCA use em produção
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true               # SQL formatado e legível no log
        use_sql_comments: true         # adiciona comentário com o tipo de operação
🛑 Nunca use ddl-auto: create ou update em produção

Essas opções podem apagar ou alterar dados sem aviso. Em produção, use sempre validate (ou none) e gerencie o schema com Flyway (capítulo seguinte).

10
Exercício — Mapeando a entidade Professor

Objetivo

Mapear a entidade Professor e verificar o DDL gerado.

Tarefa

  • Criar Professor.java com: id, nome (obrigatório), email (único), especialidade, ativo.
  • Usar @Getter @Setter @Builder do Lombok.
  • Rodar com show-sql: true e verificar no console o CREATE TABLE gerado.

Critério de aceite

O log exibe CREATE TABLE professores (id BIGSERIAL PRIMARY KEY, nome VARCHAR(100) NOT NULL, ...) ao iniciar.

✓ O que aprendemos
  • JPA é a especificação; Hibernate é a implementação padrão no Spring Boot.
  • @Entity, @Table, @Id, @GeneratedValue e @Column são as anotações fundamentais de mapeamento.
  • Lombok elimina boilerplate, mas evite @Data em entidades por conta do lazy loading.
  • ddl-auto: create-drop é útil em desenvolvimento; em produção use validate + Flyway.
Parte 4 — Spring Data JPA
Capítulo 13

Repositórios Spring Data

O Spring Data JPA elimina a necessidade de escrever implementações de acesso a dados. Você declara uma interface, e o Spring gera toda a implementação em tempo de execução — incluindo queries complexas a partir do nome do método.

A hierarquia de repositórios

InterfaceMétodos incluídosQuando usar
Repository<T,ID>Nenhum (marcador)Quando quer controle total
CrudRepository<T,ID>save, findById, findAll, delete...CRUD básico
PagingAndSortingRepository+ findAll(Pageable), findAll(Sort)Paginação e ordenação
JpaRepository<T,ID>+ saveAll, flush, deleteInBatch...Maioria dos casos — recomendado
CursoRepository.javajava
package br.com.dfcode.eduspring.repository;

import br.com.dfcode.eduspring.model.Curso;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;

// JpaRepository já inclui todos os métodos CRUD — sem código adicional
// Spring Data gera a implementação em runtime
public interface CursoRepository extends JpaRepository<Curso, Long> {

    // 1. Query Method por convenção de nome
    // Spring traduz para: SELECT * FROM cursos WHERE ativo = true
    List<Curso> findAllByAtivoTrue();

    // 2. Busca por nome contendo texto (case-insensitive)
    // Gera: WHERE LOWER(nome) LIKE LOWER('%?%')
    List<Curso> findByNomeContainingIgnoreCase(String nome);

    // 3. Múltiplos critérios
    Optional<Curso> findByNomeAndAtivoTrue(String nome);

    // 4. @Query com JPQL (orientado a objetos — usa nome da classe, não da tabela)
    @Query("SELECT c FROM Curso c WHERE c.cargaHoraria >= :minimo AND c.ativo = true")
    List<Curso> buscarComCargaMinima(@Param("minimo") Integer cargaMinima);

    // 5. @Query com SQL nativo (usa nome da tabela real)
    @Query(value = "SELECT * FROM cursos WHERE data_inicio = CURRENT_DATE", nativeQuery = true)
    List<Curso> buscarComInicioHoje();

    // 6. Paginação — Pageable permite page, size e sort na requisição
    Page<Curso> findByAtivoTrue(Pageable pageable);

    // 7. Projeção com interface — retorna apenas campos necessários
    List<CursoResumo> findAllProjectedBy();

    // 8. Contagem
    long countByAtivoTrue();

    // 9. Verificação de existência
    boolean existsByMatricula(Integer matricula);
}

// Interface de projeção — Spring gera a implementação automaticamente
interface CursoResumo {
    Long getId();
    String getNome();
    Integer getCargaHoraria();
}
Diagrama 13.1 — Como o Spring Data intercepta chamadas ao repositório
sequenceDiagram participant Svc as CursoService participant Repo as CursoRepositoryIface participant Proxy as JPAProxyRuntime participant EM as EntityManager participant DB as PostgreSQL Svc->>Repo: findByNomeContainingIgnoreCase("java") Note over Repo: é apenas uma interface! Repo->>Proxy: Spring Data intercepta a chamada Proxy->>Proxy: analisa o nome do método Proxy->>EM: createQuery("...LIKE...") EM->>DB: SELECT * FROM cursos WHERE LOWER(nome) LIKE '%java%' DB-->>EM: [Curso{Java Básico}, Curso{Java Avançado}] EM-->>Proxy: List<Curso> Proxy-->>Svc: List<Curso>

Projeções: o que são, como e quando usar

No Spring Data JPA, uma projeção é um tipo de retorno que não carrega a entidade inteira: você declara só os campos necessários. Formas usuais: interface com getters compatíveis com propriedades da entidade (ex.: getId(), getNome()), DTO imutável (record ou classe) com construtor ou propriedades alinhadas às colunas do SELECT, ou projeções dinâmicas descritas na documentação de Projections.

Quando usar: listagens e leituras em que expor o grafo completo da entidade seria pesado ou inseguro; quando você quer colunas calculadas ou joins expressos em JPQL/SQL sem mapear nova entidade; para reduzir memória e custo de IO. Quando evitar ou ter cuidado: quando o time ainda não estabilizou o contrato da API — projeções exigem manter getters/aliases coerentes com a query; em interfaces fechadas, campos aninhados seguem regras específicas do Spring Data.

O exemplo CursoResumo e o DTO nativo mais adiante neste capítulo são ambos projeções: o primeiro limita colunas via mecanismo de projeção do repositório; o segundo mapeia explicitamente aliases SQL para getters JavaBean.

SQL nativo com mapeamento para DTO (interface ou classe)

Quando você precisa de uma query nativa complexa (funções do banco, JOIN com tabelas fora do JPA), use nativeQuery = true. Para não retornar a entidade inteira, mapeie o resultado para um DTO com interface de projeção ou SqlResultSetMapping / construtor.

RelatorioCursoRepository.java — nativo + DTOjava
// DTO como interface: getters devem bater com alias da query (case-sensitive no PostgreSQL com aspas)
public interface CursoMatriculaResumoDto {
    Long getCursoId();
    String getNomeCurso();
    Long getTotalMatriculas();
}

public interface RelatorioCursoRepository extends JpaRepository<Curso, Long> {

    // Os aliases (AS curso_id, nome_curso...) devem seguir o padrão JavaBean: cursoId -> getCursoId()
    @Query(value = """
        SELECT c.id AS curso_id,
               c.nome AS nome_curso,
               COUNT(m.id) AS total_matriculas
        FROM cursos c
        LEFT JOIN matriculas m ON m.curso_id = c.id
        WHERE c.ativo = true
        GROUP BY c.id, c.nome
        ORDER BY total_matriculas DESC
        """, nativeQuery = true)
    List<CursoMatriculaResumoDto> relatorioMatriculasPorCurso();
}

// Alternativa com Record + construtor (Spring Data 3.2+ / Hibernate 6):
// @Query(... nativeQuery = true)
// List<MeuRecord> buscar();  // colunas = parâmetros do record na mesma ordem

Chamada de stored procedure

No PostgreSQL, procedures podem ser invocadas com @Procedure (nome do procedimento no banco) ou @Query com CALL. A entidade abaixo declara o mapeamento JPA para o procedimento.

SQL no PostgreSQL + Curso.java (trecho)sql
-- Exemplo: procedure que retorna quantidade de vagas disponíveis
CREATE OR REPLACE PROCEDURE sp_atualizar_vagas_curso(IN p_curso_id BIGINT)
LANGUAGE plpgsql AS $$
BEGIN
  UPDATE cursos SET vagas = vagas - 1 WHERE id = p_curso_id AND vagas > 0;
END;
$$;
CursoRepository.java — @Procedurejava
// Na entidade Curso (opcional, para nome catalogado):
// @NamedStoredProcedureQuery(
//   name = "Curso.atualizarVagas",
//   procedureName = "sp_atualizar_vagas_curso",
//   parameters = @StoredProcedureParameter(name = "p_curso_id", type = Long.class, mode = ParameterMode.IN))

public interface CursoRepository extends JpaRepository<Curso, Long> {

    // Mapeia pelo nome do procedimento no banco
    @Procedure(procedureName = "sp_atualizar_vagas_curso")
    void atualizarVagasProcedure(@Param("p_curso_id") Long cursoId);

    // Para FUNCTION que retorna valor, use @Query com nativeQuery:
    // @Query(value = "SELECT fn_total_alunos_curso(:id)", nativeQuery = true)
    // Long totalAlunos(@Param("id") Long cursoId);
}

Mapeando uma VIEW do banco

Views são somente leitura no modelo relacional. No JPA, use @Immutable, @Table(name = "nome_da_view") e evite save — trate como leitura.

VwCursoResumo.javajava
import org.hibernate.annotations.Immutable;
import jakarta.persistence.*;

@Entity
@Immutable                    // Hibernate: entidade somente leitura
@Table(name = "vw_curso_resumo")  // nome exato da VIEW no PostgreSQL
public class VwCursoResumo {

    @Id
    private Long cursoId;

    private String nome;
    private Long totalMatriculas;
    // getters ...
}

public interface VwCursoResumoRepository extends JpaRepository<VwCursoResumo, Long> {
    List<VwCursoResumo> findByTotalMatriculasGreaterThan(long minimo);
}
⚠ Atenção

Procedures e nomes de colunas variam entre PostgreSQL, MySQL e Oracle. Sempre valide o dialeto SQL e os parâmetros IN/OUT na documentação do seu banco.

11
Exercício — Busca avançada de alunos

Objetivo

Criar queries no repositório de alunos.

Tarefa

  • Criar AlunoRepository com: busca por e-mail, busca por nome (contendo, ignore case), listagem de alunos ativos ordenados por nome.
  • Criar query JPQL que retorna alunos com mais de X matrículas realizadas.
  • Criar projeção AlunoResumo com apenas id, nome e email.

Critério de aceite

GET /alunos/buscar?nome=jo usa o repositório e retorna lista correta; projeção não inclui campos sensíveis.

✓ O que aprendemos
  • O Spring Data JPA gera implementações de repositório em runtime a partir de interfaces.
  • Query Methods derivam queries SQL do nome do método — sem código adicional.
  • @Query com JPQL usa nomes de classes e campos Java (não de tabelas).
  • Projeções retornam apenas os campos necessários, melhorando performance.
Parte 4 — Spring Data JPA
Capítulo 14

Transações: REQUIRED e REQUIRES_NEW

O Spring gerencia transações declarativamente com @Transactional. Entender a propagação evita bugs sutis: commits parciais, leitura de dados não commitados e deadlocks.

O que é propagação?

Quando um método @Transactional chama outro método também transacional, o Spring precisa decidir se reutiliza a transação existente ou abre uma nova. Essa regra é a propagação (Propagation).

ValorComportamentoUso típico
REQUIRED (padrão)Se já existe transação, participa dela; senão, cria uma.Maioria dos serviços — uma única transação do controller até o repository.
REQUIRES_NEWSempre suspende a atual e abre uma nova transação; ao terminar, commit/rollback só dela.Auditoria, log de erro que deve persistir mesmo se a principal der rollback; notificações isoladas.
MANDATORYExige transação ativa; senão, exceção.Métodos internos que nunca devem ser a raiz da transação.
NOT_SUPPORTEDExecuta sem transação (suspende se houver).Chamadas a recursos que não suportam transação JTA.
MatriculaService.java — REQUIRED (implícito)java
@Service
public class MatriculaService {

    // Propagation.REQUIRED é o default — não precisa anotar
    @Transactional
    public void matricular(Long alunoId, Long cursoId) {
        // tudo aqui roda na MESMA transação:
        // se qualquer save() falhar, TUDO faz rollback
        alunoRepository.findById(alunoId).orElseThrow();
        cursoRepository.findById(cursoId).orElseThrow();
        matriculaRepository.save(new Matricula(alunoId, cursoId));
    }
}

Quando a transação faz rollback

Por padrão, o Spring desfaz a transação (rollback) se o método transacional propagar uma exceção não verificada (RuntimeException ou Error). Exceções verificadas (Exception checada) não causam rollback, a menos que você configure rollbackFor.

MatriculaService.java — rollback automático (runtime)java
@Service
public class MatriculaService {

    private final MatriculaRepository matriculaRepository;
    private final CursoRepository cursoRepository;

    @Transactional
    public void matricularComReservaDeVaga(Long alunoId, Long cursoId) {
        Curso curso = cursoRepository.findByIdForUpdate(cursoId)
            .orElseThrow(() -> new RecursoNaoEncontradoException("Curso", cursoId));
        // decrementa vaga — ainda na mesma transação
        curso.decrementarVaga();
        cursoRepository.save(curso);

        matriculaRepository.save(new Matricula(alunoId, cursoId));

        // Qualquer RuntimeException após os saves: Hibernate/JPA marca a TX para rollback
        if (curso.getVagas() < 0) {
            throw new RegraVioladaException("Vagas inconsistentes — rollback de curso + matrícula");
        }
        // Se este throw ocorrer, NADA do método é commitado no banco.
    }
}
CursoService.java — rollback em exceção checadajava
@Service
public class CursoService {

    // Por padrão, IOException NÃO daria rollback — rollbackFor corrige isso
    @Transactional(rollbackFor = Exception.class)
    public void importarCursosDeArquivo(Path arquivo) throws IOException {
        List<Curso> linhas = lerLinhas(arquivo);
        for (Curso c : linhas) {
            cursoRepository.save(c);
        }
        // Se IOException for lançada dentro do método, a transação inteira desfaz os saves
    }
}
Rollback apenas no código (sem throw)java
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

@Service
public class MatriculaService {

    @Transactional
    public void matricularComValidacaoExterna(Long alunoId, Long cursoId, boolean aprovadoPeloAntiFraude) {
        matriculaRepository.save(new Matricula(alunoId, cursoId));
        if (!aprovadoPeloAntiFraude) {
            // Marca a transação atual para rollback; ao sair do método, commit não ocorre
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return;
        }
        // ... continua o fluxo feliz
    }
}

“Proxy” de persistência: transação programática

O @Transactional funciona porque o Spring envolve seu bean em um proxy (JDK ou CGLIB) que abre/commita ou faz rollback da transação antes e depois do método real. Quando você precisa do mesmo controle sem anotação no método público (ex.: laços, APIs legadas), use TransactionTemplate — é o equivalente imperativo ao que o proxy declarativo faz por baixo dos panos.

MatriculaPersistenceFacade.java — unit of work explícitojava
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * Fachada que concentra gravações de matrícula com transação explícita.
 * Útil quando a lógica não cabe em um único método @Transactional ou vem de código externo.
 */
@Component
public class MatriculaPersistenceFacade {

    private final TransactionTemplate transactionTemplate;
    private final MatriculaRepository matriculaRepository;
    private final CursoRepository cursoRepository;

    public MatriculaPersistenceFacade(
            TransactionTemplate transactionTemplate,
            MatriculaRepository matriculaRepository,
            CursoRepository cursoRepository) {
        this.transactionTemplate = transactionTemplate;
        this.matriculaRepository = matriculaRepository;
        this.cursoRepository = cursoRepository;
    }

    public Matricula salvarMatriculaComAtualizacaoDeVaga(Long alunoId, Long cursoId) {
        // execute abre TX, commita se o callback terminar sem rollback; senão rollback
        return transactionTemplate.execute(status -> {
            Curso curso = cursoRepository.findById(cursoId)
                .orElseThrow(() -> new RecursoNaoEncontradoException("Curso", cursoId));
            curso.decrementarVaga();
            cursoRepository.save(curso);
            return matriculaRepository.save(new Matricula(alunoId, cursoId));
        });
    }
}
📄 Onde entra o “proxy”?

Com @Transactional, o bean injetado no controller é na verdade um proxy: chamadas de fora passam pelo interceptor que associa a transação ao Thread atual. Com TransactionTemplate, quem abre/fecha a transação é o template (usando o mesmo PlatformTransactionManager). Os dois compartilham o mesmo mecanismo de commit/rollback; a diferença é declarativo vs programático.

TransactionConfig.java — registrar TransactionTemplatejava
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

@Configuration
public class TransactionConfig {

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager txManager) {
        return new TransactionTemplate(txManager);
    }
}
AuditoriaService.java — REQUIRES_NEWjava
@Service
public class AuditoriaService {

    // Nova transação independente: commit mesmo se a transação "pai" der rollback
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void registrarTentativaFalha(String operacao, String detalhe) {
        logEventoRepository.save(new LogEvento(operacao, detalhe, Instant.now()));
    }
}

@Service
public class MatriculaService {
    private final AuditoriaService auditoria;

    @Transactional
    public void matricular(Long alunoId, Long cursoId) {
        try {
            // lógica principal...
        } catch (Exception e) {
            auditoria.registrarTentativaFalha("MATRICULA", e.getMessage());
            throw e; // rollback da transação principal, mas o log já foi commitado em REQUIRES_NEW
        }
    }
}
🛑 Proxy e @Transactional

A anotação só funciona em métodos chamados por fora do bean (via proxy). Chamadas internas this.metodo() não passam pelo proxy — a transação não é aplicada no método interno. Extraia para outro @Service, use TransactionTemplate dentro do mesmo bean ou self-injection com cuidado (evite ciclos).

Diagrama 12.1 — REQUIRED vs REQUIRES_NEW
sequenceDiagram participant C as Controller participant M as MatriculaService participant A as AuditoriaService participant DB as Banco C->>M: matricular() [TX1 REQUIRED] M->>DB: operações na TX1 M->>A: registrar() [TX2 REQUIRES_NEW] Note over A,DB: TX1 suspensa A->>DB: INSERT log — commit TX2 Note over M,DB: TX1 retomada M->>DB: erro — rollback TX1 Note over DB: log permanece (TX2 já commitou)
12
Exercício — Auditoria com REQUIRES_NEW

Objetivo

Garantir que um registro de auditoria seja salvo mesmo quando a operação principal falhar.

Tarefa

  • Criar entidade AuditoriaOperacao e repository.
  • Serviço com @Transactional(propagation = REQUIRES_NEW) que grava toda tentativa de matrícula.
  • Forçar erro após a auditoria e verificar no banco que o registro de auditoria existe.

Critério de aceite

Rollback da matrícula + linha de auditoria persistida.

✓ O que aprendemos
  • REQUIRED participa da transação existente ou cria uma nova.
  • REQUIRES_NEW isola commit/rollback em uma transação separada.
  • Rollback padrão em RuntimeException/Error; use rollbackFor para exceções checadas.
  • TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() aborta a TX sem lançar exceção.
  • TransactionTemplate oferece o mesmo commit/rollback do proxy, de forma programática.
  • Transações em chamadas internas this.x() não ativam proxy — prefira outro bean ou template.
Parte 4 — Spring Data JPA
Capítulo 15

Recursos Avançados de Persistência

Com as bases do JPA consolidadas, é hora de aprender os recursos que tornam sua camada de dados verdadeiramente profissional: paginação, filtros dinâmicos, auditoria automática e cache.

Paginação com Pageable

CursoController.java + CursoRepository.javajava
// No Controller: Spring MVC popula o Pageable automaticamente a partir dos query params
// GET /cursos?page=0&size=10&sort=nome,asc
@GetMapping
public Page<CursoResponse> listar(
        @RequestParam(defaultValue = "") String nome,
        Pageable pageable) {                    // injetado automaticamente pelo Spring MVC
    return cursoRepository
        .findByNomeContainingIgnoreCaseAndAtivoTrue(nome, pageable)
        .map(CursoResponse::de);              // transforma Page<Curso> em Page<CursoResponse>
}

// No Repository:
Page<Curso> findByNomeContainingIgnoreCaseAndAtivoTrue(String nome, Pageable pageable);

A resposta já inclui metadados de paginação automaticamente:

Resposta paginadajson
{
  "content": [ {"id":1,"nome":"Java Básico"}, ... ],
  "pageable": { "pageNumber": 0, "pageSize": 10 },
  "totalElements": 47,
  "totalPages": 5,
  "last": false,
  "first": true
}

Auditoria Automática

EntidadeAuditavel.javajava
@MappedSuperclass  // campos herdados pelas entidades filhas
@EntityListeners(AuditingEntityListener.class) // ativa a auditoria JPA
public abstract class EntidadeAuditavel {

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

    @LastModifiedDate
    private LocalDateTime atualizadoEm;
}

// Entidade que herda auditoria
@Entity
public class Curso extends EntidadeAuditavel {
    // ... outros campos
}

// Habilitar no @SpringBootApplication
@SpringBootApplication
@EnableJpaAuditing  // ativa os listeners de auditoria
public class EduspringApplication { ... }

Cache com Spring Cache + Caffeine

CursoService.java (com cache)java
// pom.xml: adicionar spring-boot-starter-cache e com.github.ben-manes.caffeine:caffeine

@Service
public class CursoService {

    // @Cacheable: na primeira chamada busca no banco e armazena no cache
    // nas chamadas seguintes com mesmo 'id', retorna do cache sem ir ao banco
    @Cacheable(value = "cursos", key = "#id")
    public CursoResponse buscarPorId(Long id) {
        return cursoRepository.findById(id)
            .map(CursoResponse::de)
            .orElseThrow(() -> new RecursoNaoEncontradoException("Curso", id));
    }

    // @CacheEvict: limpa o cache quando o curso é atualizado
    @CacheEvict(value = "cursos", key = "#id")
    public CursoResponse atualizar(Long id, CriarCursoRequest request) {
        // ...
    }
}
Diagrama 13.1 — Fluxo de requisição paginada
sequenceDiagram participant C as Cliente participant Ctrl as CursoController participant Repo as CursoRepository participant DB as PostgreSQL C->>Ctrl: GET /cursos?page=0&size=10&sort=nome,asc Note over Ctrl: Spring MVC monta Pageable automaticamente Ctrl->>Repo: findByNomeContaining("", Pageable{page=0,size=10,sort=nome}) Repo->>DB: SELECT * FROM cursos ORDER BY nome LIMIT 10 OFFSET 0 DB-->>Repo: 10 registros + total count Repo-->>Ctrl: Page{content=[...], totalElements=47} Ctrl-->>C: 200 OK {"content":[...],"totalElements":47,"totalPages":5}
13
Exercício — Endpoint paginado com filtros

Objetivo

Implementar listagem paginada de cursos com múltiplos filtros.

Tarefa

  • Endpoint GET /cursos aceita: nome (optional), ativo (optional), page, size, sort.
  • Adicionar campos criadoEm e atualizadoEm na entidade Curso via @CreatedDate.
  • Adicionar @Cacheable no método buscarPorId.

Critério de aceite

GET /cursos?page=0&size=5&sort=nome,asc retorna 5 cursos ordenados por nome com metadados de paginação. Segunda chamada a GET /cursos/1 não gera query SQL (servida pelo cache).

✓ O que aprendemos
  • Pageable é populado automaticamente pelo Spring MVC a partir dos query params page, size e sort.
  • Page<T> inclui metadados de paginação na resposta JSON automaticamente.
  • @CreatedDate e @LastModifiedDate com @EnableJpaAuditing preenchem timestamps automaticamente.
  • @Cacheable e @CacheEvict adicionam cache na camada de serviço sem código adicional.
Parte 4 — Integração no domínio
Capítulo 16

Eventos da aplicação (ApplicationEventPublisher)

O Spring oferece um modelo de programação orientado a eventos dentro da mesma JVM: o publicador não conhece os ouvintes, e os ouvintes não precisam ser alterados quando novos fluxos surgem — útil para efeitos colaterais (e-mail, métricas, integrações) sem acoplar o serviço principal.

Quando usar eventos internos

  • Bom: desacoplar notificações, auditoria leve, cache, métricas após uma ação de negócio.
  • Evitar: fluxo transacional crítico onde a ordem e o rollback precisam ser garantidos em uma única transação — aí use chamadas de serviço explícitas.
  • Distribuído: para várias instâncias ou sistemas externos, prefira mensageria (RabbitMQ — capítulo 26).
MatriculaRealizadaEvent.javajava
// Desde o Spring 4.2 o evento pode ser qualquer POJO (não precisa estender ApplicationEvent)
public record MatriculaRealizadaEvent(Long matriculaId, String emailAluno, String nomeCurso) {}
MatriculaService.java — publicandojava
@Service
public class MatriculaService {

    private final ApplicationEventPublisher eventPublisher;
    private final MatriculaRepository matriculaRepository;
    // ...

    @Transactional
    public MatriculaResponse matricular(Long alunoId, Long cursoId) {
        Matricula m = matriculaRepository.save(construirMatricula(alunoId, cursoId));
        String email = m.getAluno().getEmail();
        String nomeCurso = m.getCurso().getNome();

        // 1) Síncrono: listener roda ANTES do commit — use só para lógica que não depende do commit
        // 2) Para efeitos pós-commit (e-mail, fila externa), prefira @TransactionalEventListener(AFTER_COMMIT)
        eventPublisher.publishEvent(new MatriculaRealizadaEvent(m.getId(), email, nomeCurso));

        return MatriculaResponse.de(m);
    }
}

Ouvintes: ordem, assíncrono e pós-commit

Vários beans podem escutar o mesmo tipo de evento. Use @Order para controlar a sequência quando o processamento é síncrono. Com @Async, cada listener pode rodar em thread do pool — configure um executor dedicado para não competir com outras tarefas.

AsyncConfig.java — pool para eventosjava
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "eventTaskExecutor")
    public Executor eventTaskExecutor() {
        ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
        ex.setCorePoolSize(2);
        ex.setMaxPoolSize(8);
        ex.setQueueCapacity(100);
        ex.setThreadNamePrefix("eventos-");
        ex.initialize();
        return ex;
    }
}
MatriculaEventListener.java — síncrono, assíncrono e pós-commitjava
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class MatriculaEventListener {

    private final EmailService emailService;
    private final MatriculaRepository matriculaRepository;

    // Primeiro: validações baratas na mesma thread (antes ou depois do commit, conforme fase abaixo)
    @Order(1)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void aposCommitEnviarEmail(MatriculaRealizadaEvent event) {
        // Só roda se a transação do matricular() tiver dado COMMIT — evita e-mail para linha que sofreu rollback
        emailService.enviarConfirmacao(event.emailAluno(), event.nomeCurso());
    }

    @Order(2)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void aposCommitAuditoriaLeve(MatriculaRealizadaEvent event) {
        // Ex.: gravar em tabela de "eventos processados" — já enxerga dados commitados
    }

    @Async("eventTaskExecutor")
    @EventListener
    @Order(10)
    public void metricasAssincronas(MatriculaRealizadaEvent event) {
        // Roda em paralelo; não bloqueia a resposta HTTP (cuidado: pode executar antes do commit
        // se publicar o evento antes do fim da TX — por isso métricas às vezes combinam com AFTER_COMMIT)
    }

    // Estilo "clássico" ApplicationListener<T> — equivalente a @EventListener
    // @Component
    // public class MatriculaApplicationListener implements ApplicationListener<MatriculaRealizadaEvent> {
    //     public void onApplicationEvent(MatriculaRealizadaEvent e) { ... }
    // }
}
⚠ Síncrono na mesma transação vs AFTER_COMMIT

@EventListener (sem TransactionalEventListener) dispara na mesma thread, ainda dentro da transação aberta pelo @Transactional do publicador — útil para domínio puro. Já AFTER_COMMIT garante que integrações externas (SMTP, webhook) não reajam a dados que foram desfeitos por rollback.

Evento genérico e payload rico

Prefira records ou POJOs imutáveis com os IDs necessários; listeners podem recarregar o agregado do banco no AFTER_COMMIT para evitar stale state.

MatriculaRealizadaEvent.java — fonte da verdade no listenerjava
public record MatriculaRealizadaEvent(Long matriculaId, String emailAluno, String nomeCurso) {}

// No listener AFTER_COMMIT:
// var matricula = matriculaRepository.findById(event.matriculaId()).orElseThrow();
// — garante estado persistido e consistente
Diagrama 14.1 — Publicação e entrega síncrona de evento
sequenceDiagram participant S as MatriculaService participant P as ApplicationEventPublisher participant L as MatriculaEventListener participant E as EmailService S->>P: publishEvent(MatriculaRealizadaEvent) P->>L: dispatch (mesma thread por padrão) L->>E: enviarConfirmacao(...) E-->>L: ok L-->>S: retorno ao fluxo do service
14
Exercício — Evento após registro de aluno

Objetivo

Publicar AlunoRegistradoEvent após POST /alunos e tratar com listener que apenas registra log estruturado.

Critério de aceite

Log aparece após criação bem-sucedida; falha no listener não deve quebrar a resposta HTTP se configurado com @Async e tratamento de erro adequado.

✓ O que aprendemos
  • ApplicationEventPublisher.publishEvent notifica todos os listeners compatíveis.
  • @TransactionalEventListener(AFTER_COMMIT) evita efeitos colaterais quando a TX principal faz rollback.
  • @Order ordena listeners síncronos; @Async("beanExecutor") usa pool dedicado.
  • Payload com IDs + recarga no listener mantém consistência após commit.
  • Eventos internos não substituem filas (RabbitMQ/Kafka) para integração entre instâncias ou sistemas.
Parte 4 — Cross-cutting concerns
Capítulo 17

AOP com Spring (@AspectJ)

Programação Orientada a Aspectos (AOP) modulariza preocupações que atravessam várias camadas — logging, segurança, métricas, retry. O Spring AOP usa proxies em tempo de execução (JDK ou CGLIB) e a sintaxe de pointcuts no estilo AspectJ.

Conceitos e tipos de advice

Aspect = classe @Aspect que agrupa pointcuts e advices. Join point = execução de método (no Spring AOP). Pointcut = expressão AspectJ que seleciona métodos. Advice = @Before, @AfterReturning, @AfterThrowing, @After (finally) ou @Around (o mais poderoso — envolve o método com proceed()).

pom.xmlxml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Exemplo completo: auditoria REST com anotação customizada

O padrão “anotação + aspect” concentra o cross-cutting em um só lugar: você marca endpoints sensíveis com @AuditarRest e o aspect registra entrada, tempo, sucesso ou falha — sem poluir o controller com try/catch de log.

AuditarRest.javajava
package br.com.dfcode.eduspring.aop;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditarRest {
    /** Nome lógico da operação (aparece nos logs) */
    String operacao();
}
RestAuditoriaAspect.java — Around + AfterReturning + AfterThrowingjava
package br.com.dfcode.eduspring.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import java.util.UUID;

@Aspect
@Component
public class RestAuditoriaAspect {

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

    // Pointcut nomeado: qualquer método público em controllers do pacote
    @Pointcut("execution(public * br.com.dfcode.eduspring.controller..*(..))")
    public void controllersEduSpring() {}

    // Combina pacote + presença da anotação no método
    @Pointcut("controllersEduSpring() && @annotation(auditar)")
    public void metodosAuditados(AuditarRest auditar) {}

    @Around("metodosAuditados(auditar)")
    public Object auditarTempo(ProceedingJoinPoint pjp, AuditarRest auditar) throws Throwable {
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        StopWatch sw = new StopWatch();
        sw.start();
        try {
            log.info("[REST] Início operacao={} metodo={}", auditar.operacao(), pjp.getSignature().toShortString());
            return pjp.proceed();
        } finally {
            sw.stop();
            log.info("[REST] Fim operacao={} tempoMs={}", auditar.operacao(), sw.getTotalTimeMillis());
            MDC.remove("traceId");
        }
    }

    // Só executa se o método retornou sem exceção — útil para registrar sucesso com retorno
    @AfterReturning(pointcut = "metodosAuditados(auditar)", returning = "retorno", argNames = "auditar,retorno")
    public void aposSucesso(AuditarRest auditar, Object retorno) {
        log.debug("[REST] Sucesso operacao={} tipoRetorno={}", auditar.operacao(),
            retorno != null ? retorno.getClass().getSimpleName() : "void");
    }

    @AfterThrowing(pointcut = "metodosAuditados(auditar)", throwing = "ex", argNames = "auditar,ex")
    public void aposErro(AuditarRest auditar, Throwable ex) {
        log.warn("[REST] Falha operacao={} excecao={}", auditar.operacao(), ex.getClass().getSimpleName(), ex);
    }
}
CursoController.java — uso da anotaçãojava
@RestController
@RequestMapping("/cursos")
public class CursoController {

    private final CursoService cursoService;

    @AuditarRest(operacao = "LISTAR_CURSOS")
    @GetMapping
    public List<CursoResponse> listar() {
        return cursoService.listarTodos();
    }

    @AuditarRest(operacao = "CRIAR_CURSO")
    @PostMapping
    public ResponseEntity<CursoResponse> criar(@Valid @RequestBody CriarCursoRequest req) {
        return ResponseEntity.status(HttpStatus.CREATED).body(cursoService.criar(req));
    }
}

Aspect só em services (sem anotação)

ServiceMonitoringAspect.java — alerta de lentidãojava
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ServiceMonitoringAspect {

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

    @Around("execution(* br.com.dfcode.eduspring.service..*.*(..))")
    public Object alertarLentidao(ProceedingJoinPoint pjp) throws Throwable {
        long inicio = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            long ms = System.currentTimeMillis() - inicio;
            if (ms > 500) {
                log.warn("LENTO {} demorou {} ms", pjp.getSignature().toShortString(), ms);
            }
        }
    }
}
EduspringApplication.javajava
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true) // CGLIB: necessário quando o alvo não tem interface
public class EduspringApplication { ... }
Diagrama 15.1 — Chamada ao controller passando pelo proxy e pelo aspect
sequenceDiagram participant C as Cliente HTTP participant P as Proxy CGLIB participant A as RestAuditoriaAspect participant Ctrl as CursoController C->>P: GET /cursos P->>A: @Around (início / MDC) A->>Ctrl: proceed() → listar() Ctrl-->>A: List A-->>P: @AfterReturning + fim Around P-->>C: 200 JSON
⚠ Limitações do Spring AOP

Só intercepta chamadas externas ao bean (via proxy). Chamadas internas this.metodo() não passam pelo aspect. proxyTargetClass = true força CGLIB (útil para classes sem interface). Para pointcuts em private ou weaving estático, use AspectJ completo.

15
Exercício — Métrica por anotação

Objetivo

Criar anotação @MedirTempo e aspect @Around que incremente um contador (Micrometer Counter ou simples AtomicLong) por nome de operação.

Tarefa

  • Anotar dois métodos de controllers diferentes e validar nos logs ou no /actuator/metrics.
  • Adicionar @AfterThrowing que incremente um contador de erros separado.
✓ O que aprendemos
  • @Pointcut reutiliza expressões; combine com &&, ||, !.
  • @Around controla antes/depois e deve chamar proceed() para executar o método real.
  • @AfterReturning / @AfterThrowing recebem returning / throwing para logs e auditoria.
  • Anotações customizadas + @annotation(...) deixam o cross-cutting explícito e seletivo.
  • MDC/traceId em aspectos facilita correlacionar logs da mesma requisição.
Parte 5 — Spring Security
Capítulo 18

Segurança 101 com Spring Security

Adicionar spring-boot-starter-security ao projeto imediatamente protege todos os endpoints. Neste capítulo entendemos a arquitetura de filtros e configuramos a segurança corretamente para APIs REST.

A surpresa do Spring Security

Adicione a dependência e tente acessar qualquer endpoint: você receberá um HTTP 401 Unauthorized. O Spring Security protege tudo por padrão — um comportamento seguro que força você a declarar explicitamente o que é público.

A Cadeia de Filtros (Filter Chain)

O Spring Security funciona como uma série de filtros que interceptam cada requisição HTTP antes de chegar ao controller. Cada filtro tem uma responsabilidade específica:

Diagrama 16.1 — Requisição passando pela Filter Chain do Spring Security
sequenceDiagram participant C as Cliente participant CSF as CorsFilter participant SF as SecurityContextFilter participant JF as JwtAuthFilter participant EAP as ExceptionTranslationFilter participant FA as FilterSecurityInterceptor participant Ctrl as Controller C->>CSF: POST /cursos + Bearer token CSF->>SF: passa para frente SF->>JF: extrai e valida o JWT JF->>SF: seta Authentication no contexto SF->>EAP: passa para frente EAP->>FA: verifica autorização FA->>Ctrl: usuário autorizado — executa Ctrl-->>C: 201 Created Note over JF,EAP: Se token inválido: JF-->>C: 401 Unauthorized (JSON) Note over FA,Ctrl: Se sem permissão: EAP-->>C: 403 Forbidden (JSON)

SecurityFilterChain para APIs REST

SecurityConfig.javajava
package br.com.dfcode.eduspring.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // APIs REST são stateless — não usamos sessão HTTP
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

            // Desabilita CSRF: necessário para APIs stateless com JWT
            // (CSRF protege sessões com cookies — não se aplica a Bearer tokens)
            .csrf(csrf -> csrf.disable())

            // Define quem pode acessar o quê
            .authorizeHttpRequests(auth -> auth
                // Endpoints públicos: documentação e autenticação
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                .requestMatchers(HttpMethod.POST, "/auth/login", "/auth/registrar").permitAll()

                // Leitura pública de cursos
                .requestMatchers(HttpMethod.GET, "/cursos/**").permitAll()

                // Todo o resto exige autenticação
                .anyRequest().authenticated()
            )

            // Retorna JSON em vez de redirect para login page
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) -> {
                    res.setContentType("application/json");
                    res.setStatus(401);
                    res.getWriter().write("{\"erro\":\"Token ausente ou inválido\"}");
                })
                .accessDeniedHandler((req, res, e) -> {
                    res.setContentType("application/json");
                    res.setStatus(403);
                    res.getWriter().write("{\"erro\":\"Acesso negado\"}");
                })
            );

        return http.build();
    }
}
16
Exercício — Primeira configuração de segurança

Objetivo

Adicionar Spring Security e verificar o comportamento dos endpoints.

Tarefa

  • Adicionar spring-boot-starter-security ao projeto.
  • Verificar que todos os endpoints retornam 401 sem configuração.
  • Criar SecurityConfig liberando GET /cursos/** e /swagger-ui/**.
  • Verificar que o Swagger ainda funciona mas POST /cursos retorna 401 em JSON.

Critério de aceite

GET /cursos retorna 200; POST /cursos retorna {"erro":"Token ausente ou inválido"} com status 401.

✓ O que aprendemos
  • O Spring Security protege todos os endpoints por padrão ao ser adicionado.
  • A Filter Chain intercepta requisições antes do controller em múltiplas etapas.
  • SessionCreationPolicy.STATELESS + CSRF desabilitado é o padrão correto para APIs REST com JWT.
  • AuthenticationEntryPoint e AccessDeniedHandler customizados retornam JSON em vez de páginas HTML.
Parte 5 — Spring Security
Capítulo 19

Gerenciando Usuários e Senhas

Nunca armazene senhas em texto puro. Neste capítulo implementamos o gerenciamento de usuários integrado ao banco de dados, com hash seguro de senhas usando BCrypt.

Por que nunca salvar senha em texto puro?

Se seu banco for comprometido e as senhas estiverem em texto puro, o atacante tem acesso imediato a todas as contas — inclusive em outros serviços onde o usuário usa a mesma senha. O hash torna esse ataque computacionalmente inviável.

PasswordEncoder: BCrypt

SecurityConfig.java (adicionando BCrypt)java
@Bean
public PasswordEncoder passwordEncoder() {
    // BCrypt gera um salt aleatório a cada hash — mesma senha = hash diferente
    // Fator 12: ~250ms por hash — lento o suficiente para dificultar força bruta
    return new BCryptPasswordEncoder(12);
}

// Demonstração:
// encoder.encode("minhasenha") → "$2a$12$xyz..." (hash diferente a cada chamada)
// encoder.matches("minhasenha", hash) → true  (verifica sem saber a senha original)

Entidade Usuario implementando UserDetails

Usuario.javajava
@Entity
@Table(name = "usuarios")
@Getter @Setter @NoArgsConstructor
public class Usuario implements UserDetails {  // contrato do Spring Security

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

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String senha;  // armazenado como hash BCrypt

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    // UserDetails: retorna as authorities (roles) do usuário
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }

    @Override public String getPassword() { return senha; }
    @Override public String getUsername() { return email; } // email como username
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

// Enum de roles
public enum Role { ADMIN, PROFESSOR, ALUNO }

UsuarioRepository e UserDetailsService

UsuarioRepository.java + UsuarioDetailsServiceImpl.javajava
// Repository
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
    Optional<Usuario> findByEmail(String email);
    boolean existsByEmail(String email);
}

// UserDetailsService: Spring Security chama loadUserByUsername ao autenticar
@Service
public class UsuarioDetailsServiceImpl implements UserDetailsService {

    private final UsuarioRepository usuarioRepository;

    public UsuarioDetailsServiceImpl(UsuarioRepository usuarioRepository) {
        this.usuarioRepository = usuarioRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return usuarioRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("Usuário não encontrado: " + email));
    }
}

// Service de registro
@Service
public class AuthService {
    private final UsuarioRepository usuarioRepository;
    private final PasswordEncoder passwordEncoder;

    public void registrar(RegistrarRequest request) {
        if (usuarioRepository.existsByEmail(request.email())) {
            throw new RegraVioladaException("E-mail já cadastrado");
        }
        var usuario = new Usuario();
        usuario.setEmail(request.email());
        // SEMPRE hash antes de salvar — nunca salve a senha original
        usuario.setSenha(passwordEncoder.encode(request.senha()));
        usuario.setRole(Role.ALUNO);  // role padrão para novos usuários
        usuarioRepository.save(usuario);
    }
}
Diagrama 17.1 — Fluxo de autenticação com DaoAuthenticationProvider
sequenceDiagram participant C as Cliente participant AC as AuthController participant AM as AuthenticationManager participant DAP as DaoAuthenticationProvider participant UDS as UserDetailsService participant DB as Banco de Dados C->>AC: POST /auth/login {"email":"a@b.com","senha":"123"} AC->>AM: authenticate(UsernamePasswordToken) AM->>DAP: delega autenticação DAP->>UDS: loadUserByUsername("a@b.com") UDS->>DB: SELECT * FROM usuarios WHERE email = ? DB-->>UDS: Usuario{senha: "$2a$12$hash..."} UDS-->>DAP: UserDetails DAP->>DAP: BCrypt.matches("123", "$2a$12$hash...") DAP-->>AM: Authentication OK AM-->>AC: usuário autenticado AC-->>C: 200 OK + JWT token
17
Exercício — Endpoint de registro

Objetivo

Implementar o endpoint de cadastro de usuários.

Tarefa

  • Criar POST /auth/registrar que recebe email e senha.
  • Validar que o e-mail não está em uso (existsByEmail).
  • Salvar com senha hasheada pelo BCryptPasswordEncoder.
  • Retornar 201 com o e-mail cadastrado (nunca retorne a senha).

Critério de aceite

Registro com e-mail novo → 201. Registro com e-mail existente → 422 com mensagem de erro. Verificar no banco que a senha está hasheada (começa com $2a$).

✓ O que aprendemos
  • Senhas devem ser sempre hashadas com BCrypt antes de armazenar.
  • UserDetails é o contrato que o Spring Security usa para representar um usuário autenticado.
  • UserDetailsService.loadUserByUsername é chamado pelo Spring Security durante a autenticação.
  • O DaoAuthenticationProvider conecta o Spring Security ao seu banco de dados automaticamente.
Parte 5 — Spring Security
Capítulo 20

Controle de Acesso por Roles

Autenticação verifica quem você é. Autorização decide o que você pode fazer. O Spring Security oferece duas formas complementares de controle de acesso: por URL e por método.

Protegendo URLs com requestMatchers

SecurityConfig.java (controle por URL)java
.authorizeHttpRequests(auth -> auth
    // Público: documentação e autenticação
    .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
    .requestMatchers(HttpMethod.POST, "/auth/**").permitAll()

    // Somente ADMIN pode criar, editar e remover cursos
    .requestMatchers(HttpMethod.POST,   "/cursos/**").hasRole("ADMIN")
    .requestMatchers(HttpMethod.PUT,    "/cursos/**").hasRole("ADMIN")
    .requestMatchers(HttpMethod.DELETE, "/cursos/**").hasRole("ADMIN")

    // ADMIN e PROFESSOR podem gerenciar matrículas
    .requestMatchers("/matriculas/**").hasAnyRole("ADMIN", "PROFESSOR")

    // Qualquer autenticado pode ler
    .requestMatchers(HttpMethod.GET, "/cursos/**").authenticated()

    // Tudo mais: autenticado
    .anyRequest().authenticated()
)

@PreAuthorize — Controle no método

MatriculaController.javajava
// Habilitar no SecurityConfig (ou na classe principal):
// @EnableMethodSecurity

@RestController
@RequestMapping("/matriculas")
public class MatriculaController {

    // Apenas ADMIN pode listar todas as matrículas
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping
    public List<MatriculaResponse> listarTodas() { ... }

    // ALUNO só pode ver suas próprias matrículas
    // #authentication é o objeto de autenticação atual
    // principal.username é o e-mail do usuário logado
    @PreAuthorize("hasRole('ADMIN') or #email == authentication.principal.username")
    @GetMapping("/aluno/{email}")
    public List<MatriculaResponse> listarDoAluno(@PathVariable String email) { ... }

    // ADMIN ou PROFESSOR podem criar matrículas
    @PreAuthorize("hasAnyRole('ADMIN', 'PROFESSOR')")
    @PostMapping
    public ResponseEntity<MatriculaResponse> criar(@Valid @RequestBody CriarMatriculaRequest req) { ... }
}
Diagrama 18.1 — Árvore de decisão de autorização
flowchart TD A["Requisição chega"] --> B{"Token JWT presente?"} B -- Não --> C["401 Unauthorized"] B -- Sim --> D{"Token válido?"} D -- Não --> C D -- Sim --> E{"URL pública?"} E -- Sim --> F["Permite acesso"] E -- Não --> G{"Usuário tem a role exigida?"} G -- Não --> H["403 Forbidden"] G -- Sim --> F
18
Exercício — Proteção por roles no EduSpring

Objetivo

Implementar controle de acesso completo para os endpoints de cursos e matrículas.

Tarefa

  • Configurar regras: ADMIN pode tudo; PROFESSOR pode criar matrículas; ALUNO pode apenas ler cursos e suas próprias matrículas.
  • Usar @PreAuthorize nos controllers para validação por método.
  • Testar com três tokens de roles diferentes (criar via Postman).

Critério de aceite

  • Token ALUNO em DELETE /cursos/1 → 403 Forbidden.
  • Token ADMIN em DELETE /cursos/1 → 204 No Content.
  • Token ALUNO em GET /matriculas/aluno/{email-proprio} → 200 OK.
✓ O que aprendemos
  • requestMatchers define regras de acesso por padrão de URL e método HTTP.
  • @PreAuthorize oferece controle granular por método, com suporte a SpEL.
  • A combinação de ambos (URL + método) cobre todos os cenários de controle de acesso.
  • CSRF deve ser desabilitado para APIs stateless com Bearer tokens.
Parte 5 — Spring Security
Capítulo 21

Autenticação com JWT

JWT (JSON Web Token) é o padrão de mercado para autenticação stateless em APIs REST. O cliente recebe um token no login e o envia em todas as requisições seguintes — sem sessão no servidor.

Estrutura do JWT

Um JWT é uma string com três partes separadas por ponto: header.payload.signature. Cada parte é um Base64 URL-encoded JSON:

Anatomia de um JWTjson
// HEADER: algoritmo de assinatura
{ "alg": "HS256", "typ": "JWT" }

// PAYLOAD: dados do usuário (claims)
{
  "sub": "usuario@email.com",
  "role": "ADMIN",
  "iat": 1704067200,   // issued at (emitido em)
  "exp": 1704153600    // expiration (expira em — 24h depois)
}

// SIGNATURE: HMACSHA256(base64(header) + "." + base64(payload), SECRET_KEY)
// Garante que o token não foi adulterado
⚠ O payload do JWT não é criptografado

Qualquer pessoa pode decodificar o payload (é apenas Base64). Nunca coloque senhas, dados sensíveis ou informações privadas no payload. A assinatura garante integridade (que não foi adulterado), não confidencialidade.

Implementação completa do JWT

pom.xml — dependência JJWTxml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
JwtService.javajava
@Service
public class JwtService {

    // Chave secreta — NUNCA no código-fonte; use variável de ambiente
    @Value("${app.jwt.secret}")
    private String secret;

    @Value("${app.jwt.expiracao-horas:24}")
    private Long expiracaoHoras;

    // Gera um token JWT para o usuário
    public String gerarToken(UserDetails usuario) {
        return Jwts.builder()
            .subject(usuario.getUsername())          // email como subject
            .claim("role", extrairRole(usuario))     // adiciona a role como claim
            .issuedAt(new Date())                    // momento de criação
            .expiration(calcularExpiracao())         // momento de expiração
            .signWith(chaveSecreta())                // assina com HMAC-SHA256
            .compact();
    }

    // Extrai o username (email) do token
    public String extrairUsername(String token) {
        return extrairClaim(token, Claims::getSubject);
    }

    // Verifica se o token é válido para o usuário
    public boolean isTokenValido(String token, UserDetails usuario) {
        String username = extrairUsername(token);
        return username.equals(usuario.getUsername()) && !isTokenExpirado(token);
    }

    private boolean isTokenExpirado(String token) {
        return extrairClaim(token, Claims::getExpiration).before(new Date());
    }

    private <T> T extrairClaim(String token, Function<Claims, T> resolver) {
        Claims claims = Jwts.parser()
            .verifyWith(chaveSecreta())
            .build()
            .parseSignedClaims(token)
            .getPayload();
        return resolver.apply(claims);
    }

    private SecretKey chaveSecreta() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    private Date calcularExpiracao() {
        long agora = System.currentTimeMillis();
        return new Date(agora + expiracaoHoras * 3600 * 1000);
    }

    private String extrairRole(UserDetails usuario) {
        return usuario.getAuthorities().stream()
            .findFirst()
            .map(GrantedAuthority::getAuthority)
            .orElse("");
    }
}
JwtAuthFilter.javajava
@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    // ... construtor

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        // Se não tem header Authorization ou não começa com "Bearer ", pula
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authHeader.substring(7); // remove "Bearer "
        String username = jwtService.extrairUsername(token);

        // Só processa se tem username e contexto ainda não autenticado
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails usuario = userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValido(token, usuario)) {
                // Cria o objeto de autenticação e seta no contexto
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(usuario, null, usuario.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }

        filterChain.doFilter(request, response);  // continua a cadeia
    }
}
Diagrama 19.1 — Fluxo completo de autenticação e uso do JWT
sequenceDiagram participant C as Cliente participant AC as AuthController participant AS as AuthService participant JS as JwtService participant JF as JwtAuthFilter participant Ctrl as CursoController Note over C,Ctrl: Fase 1: Login C->>AC: POST /auth/login {"email":"a@b.com","senha":"123"} AC->>AS: autenticar(email, senha) AS-->>AC: Usuario autenticado AC->>JS: gerarToken(usuario) JS-->>AC: "eyJhbGc..." AC-->>C: 200 OK {"token":"eyJhbGc...","expira":"24h"} Note over C,Ctrl: Fase 2: Requisição autenticada C->>JF: POST /cursos + Authorization: Bearer eyJhbGc... JF->>JS: extrairUsername(token) JS-->>JF: "a@b.com" JF->>JS: isTokenValido(token, usuario) JS-->>JF: true JF->>JF: seta Authentication no SecurityContext JF->>Ctrl: passa a requisição adiante Ctrl-->>C: 201 Created
19
Exercício — Endpoint de login com JWT

Objetivo

Implementar o fluxo completo de autenticação JWT.

Tarefa

  • Criar POST /auth/login que recebe email/senha e retorna o JWT.
  • Registrar o JwtAuthFilter antes do UsernamePasswordAuthenticationFilter no SecurityConfig.
  • Testar: login → copiar token → usar em Authorization: Bearer {token} no Postman.

Critério de aceite

  • Login com credenciais válidas → 200 com token JWT.
  • Token usado em POST /cursos com role ADMIN → 201 Created.
  • Token expirado ou inválido → 401 Unauthorized.
✓ O que aprendemos
  • JWT tem três partes: header, payload e signature. O payload é legível mas não criptografado.
  • JwtService encapsula a geração e validação de tokens.
  • JwtAuthFilter intercepta requisições, extrai o token do header e autentica o usuário no contexto Spring Security.
  • A chave secreta deve ser longa, aleatória e armazenada em variável de ambiente — nunca no código.
Parte 6 — Tópicos Avançados
Capítulo 22

Testes Automatizados

Código sem testes é código frágil. Testes automatizados garantem que sua aplicação funciona conforme esperado e permitem refatorar com confiança. O Spring Boot oferece um ecossistema de testes completo e bem integrado.

A Pirâmide de Testes

Diagrama 20.1 — Pirâmide de testes do EduSpring
graph TD E2E["Testes E2E
Poucos, lentos, caros
Postman Collection, REST Assured"] INT["Testes de Integração
@SpringBootTest, Testcontainers
Banco real, contexto completo"] UNIT["Testes Unitários
Muitos, rápidos, baratos
JUnit 5 + Mockito, sem Spring"] E2E --- INT INT --- UNIT

Testes Unitários com JUnit 5 e Mockito

MatriculaServiceTest.javajava
@ExtendWith(MockitoExtension.class)  // ativa Mockito — sem Spring Context
class MatriculaServiceTest {

    @Mock  // cria um mock (objeto falso) do repositório
    private MatriculaRepository matriculaRepository;

    @Mock
    private CursoRepository cursoRepository;

    @InjectMocks  // injeta os mocks no service
    private MatriculaService matriculaService;

    @Test
    @DisplayName("Deve lançar exceção ao tentar matricular em curso sem vagas")
    void deveRejeitarMatriculaEmCursoSemVagas() {
        // GIVEN: cenário inicial
        Curso cursoSemVagas = Curso.builder()
            .id(1L).nome("Java Avançado").vagas(0).ativo(true).build();

        // Configura o mock para retornar o curso quando chamado
        given(cursoRepository.findById(1L)).willReturn(Optional.of(cursoSemVagas));

        // WHEN + THEN: executa e verifica a exceção
        assertThatThrownBy(() -> matriculaService.matricular(10L, 1L))
            .isInstanceOf(RegraVioladaException.class)
            .hasMessageContaining("sem vagas");

        // Verifica que o repositório de matrículas NÃO foi chamado
        verify(matriculaRepository, never()).save(any());
    }

    @Test
    @DisplayName("Deve criar matrícula com sucesso quando há vagas")
    void deveCriarMatriculaComSucesso() {
        // GIVEN
        Aluno aluno = Aluno.builder().id(10L).nome("João").ativo(true).build();
        Curso curso  = Curso.builder().id(1L).nome("Java").vagas(30).ativo(true).build();

        given(cursoRepository.findById(1L)).willReturn(Optional.of(curso));
        given(matriculaRepository.existsByAlunoIdAndCursoId(10L, 1L)).willReturn(false);
        given(matriculaRepository.save(any())).willAnswer(inv -> inv.getArgument(0));

        // WHEN
        matriculaService.matricular(10L, 1L);

        // THEN: verifica que save foi chamado exatamente uma vez
        verify(matriculaRepository, times(1)).save(any(Matricula.class));
    }
}

Testando Controllers com MockMvc

CursoControllerTest.javajava
// @WebMvcTest carrega apenas a camada web (controllers, filters, advice)
// É muito mais rápido que @SpringBootTest
@WebMvcTest(CursoController.class)
class CursoControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    // @MockitoBean substitui o bean real por um mock no contexto Spring (Boot 3.4+)
    @MockitoBean
    private CursoService cursoService;

    @Test
    @DisplayName("GET /cursos deve retornar 200 com lista de cursos")
    void deveListarCursos() throws Exception {
        // GIVEN
        List<CursoResponse> cursos = List.of(
            new CursoResponse(1L, "Java Básico", "...", 40, null, true)
        );
        given(cursoService.listarTodos()).willReturn(cursos);

        // WHEN + THEN
        mockMvc.perform(get("/cursos")
                .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(1)))
            .andExpect(jsonPath("$[0].nome").value("Java Básico"));
    }

    @Test
    @DisplayName("POST /cursos com dados inválidos deve retornar 422")
    void deveRetornarErroValidacao() throws Exception {
        // Request com nome vazio
        String json = """{"nome": "", "cargaHoraria": 0}""";

        mockMvc.perform(post("/cursos")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isUnprocessableEntity())
            .andExpect(jsonPath("$.erros").isArray());
    }
}

Testes de Repositório com @DataJpaTest

CursoRepositoryTest.javajava
// @DataJpaTest configura apenas a camada JPA com banco H2 em memória
// Muito mais rápido que @SpringBootTest
@DataJpaTest
class CursoRepositoryTest {

    @Autowired
    private CursoRepository cursoRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    @DisplayName("Deve encontrar cursos ativos pelo nome (case insensitive)")
    void deveBuscarPorNomeIgnorandoCase() {
        // GIVEN: persiste dados de teste
        entityManager.persist(Curso.builder().nome("Java Básico").ativo(true).build());
        entityManager.persist(Curso.builder().nome("JAVA Avançado").ativo(true).build());
        entityManager.persist(Curso.builder().nome("Python").ativo(true).build());
        entityManager.flush();

        // WHEN
        List<Curso> resultado = cursoRepository.findByNomeContainingIgnoreCase("java");

        // THEN
        assertThat(resultado).hasSize(2);
        assertThat(resultado).extracting(Curso::getNome)
            .containsExactlyInAnyOrder("Java Básico", "JAVA Avançado");
    }
}
20
Exercício — Cobertura de testes

Objetivo

Atingir 80% de cobertura no MatriculaService e CursoController.

Tarefa

  • Escrever testes unitários para todos os métodos do MatriculaService (cenários de sucesso e erro).
  • Escrever testes de controller com MockMvc para todos os endpoints de CursoController.
  • Adicionar o plugin JaCoCo no pom.xml e rodar ./mvnw verify.

Critério de aceite

Relatório JaCoCo em target/site/jacoco/index.html mostra cobertura ≥ 80% nas classes-alvo.

✓ O que aprendemos
  • Testes unitários com Mockito são rápidos e não precisam do Spring Context.
  • @WebMvcTest testa controllers de forma isolada, sem banco de dados.
  • @DataJpaTest testa repositórios com banco H2 sem inicializar a aplicação toda.
  • MockMvc permite testar endpoints HTTP com assertions em status, headers e body JSON.
Parte 6 — Tópicos Avançados
Capítulo 23

Funcionalidades do Mundo Real

Toda aplicação real precisa de upload de arquivos, envio de e-mails e tarefas agendadas. Aqui fazemos um primeiro passe com exemplos mínimos que funcionam no EduSpring. Para aprofundar logging (SLF4J/Logback, Loggly, profiles), agendamento e e-mail (SMTP, Thymeleaf, SendGrid), siga os capítulos 28 a 30 (Parte 8).

Continuação

Cap. 28 — Logging · Cap. 29@Scheduled em detalhe · Cap. 30 — E-mail na aplicação (referências oficiais e tutoriais).

Upload de Arquivos com MultipartFile

AlunoController.java (upload de foto)java
@PostMapping("/{id}/foto")
public ResponseEntity<String> uploadFoto(
        @PathVariable Long id,
        @RequestParam("arquivo") MultipartFile arquivo) {

    // Validações de segurança
    if (arquivo.isEmpty()) {
        throw new RegraVioladaException("Arquivo não pode ser vazio");
    }

    String tipoConteudo = arquivo.getContentType();
    if (!List.of("image/jpeg", "image/png", "image/webp").contains(tipoConteudo)) {
        throw new RegraVioladaException("Apenas imagens JPEG, PNG e WebP são aceitas");
    }

    if (arquivo.getSize() > 5 * 1024 * 1024) {  // 5MB
        throw new RegraVioladaException("Tamanho máximo: 5MB");
    }

    String urlFoto = uploadService.salvar(id, arquivo);
    return ResponseEntity.ok(urlFoto);
}

// application.yml
// spring.servlet.multipart.max-file-size=5MB
// spring.servlet.multipart.max-request-size=10MB

Enviando E-mails com JavaMailSender

EmailService.javajava
@Service
@Profile("prod")  // versão real só em produção
public class EmailServiceSmtp implements EmailService {

    private final JavaMailSender mailSender;

    public void enviarConfirmacaoMatricula(String destinatario, String nomeCurso) {
        SimpleMailMessage mensagem = new SimpleMailMessage();
        mensagem.setFrom("noreply@dfcode.com.br");
        mensagem.setTo(destinatario);
        mensagem.setSubject("Matrícula confirmada: " + nomeCurso);
        mensagem.setText("""
            Olá!

            Sua matrícula no curso "%s" foi confirmada com sucesso.
            Acesse a plataforma para começar seus estudos.

            Equipe EduSpring
            """.formatted(nomeCurso));

        mailSender.send(mensagem);
    }
}

// application-prod.yml
// spring.mail.host: smtp.gmail.com
// spring.mail.port: 587
// spring.mail.username: ${MAIL_USER}
// spring.mail.password: ${MAIL_PASS}
// spring.mail.properties.mail.smtp.auth: true
// spring.mail.properties.mail.smtp.starttls.enable: true

Tarefas Agendadas com @Scheduled

RelatorioScheduler.javajava
@Component
// @EnableScheduling deve estar na classe principal ou em uma @Configuration
public class RelatorioScheduler {

    private final MatriculaRepository matriculaRepository;
    private final EmailService emailService;

    // Executa todo dia às 8h (cron: segundo minuto hora diaDoMes mês diaDaSemana)
    @Scheduled(cron = "0 0 8 * * *")
    public void enviarRelatorioMatriculasDiario() {
        LocalDate ontem = LocalDate.now().minusDays(1);
        List<Matricula> novasMatriculas = matriculaRepository.findByDataMatricula(ontem);

        if (!novasMatriculas.isEmpty()) {
            log.info("Enviando relatório: {} novas matrículas ontem", novasMatriculas.size());
            emailService.enviarRelatorioProfessores(novasMatriculas);
        }
    }

    // Executa a cada 30 minutos
    @Scheduled(fixedRate = 30 * 60 * 1000)
    public void limparTokensExpirados() {
        int removidos = tokenRepository.deleteByExpiracaoAntesDe(LocalDateTime.now());
        log.debug("Tokens expirados removidos: {}", removidos);
    }
}
Diagrama 21.1 — Envio de e-mail assíncrono após matrícula
sequenceDiagram participant C as Cliente participant MC as MatriculaController participant MS as MatriculaService participant DB as Banco participant ES as EmailService C->>MC: POST /matriculas {"alunoId":1,"cursoId":2} MC->>MS: matricular(1, 2) MS->>DB: salva Matricula DB-->>MS: Matricula salva MS->>ES: enviarConfirmacao (assíncrono @Async) Note over ES: executa em thread separada MS-->>MC: MatriculaResponse MC-->>C: 201 Created (imediato!) ES->>ES: prepara e envia e-mail SMTP Note over ES: usuário não precisa esperar
22
Exercício — E-mail assíncrono de boas-vindas

Objetivo

Enviar e-mail de boas-vindas ao novo aluno de forma assíncrona.

Tarefa

  • Anotar o método enviarConfirmacao com @Async.
  • Habilitar @EnableAsync na aplicação.
  • No profile dev, usar EmailServiceFake que loga o e-mail em vez de enviar.
  • Verificar que o endpoint de matrícula responde imediatamente (sem esperar o envio do e-mail).

Critério de aceite

POST /matriculas responde em menos de 100ms; o log do e-mail fake aparece alguns milissegundos depois no console.

✓ O que aprendemos
  • MultipartFile recebe arquivos enviados via form-data; valide tipo e tamanho antes de processar.
  • JavaMailSender integra com qualquer servidor SMTP com configuração simples no application.yml.
  • @Scheduled agenda tarefas com cron ou intervalo fixo — ideal para relatórios e limpeza de dados.
  • @Async executa métodos em threads separadas, desacoplando operações lentas do fluxo principal.
Parte 6 — Tópicos Avançados
Capítulo 24

Comunicação entre Serviços

Quando o monólito vira vários deploys, o serviço-matriculas precisa chamar o serviço-cursos pela rede. O ecossistema Spring oferece vários clientes HTTP: o clássico RestTemplate (ainda omnipresente em bases legadas), o RestClient (recomendado no Boot 3.2+), o WebClient (reativo) e o OpenFeign (declarativo, com Spring Cloud). A escolha impacta testes, timeouts e como você modela falhas — tema do capítulo seguinte (Resilience4j).

RestTemplate — API clássica (modo manutenção)

RestTemplate foi, por anos, o cliente síncrono padrão. Hoje está em modo manutenção: não recebe recursos novos; o time do Spring orienta projetos novos a usar RestClient. Mesmo assim, você encontrará dezenas de exemplos e serviços em produção que ainda o utilizam — saber ler e testar esse código é obrigatório.

CursoClientRestTemplate.javajava
@Service
public class CursoClientRestTemplate {

    private final RestTemplate restTemplate;

    /** RestTemplateBuilder aplica timeouts e message converters do Spring Boot. */
    public CursoClientRestTemplate(RestTemplateBuilder builder,
                                   @Value("${servicos.cursos.url}") String baseUrl) {
        this.restTemplate = builder
            .rootUri(baseUrl)
            .setConnectTimeout(Duration.ofSeconds(2))
            .setReadTimeout(Duration.ofSeconds(3))
            .build();
    }

    public CursoResponse buscarCurso(Long id) {
        try {
            return restTemplate.getForObject("/cursos/{id}", CursoResponse.class, id);
        } catch (HttpClientErrorException.NotFound ex) {
            throw new RecursoNaoEncontradoException("Curso", id);
        }
    }

    public CursoResponse criarCurso(CriarCursoRequest request) {
        return restTemplate.postForObject("/cursos", request, CursoResponse.class);
    }
}

Para erros genéricos ou corpo de erro customizado, use restTemplate.exchange(...) com ParameterizedTypeReference. Em código novo, prefira RestClient — a API é mais segura em tipos e simétrica ao modelo de retrieve().

RestClient — O cliente moderno (Spring Boot 3.2+)

CursoClientService.java (RestClient)java
@Service
public class CursoClientService {

    private final RestClient restClient;

    public CursoClientService(RestClient.Builder builder,
                               @Value("${servicos.cursos.url}") String cursoServiceUrl) {
        this.restClient = builder
            .baseUrl(cursoServiceUrl)
            .defaultHeader("Content-Type", "application/json")
            .build();
    }

    // Busca um curso pelo ID no serviço externo
    public CursoResponse buscarCurso(Long id) {
        return restClient.get()
            .uri("/cursos/{id}", id)
            .retrieve()
            .onStatus(status -> status.value() == 404,
                (req, res) -> { throw new RecursoNaoEncontradoException("Curso", id); })
            .body(CursoResponse.class);
    }

    // POST para criar matrícula no serviço externo
    public MatriculaResponse criarMatricula(CriarMatriculaRequest request) {
        return restClient.post()
            .uri("/matriculas")
            .body(request)
            .retrieve()
            .body(MatriculaResponse.class);
    }
}

Timeouts (connect / read)

Sem limite de tempo, uma chamada a um serviço lento segura a thread do Tomcat por minutos. No Spring Boot 3.2+ você pode padronizar no YAML (vale para RestClient criado pelo builder auto-configurado):

application.ymlyaml
spring:
  http:
    client:
      connect-timeout: 2s
      read-timeout: 5s

Com RestTemplate, use RestTemplateBuilder.setConnectTimeout / setReadTimeout como no exemplo anterior.

WebClient — Cliente reativo (Spring WebFlux)

CursoClientWebFlux.java (WebClient)java
@Service
public class CursoClientWebFlux {

    private final WebClient webClient;

    public CursoClientWebFlux(WebClient.Builder builder,
                               @Value("${servicos.cursos.url}") String url) {
        this.webClient = builder.baseUrl(url).build();
    }

    // Retorna Mono (0 ou 1 resultado) — não-bloqueante
    public Mono<CursoResponse> buscarCurso(Long id) {
        return webClient.get()
            .uri("/cursos/{id}", id)
            .retrieve()
            .bodyToMono(CursoResponse.class);
    }

    // Para usar de forma síncrona (aguarda o resultado):
    public CursoResponse buscarCursoSincrono(Long id) {
        return buscarCurso(id).block();  // bloqueia até receber a resposta
    }
}

OpenFeign — Cliente declarativo

CursoFeignClient.java (OpenFeign)java
// pom.xml: spring-cloud-starter-openfeign

// Habilitar na aplicação principal:
// @EnableFeignClients

// Com Feign, você só declara a interface — Spring gera a implementação
@FeignClient(name = "servico-cursos", url = "${servicos.cursos.url}")
public interface CursoFeignClient {

    // Mapeamento idêntico ao controller do serviço de cursos
    @GetMapping("/cursos/{id}")
    CursoResponse buscarCurso(@PathVariable Long id);

    @PostMapping("/cursos")
    CursoResponse criarCurso(@RequestBody CriarCursoRequest request);

    @GetMapping("/cursos")
    List<CursoResponse> listar(@RequestParam(defaultValue = "") String nome);
}

// Uso no Service — exatamente como um Repository local
@Service
public class MatriculaService {
    private final CursoFeignClient cursoClient;  // injetado normalmente

    public void matricular(Long alunoId, Long cursoId) {
        CursoResponse curso = cursoClient.buscarCurso(cursoId); // chamada HTTP transparente
        // ...
    }
}

Comparativo: qual usar?

CaracterísticaRestTemplateRestClientWebClientOpenFeign
StatusManutençãoRecomendado (novo)AtivoAtivo (Cloud)
APIMétodos por verbo HTTPFluente (retrieve())Reativa (Mono/Flux)Interface + anotações
Bloqueante?SimSimNão (pode block())Sim
BoilerplateMédioMédioMédioBaixo
Quando usarLegado / leituraAPIs síncronas novasStack reativaMuitos clientes declarativos
23
Exercício — ViaCEP com dois clientes

Objetivo

Integrar o EduSpring com a API ViaCEP e comparar duas APIs de cliente.

Tarefa

  • Implementar ViaCepRestClient com RestClient apontando para https://viacep.com.br e URI /ws/{cep}/json/.
  • Implementar ViaCepRestTemplate equivalente com RestTemplateBuilder (mesma URL).
  • Expor GET /enderecos/{cep} escolhendo o backend via propriedade app.cep.client=restclient|resttemplate.
  • Tratar CEP inexistente ({"erro": true}) com 404.

Critério de aceite

  • CEP válido → 200 com logradouro, bairro, cidade, estado.
  • CEP inválido → 404 com mensagem de erro.
✓ O que aprendemos
  • RestTemplate permanece em muitos projetos; configure timeouts com RestTemplateBuilder.
  • RestClient é o substituto natural para código síncrono novo no Spring Boot 3.2+.
  • spring.http.client.* define connect/read timeout global para clientes criados pelo Boot.
  • WebClient integra à stack reativa; OpenFeign reduz boilerplate quando há Spring Cloud.
  • Modele erros HTTP do serviço externo com exceções de domínio — facilita Circuit Breaker e testes.
Parte 7 — Resiliência e Mensageria
Capítulo 25

Circuit Breaker com Resilience4j

Em produção, indisponibilidade é normal: deploy, rede instável, pico de CPU no vizinho. Sem proteção, o chamador segura threads, esgota pools e vira o próximo ponto de falha. O Resilience4j implementa, no mesmo estilo declarativo do Spring, Circuit Breaker, Retry, Rate Limiter, Bulkhead e Time Limiter — neste capítulo aprofundamos o fluxo real: timeouts HTTP + métricas + fallback + simulação com Docker.

O problema da cascata de falhas

Imagine que o serviço-cursos está fora do ar ou demorando 30s por resposta. Cada thread do Tomcat no serviço-matriculas fica presa até o read timeout (capítulo 24). Com 100 requisições/s e pool de 200 threads, em poucos segundos ninguém mais atende health check — um clássico cascata. O Circuit Breaker para de tentar enquanto o sintoma persiste e devolve fallback rápido ou erro controlado.

Pré-requisito: timeout HTTP menor que “o infinito”

O CB não substitui timeout: se a chamada não tiver limite, a thread ainda trava. Garanta spring.http.client.read-timeout ou RestTemplateBuilder.setReadTimeout (cap. 23) antes de contar falhas no Resilience4j.

Os três estados do Circuit Breaker

Diagrama 24.1 — Máquina de estados do Circuit Breaker
flowchart LR CLOSED["CLOSED\n(normal)"] OPEN["OPEN\n(bloqueado)"] HALF["HALF-OPEN\n(testando)"] CLOSED -->|"taxa de falha > 50%"| OPEN OPEN -->|"waitDurationInOpenState (60s)"| HALF HALF -->|"teste bem-sucedido"| CLOSED HALF -->|"teste falhou"| OPEN

Janela deslizante: contagem vs tempo

Por padrão, slidingWindowType: COUNT_BASED usa as últimas N chamadas (slidingWindowSize). Em APIs de alto throughput, TIME_BASED avalia falhas em uma janela móvel em segundos — evita que picos antigos “congelem” o estado. Ajuste minimumNumberOfCalls para não abrir o circuito com amostra pequena demais (ex.: exige pelo menos 5 chamadas antes de calcular taxa).

Chamadas lentas podem ser tratadas como falha com slowCallDurationThreshold + slowCallRateThreshold — útil quando o serviço responde 200 mas depois de 8s, degradando o pool.

Configurando o Resilience4j

pom.xmlxml
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Opcional: expor métricas no formato Prometheus -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
application.yml — Circuit Breaker, Retry e Actuatoryaml
spring:
  http:
    client:
      connect-timeout: 2s
      read-timeout: 3s

resilience4j:
  circuitbreaker:
    instances:
      servico-cursos:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        failureRateThreshold: 50
        slowCallDurationThreshold: 2s
        slowCallRateThreshold: 50
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 5
        registerHealthIndicator: true
  retry:
    instances:
      servico-cursos:
        maxAttempts: 3
        waitDuration: 400ms
        retryExceptions:
          - org.springframework.web.client.ResourceAccessException
          - java.io.IOException

management:
  endpoints:
    web:
      exposure:
        include: health,circuitbreakers,circuitbreakerevents,metrics,prometheus
  endpoint:
    health:
      show-details: when_authorized
  metrics:
    tags:
      application: eduspring-matriculas
TimeLimiter e métodos síncronos

No Spring, a anotação @TimeLimiter do Resilience4j integra naturalmente a métodos que retornam CompletableFuture. Em método CursoResponse buscarCurso(...) síncrono, use timeout de I/O (HTTP) como mostrado acima; reserve @TimeLimiter para fluxos assíncronos ou composition reativa.

Serviço de exemplo com Docker Compose

Para reproduzir falhas de forma realista, suba dois containers na mesma rede. O matriculas-api aponta SERVICOS_CURSOS_URL=http://cursos-api:8080. Ao executar docker compose stop cursos-api, as chamadas passam a falhar com Connection refused — contabilizadas como falha no CB após os retries.

docker-compose.dev-resilience.yml (trecho)yaml
services:
  cursos-api:
    build: ./servico-cursos
    ports: ["8081:8080"]

  matriculas-api:
    build: ./servico-matriculas
    ports: ["8082:8080"]
    environment:
      SERVICOS_CURSOS_URL: http://cursos-api:8080
    depends_on: [cursos-api]

Bulkhead: limitar concorrência por dependência

Mesmo com CB fechado, um estouro de chamadas paralelas ao mesmo downstream pode saturar sockets. O Bulkhead limita quantas execuções simultâneas usam aquele cliente:

application.yml — Bulkheadyaml
resilience4j:
  bulkhead:
    instances:
      servico-cursos:
        maxConcurrentCalls: 8
        maxWaitDuration: 200ms
CursoClientService.javajava
@Service
public class CursoClientService {

    private static final Logger log = LoggerFactory.getLogger(CursoClientService.class);
    private final RestClient restClient;

    public CursoClientService(RestClient.Builder builder,
                              @Value("${servicos.cursos.url}") String baseUrl) {
        this.restClient = builder.baseUrl(baseUrl).build();
    }

    @Bulkhead(name = "servico-cursos")
    @CircuitBreaker(name = "servico-cursos", fallbackMethod = "fallbackBuscarCurso")
    @Retry(name = "servico-cursos")
    public CursoResponse buscarCurso(Long id) {
        return restClient.get()
            .uri("/cursos/{id}", id)
            .retrieve()
            .body(CursoResponse.class);
    }

    public CursoResponse fallbackBuscarCurso(Long id, Throwable ex) {
        log.warn("Cursos indisponível id={} — fallback. Causa: {}", id, ex.toString());
        return new CursoResponse(id, "Curso temporariamente indisponível",
            "Tente novamente em instantes", 0, null, false);
    }

    @Bulkhead(name = "servico-cursos")
    @CircuitBreaker(name = "servico-cursos", fallbackMethod = "fallbackListarCursos")
    public List<CursoResponse> listarCursos() {
        return restClient.get()
            .uri("/cursos")
            .retrieve()
            .body(new ParameterizedTypeReference<>() {});
    }

    public List<CursoResponse> fallbackListarCursos(Throwable ex) {
        log.warn("Listagem de cursos em fallback: {}", ex.getMessage());
        return List.of();
    }
}

Observabilidade: health, eventos e Prometheus

  • GET /actuator/circuitbreakers — estado (CLOSED/OPEN/HALF_OPEN) por instância.
  • GET /actuator/circuitbreakerevents — últimas transições (útil para post-mortem).
  • GET /actuator/metrics/resilience4j.circuitbreaker.calls — séries agregadas; com Prometheus, faça scrape em /actuator/prometheus.
Diagrama 24.2 — Circuit Breaker protegendo o serviço de matrículas
sequenceDiagram participant C as Cliente participant MS as Serviço Matrículas participant CB as Circuit Breaker participant CS as Serviço Cursos Note over C,CS: Cenário 1: Circuito FECHADO (normal) C->>MS: POST /matriculas MS->>CB: buscarCurso(1) CB->>CS: GET /cursos/1 CS-->>CB: 200 OK CB-->>MS: CursoResponse MS-->>C: 201 Created Note over C,CS: Cenário 2: Circuito ABERTO (serviço fora do ar) C->>MS: POST /matriculas MS->>CB: buscarCurso(1) Note over CB: circuito aberto — não tenta chamar CB-->>MS: fallback imediato MS-->>C: 201 Created (com dados do fallback) Note over C: resposta rápida sem esperar timeout!
24
Exercício — Falha real, métricas e recuperação

Objetivo

Reproduzir o ciclo fechado → aberto → meio-aberto com Docker e Actuator.

Tarefa

  • Subir cursos-api e matriculas-api com Compose; confirmar GET /matriculas/validar-curso/1 (ou endpoint equivalente que chame o cliente) retorna 200.
  • Registrar baseline em GET /actuator/circuitbreakers (estado CLOSED).
  • Parar só o container cursos-api; disparar 30 requisições concorrentes (ex.: hey -n 30 -c 10 ou script) e observar transição para OPEN em circuitbreakers e eventos em circuitbreakerevents.
  • Medir latência: com CB aberto, tempo médio deve cair para milissegundos (fallback), não segundos.
  • Subir novamente o cursos-api, aguardar waitDurationInOpenState e validar retorno a HALF_OPEN / CLOSED após chamadas bem-sucedidas.

Critério de aceite

Captura de tela ou log com três estados distintos + documentação do valor de failureRateThreshold e slidingWindowSize usados.

✓ O que aprendemos
  • Timeout HTTP e Circuit Breaker são complementares: um limita duração da chamada, o outro limita insistência.
  • Janela deslizante (COUNT_BASED / TIME_BASED) e minimumNumberOfCalls definem quando a taxa de falha “vale” estatisticamente.
  • @Retry em exceções de rede deve ser combinado com limites — retries infinitos só adiam o OPEN.
  • @Bulkhead protege o chamador de autopique quando o downstream ainda responde mas não aguenta concorrência.
  • Actuator + Prometheus transformam o CB em métrica operacional, não só exceção no log.
  • Fallback precisa ser seguro e barato — nunca esconder erro crítico sem métrica/alerta.
Parte 7 — Resiliência e Mensageria
Capítulo 26

Mensageria com RabbitMQ e Spring AMQP

A comunicação síncrona entre serviços cria acoplamento temporal: se o serviço de destino estiver fora do ar, a operação falha. A mensageria com RabbitMQ resolve isso com comunicação assíncrona — o produtor envia uma mensagem e continua; o consumidor processa quando estiver disponível.

Conceitos fundamentais do RabbitMQ

ConceitoAnalogiaPapel
ProducerRemetenteEnvia mensagens para o Exchange
ExchangeAgência de correiosRecebe mensagens e roteia para filas
QueueCaixa postalArmazena mensagens até o consumidor processar
ConsumerDestinatárioLê e processa mensagens da fila
BindingEndereçoRegra que conecta Exchange a uma Queue
Routing KeyCEPCritério de roteamento da mensagem
Diagrama 25.1 — Topologia RabbitMQ do EduSpring
flowchart LR P["Serviço Matrículas\n(Producer)"] EX["Exchange\neduspring.eventos\n(Topic)"] Q1["Queue\nmatriculas.confirmacao"] Q2["Queue\nmatriculas.relatorio"] DLQ["Dead Letter Queue\nmatriculas.dlq"] C1["Serviço Notificações\n(Consumer)"] C2["Serviço Relatórios\n(Consumer)"] P -->|"matricula.criada"| EX EX -->|"matricula.*"| Q1 EX -->|"matricula.*"| Q2 Q1 -->|"nack / erro"| DLQ Q1 --> C1 Q2 --> C2

Configuração com Spring AMQP

RabbitMQConfig.javajava
@Configuration
public class RabbitMQConfig {

    public static final String EXCHANGE = "eduspring.eventos";
    public static final String FILA_CONFIRMACAO = "matriculas.confirmacao";
    public static final String FILA_RELATORIO   = "matriculas.relatorio";
    public static final String FILA_DLQ         = "matriculas.dlq";
    public static final String ROUTING_KEY      = "matricula.criada";

    // Exchange do tipo Topic: roteamento por padrão de routing key
    @Bean
    public TopicExchange exchange() {
        return new TopicExchange(EXCHANGE, true, false);
        // durable=true: sobrevive ao restart do RabbitMQ
    }

    // Fila principal com Dead Letter Queue configurada
    @Bean
    public Queue filaPrincipal() {
        return QueueBuilder.durable(FILA_CONFIRMACAO)
            .withArgument("x-dead-letter-exchange", "")        // usa default exchange
            .withArgument("x-dead-letter-routing-key", FILA_DLQ) // redireciona para DLQ
            .build();
    }

    // Dead Letter Queue: armazena mensagens que falharam
    @Bean
    public Queue filaDlq() {
        return QueueBuilder.durable(FILA_DLQ).build();
    }

    // Binding: conecta o exchange à fila usando o routing key
    @Bean
    public Binding binding(Queue filaPrincipal, TopicExchange exchange) {
        return BindingBuilder.bind(filaPrincipal)
            .to(exchange)
            .with(ROUTING_KEY);
    }

    // Converte objetos Java para JSON automaticamente
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

Producer: publicando eventos de matrícula

MatriculaEventPublisher.javajava
@Service
public class MatriculaEventPublisher {

    private final RabbitTemplate rabbitTemplate;

    public MatriculaEventPublisher(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    // Publica o evento após a matrícula ser salva no banco
    public void publicarMatriculaCriada(MatriculaResponse matricula) {
        MatriculaCriadaEvent evento = new MatriculaCriadaEvent(
            matricula.id(),
            matricula.alunoEmail(),
            matricula.cursoNome(),
            LocalDateTime.now()
        );

        // Envia para o exchange com o routing key
        // O RabbitMQ roteia para todas as filas com binding compatível
        rabbitTemplate.convertAndSend(
            RabbitMQConfig.EXCHANGE,
            RabbitMQConfig.ROUTING_KEY,
            evento
        );

        log.info("Evento publicado: MatriculaCriada para {}", matricula.alunoEmail());
    }
}

// DTO do evento — deve ser serializável para JSON
public record MatriculaCriadaEvent(
    Long matriculaId,
    String alunoEmail,
    String cursoNome,
    LocalDateTime ocorridoEm
) {}

Consumer: processando mensagens

NotificacaoConsumer.javajava
@Component
public class NotificacaoConsumer {

    private final EmailService emailService;

    // @RabbitListener: fica "escutando" a fila e processa cada mensagem
    // acknowledge-mode: manual = confirma só após processar com sucesso
    @RabbitListener(queues = RabbitMQConfig.FILA_CONFIRMACAO,
                    ackMode = "MANUAL")
    public void processarMatriculaCriada(
            MatriculaCriadaEvent evento,
            Channel channel,
            @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {

        try {
            log.info("Processando matrícula: {} - {}", evento.alunoEmail(), evento.cursoNome());

            emailService.enviarConfirmacaoMatricula(evento.alunoEmail(), evento.cursoNome());

            // ACK: confirma que a mensagem foi processada — removida da fila
            channel.basicAck(deliveryTag, false);

        } catch (Exception e) {
            log.error("Erro ao processar evento de matrícula: {}", e.getMessage());
            try {
                // NACK: rejeita a mensagem — vai para a Dead Letter Queue
                // requeue=false: não volta para a fila principal
                channel.basicNack(deliveryTag, false, false);
            } catch (IOException ioEx) {
                log.error("Erro ao fazer NACK: {}", ioEx.getMessage());
            }
        }
    }
}
Diagrama 24.2 — Fluxo completo de matrícula com mensageria
sequenceDiagram participant C as Cliente participant MS as Serviço Matrículas participant DB as Banco participant RMQ as RabbitMQ participant NS as Serviço Notificações C->>MS: POST /matriculas MS->>DB: salva Matricula DB-->>MS: Matricula{id:42} MS->>RMQ: publish MatriculaCriadaEvent (routing: matricula.criada) Note over RMQ: roteia para fila matriculas.confirmacao MS-->>C: 201 Created {"id":42} (imediato!) Note over RMQ,NS: processamento assíncrono RMQ->>NS: MatriculaCriadaEvent NS->>NS: envia e-mail de confirmação NS->>RMQ: ACK (mensagem processada) Note over RMQ,NS: em caso de erro: NS-->>RMQ: NACK RMQ->>RMQ: move para Dead Letter Queue
💡 Docker Compose para RabbitMQ local
services:
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"   # AMQP
      - "15672:15672" # Management UI
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest

Acesse http://localhost:15672 para monitorar filas, mensagens e conexões.

24
Exercício — Dead Letter Queue e reprocessamento

Objetivo

Implementar tratamento de mensagens que falham repetidamente.

Tarefa

  • Configurar a DLQ matriculas.dlq conforme o exemplo acima.
  • Simular falha no consumer (lançar exceção para um e-mail específico).
  • Verificar no RabbitMQ Management que a mensagem vai para a DLQ.
  • Criar endpoint admin POST /admin/reprocessar-dlq que relê as mensagens da DLQ e tenta novamente.

Critério de aceite

Mensagem com erro vai para DLQ sem bloquear outras mensagens. O endpoint de reprocessamento consegue mover mensagens da DLQ de volta para a fila principal.

✓ O que aprendemos
  • RabbitMQ desacopla serviços temporalmente: produtor e consumidor não precisam estar ativos ao mesmo tempo.
  • Exchange roteia mensagens para filas baseado no routing key e tipo de exchange.
  • @RabbitListener com acknowledge manual garante que mensagens só sejam removidas após processamento bem-sucedido.
  • Dead Letter Queue armazena mensagens que falharam, permitindo análise e reprocessamento.
Fonte oficial: Spring AMQP Reference
Parte 7 — Resiliência e Mensageria
Capítulo 27

Empacotando e Deploy

Uma aplicação só tem valor quando está rodando em produção. Neste capítulo aprendemos a empacotar o EduSpring em um JAR executável, criar uma imagem Docker otimizada e fazer deploy em nuvem. Depois dele, a Parte 8 aprofunda logging, agendamento e e-mail em capítulos dedicados.

Gerando o JAR executável

terminalbash
# Gera o fat JAR (inclui todas as dependências)
./mvnw clean package -DskipTests

# O JAR gerado em target/ é autossuficiente
java -jar target/eduspring-1.0.0.jar

# Com profile de produção e variáveis de ambiente
SPRING_PROFILES_ACTIVE=prod \
DB_USER=postgres \
DB_PASS=secret \
java -jar target/eduspring-1.0.0.jar

Dockerfile com Multi-Stage Build

Dockerfilebash
# STAGE 1: Build — usa imagem completa do Maven para compilar
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
# Copia pom.xml primeiro para aproveitar cache de dependências
COPY pom.xml .
RUN mvn dependency:go-offline -q
# Copia o código e compila
COPY src ./src
RUN mvn clean package -DskipTests -q

# STAGE 2: Runtime — usa imagem mínima apenas com JRE
# Resultado: imagem final ~150MB (vs 500MB+ com JDK completo)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

# Cria usuário não-root (boa prática de segurança)
RUN addgroup -S spring && adduser -S spring -G spring
USER spring

# Copia apenas o JAR do stage anterior
COPY --from=build /app/target/eduspring-*.jar app.jar

EXPOSE 8080

# Executa com opções de JVM otimizadas para container
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

Docker Compose completo do EduSpring

docker-compose.ymlyaml
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_USER: ${DB_USER:-postgres}
      DB_PASS: ${DB_PASS:-postgres}
      DATABASE_URL: jdbc:postgresql://postgres:5432/eduspring
      RABBITMQ_HOST: rabbitmq
      APP_JWT_SECRET: ${JWT_SECRET}
    depends_on:
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: eduspring
      POSTGRES_USER: ${DB_USER:-postgres}
      POSTGRES_PASSWORD: ${DB_PASS:-postgres}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "15672:15672"    # UI apenas para desenvolvimento
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Deploy gratuito no Railway

Deploy no Railway (passo a passo)bash
# 1. Instale o Railway CLI
npm install -g @railway/cli

# 2. Login
railway login

# 3. Crie o projeto (na raiz do projeto)
railway init

# 4. Adicione serviços PostgreSQL e RabbitMQ no dashboard Railway

# 5. Configure variáveis de ambiente no dashboard:
# SPRING_PROFILES_ACTIVE = prod
# JWT_SECRET = (gere uma string aleatória segura)
# As variáveis de banco são injetadas automaticamente pelo Railway

# 6. Deploy!
railway up
# Railway detecta o Dockerfile e faz o build automaticamente
Diagrama 25.1 — Pipeline de build e deploy
flowchart LR CODE["Código\ngit push"] BUILD["Build Maven\nmvn package"] DOCKER["Docker Build\nmulti-stage"] REGISTRY["Container Registry\nDockerHub / Railway"] DEPLOY["Deploy\nRailway / Render"] HEALTH["Health Check\n/actuator/health"] CODE --> BUILD BUILD --> DOCKER DOCKER --> REGISTRY REGISTRY --> DEPLOY DEPLOY --> HEALTH HEALTH -->|"status: UP"| READY["Produção\nOnline!"]
25
Exercício — Deploy completo do EduSpring

Objetivo

Colocar o EduSpring em produção, acessível via URL pública.

Tarefa

  • Criar o Dockerfile com multi-stage build.
  • Testar localmente com docker compose up e verificar todos os endpoints.
  • Fazer deploy no Railway (ou Render) com PostgreSQL como serviço.
  • Verificar que https://seu-app.up.railway.app/actuator/health retorna UP.

Critério de aceite

  • Aplicação acessível via URL pública HTTPS.
  • /actuator/health retorna {"status":"UP"}.
  • Swagger UI acessível em /swagger-ui.html.
  • Criar usuário via /auth/registrar e fazer login via /auth/login funciona.
✓ O que aprendemos
  • O fat JAR do Spring Boot contém todas as dependências e é executável diretamente.
  • Dockerfile multi-stage separa build e runtime, gerando imagens menores e mais seguras.
  • Docker Compose orquestra múltiplos serviços localmente com healthchecks e dependências.
  • Plataformas como Railway e Render permitem deploy simples com detecção automática de Dockerfile.
  • Variáveis de ambiente são o padrão correto para configurações de produção — nunca em arquivos de código.
Parte 8 — Logging, agendamento e e-mail
Capítulo 28

Logging com SLF4J e Logback

Logs são o raio-X da aplicação em produção. Neste capítulo unificamos a API de logging (SLF4J), a implementação padrão no Spring Boot (Logback), o envio para a nuvem (Loggly) e a troca de configuração por Spring Profiles.

27.1. Introdução ao Logback e SLF4J

SLF4J (org.slf4j.Logger) é uma fachada: você programa contra uma API estável e, em runtime, o classpath escolhe o backend (no Boot, quase sempre Logback). Isso evita acoplar o código a java.util.logging ou a outra biblioteca diretamente.

CursoService.javajava
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class CursoService {

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

    public Curso buscar(Long id) {
        log.debug("Buscando curso id={}", id);
        // ...
        log.info("Curso encontrado: {}", id);
        return curso;
    }
}
  • Use placeholders {} em vez de concatenar strings — o SLF4J só monta a mensagem se o nível estiver habilitado.
  • Níveis usuais: TRACE < DEBUG < INFO < WARN < ERROR.
  • No Spring Boot, logging.level.root e logging.level.com.dfcode no application.yml ajustam verbosidade sem recompilar.

27.2. Desafio: registrando logs de exceções não tratadas

Exceções que não passam por um @RestControllerAdvice (erro no container, filtro, ou bug fora do MVC) podem não gerar o log que você espera. Duas linhas de ação:

  • Camada web: garanta um @RestControllerAdvice (como no capítulo 6) que logue em WARN/ERROR com stack trace antes de montar o ProblemDetail.
  • Último recurso (JVM): em uma @Configuration, registre um handler para threads que morrem sem tratamento — útil para workers ou código legado.
JvmExceptionLoggingConfig.javajava
@Configuration
public class JvmExceptionLoggingConfig {

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

    @PostConstruct
    void registrarUncaught() {
        Thread.setDefaultUncaughtExceptionHandler((thread, ex) ->
            log.error("Exceção não tratada na thread {}", thread.getName(), ex));
    }
}
Atenção

O handler acima não substitui tratamento de negócio nem respostas HTTP corretas; ele só garante rastro no log quando algo escapa do fluxo normal.

27.3. Criando uma conta no Loggly

Loggly é um serviço SaaS de agregação de logs: você envia eventos via HTTP/TCP, pesquisa por texto, monta dashboards e alertas. Fluxo típico:

  1. Criar conta e obter o customer token (identificador da sua conta no endpoint de ingestão).
  2. Definir o token como variável de ambiente (ex.: LOGGLY_TOKEN) — nunca commitar no repositório.
  3. Validar ingestão com um evento de teste (curl ou appender, próxima seção).

27.4. Configurando o appender do Loggly no Logback

O Spring Boot carrega logback-spring.xml (preferencial a logback.xml) para poder usar <springProfile>. Para HTTP, costuma-se usar um appender que posta JSON ou texto para o endpoint Loggly. Exemplo conceitual com logstash-logback-encoder (dependência no pom.xml):

logback-spring.xml (trecho — produção)xml
<configuration>
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  <springProperty name="LOGGLY_TOKEN" source="loggly.token" />

  <appender name="LOGGLY" class="ch.qos.logback.core.ConsoleAppender">
    <!-- Em produção real: use appender HTTP ou logstash encoder apontando para
         https://logs-01.loggly.com/inputs/SEU_TOKEN/tag/http -->
    <encoder>
      <pattern>%d{ISO8601} %-5level [%thread] %logger - %msg%n</pattern>
    </encoder>
  </appender>

  <springProfile name="prod">
    <root level="INFO">
      <appender-ref ref="CONSOLE"/>
      <appender-ref ref="LOGGLY"/>
    </root>
  </springProfile>
</configuration>

Substitua o ConsoleAppender de exemplo pelo appender oficial da sua stack (documentação Loggly + encoder escolhido). O importante é: token via propriedade, appender só em prod, e formato estruturado (JSON) quando possível para buscas no painel.

27.5. Configurando o Logback para alternar por Spring Profiles

Use <springProfile name="dev"> / prod dentro do logback-spring.xml ou mantenha um único arquivo e sobrescreva níveis no application-{profile}.yml:

application-dev.yml / application-prod.ymlyaml
# dev: verboso
logging:
  level:
    root: INFO
    com.dfcode.eduspring: DEBUG

---
# prod: só o necessário
logging:
  level:
    root: WARN
    com.dfcode.eduspring: INFO

Combine: application.yml com padrão, profiles com níveis e appenders, e variáveis de ambiente para tokens de serviços externos.

27
Exercício — Logging em camadas

Objetivo

Padronizar logs no EduSpring com níveis corretos e profile prod menos barulhento que dev.

Tarefa

  • Adicionar Logger estático em CursoService e MatriculaService com debug em operações de leitura e info em mutações.
  • Criar logback-spring.xml com <springProfile name="dev"> em DEBUG para o pacote da aplicação.
  • Registrar (ou simular) envio de um evento ERROR com stack trace para o painel Loggly ou equivalente.
✓ O que aprendemos
  • SLF4J abstrai o backend; Logback é o padrão do Spring Boot.
  • Exceções devem ser logadas no advice global; opcionalmente há handler JVM para threads sem catch.
  • SaaS de logs (Loggly) recebe eventos via appender; token sempre por segredo/ambiente.
  • logback-spring.xml + profiles alinham verbosidade e destinos por ambiente.
Parte 8 — Logging, agendamento e e-mail
Capítulo 29

Agendamento de tarefas com @Scheduled

O Spring abstrai execução e agendamento com TaskExecutor e TaskScheduler. Na prática do dia a dia, @EnableScheduling + @Scheduled resolvem relatórios periódicos, limpeza de dados e integrações batch leves — sem precisar gerenciar Timer manualmente.

Leitura oficial

A referência completa (interfaces, CronTrigger, pools, @Async, namespace XML task:) está em Task Execution and Scheduling — Spring Framework.

Habilitar o agendamento

Uma única classe de configuração (ou a classe principal) deve ter @EnableScheduling. O bean que contém os métodos agendados precisa ser gerenciado pelo container (@Component, @Service, etc.).

EduSpringApplication.java ou SchedulingConfig.javajava
@SpringBootApplication
@EnableScheduling
public class EduSpringApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduSpringApplication.class, args);
    }
}

fixedDelay, fixedRate e initialDelay

  • fixedDelay: espera o intervalo depois que o método termina — bom quando a duração varia (ex.: processar fila e só então esperar 5 min).
  • fixedRate: tenta iniciar a cada período a partir do início da execução anterior — pode sobrepor se o job demorar mais que o período (use lock ou pool maior).
  • initialDelay: atraso antes da primeira execução.
  • Desde versões recentes do Spring, use timeUnit = TimeUnit.SECONDS (ou minutos) para legibilidade em vez de milissegundos mágicos.
ManutencaoAgendada.javajava
@Component
public class ManutencaoAgendada {

    private static final Logger log = LoggerFactory.getLogger(ManutencaoAgendada.class);
    private final TokenRepository tokenRepository;

    public ManutencaoAgendada(TokenRepository tokenRepository) {
        this.tokenRepository = tokenRepository;
    }

    /** Após cada término, aguarda 10 minutos. */
    @Scheduled(fixedDelay = 10, timeUnit = TimeUnit.MINUTES)
    public void limparTokensExpirados() {
        int n = tokenRepository.deleteExpiradosAntesDe(Instant.now());
        log.info("Tokens removidos: {}", n);
    }

    /** Métricas a cada minuto (relógio entre inícios). */
    @Scheduled(initialDelay = 30, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
    public void publicarMetricasSinteticas() {
        // ex.: incrementar gauge ou enviar heartbeat
    }
}

Cron no Spring (seis campos)

No Spring, a expressão cron tem seis campos: segundo, minuto, hora, dia do mês, mês, dia da semana. Exemplo: todo dia às 8h (horário do servidor, a menos que você defina zone):

RelatorioDiarioScheduler.javajava
@Component
public class RelatorioDiarioScheduler {

    @Scheduled(cron = "0 0 8 * * *", zone = "America/Sao_Paulo")
    public void enviarResumoMatriculas() {
        // monta relatório do dia anterior e envia e-mail (cap. 28)
    }
}

O parâmetro zone evita surpresas quando o servidor está em UTC e o negócio é Brasil — veja Spring Framework — @Scheduled e zona horária.

Para agendamento no ecossistema Spring, combine Spring Boot — Task Scheduling com a referência do Spring Framework.

Armadilhas comuns

  • Chamada interna: métodos @Scheduled são interceptados por proxy; chamar this.outroMetodoAgendado() da mesma classe não agenda — extraia para outro bean ou use @Lazy + auto-injeção se necessário.
  • Pool padrão: com muitas tarefas longas, configure um TaskScheduler com mais threads via SchedulingConfigurer (documentação Spring).
  • Tarefas pesadas: preferir fila (RabbitMQ, cap. 24) para não bloquear o scheduler único.
Diagrama 28.1 — Onde o @Scheduled encaixa no container
flowchart LR subgraph Spring["Spring Context"] ES["@EnableScheduling"] TS["TaskScheduler"] B["@Component\nRelatorioScheduler"] end ES --> TS TS -->|"dispara no trigger"| B B -->|"fixedDelay / cron"| JOB["Runnable do método"]
28
Exercício — Dois agendamentos reais

Objetivo

Consolidar cron + fixedDelay no EduSpring.

Tarefa

  • Criar @Component RelatorioScheduler com @Scheduled(cron = "0 0 8 * * *", zone = "America/Sao_Paulo") que apenas registre INFO “relatório diário disparado”.
  • Adicionar segundo método com fixedDelay de 2 minutos (ambiente dev apenas) usando @Scheduled + condicional ou profile, para não poluir produção.
  • Provar nos logs que, após subir a aplicação, ambos disparam conforme esperado.
✓ O que aprendemos
  • @EnableScheduling ativa o registro de triggers no TaskScheduler.
  • fixedDelay mede a partir do fim; fixedRate a partir do início; cron usa seis campos no Spring.
  • zone alinha horário de negócio ao fuso correto.
  • Proxies do Spring exigem cuidado com chamadas internas e dimensionamento do pool para jobs longos.
Parte 8 — Logging, agendamento e e-mail
Capítulo 30

E-mail na aplicação

Notificações por e-mail continuam sendo o padrão para confirmações, recuperação de senha e alertas operacionais. O Spring integra com Jakarta Mail via JavaMailSender; em cenários SaaS, APIs como SendGrid também se integram bem. Este capítulo organiza as opções e aponta leituras práticas.

Documentação Spring

A base conceitual (MailSender, SimpleMailMessage, MimeMessageHelper, anexos, HTML) está em Spring Framework — Email.

E-mail texto com JavaMailSender

Dependência típica no Boot: spring-boot-starter-mail (traz Angus/Jakarta Mail). Configure SMTP no YAML e injete JavaMailSender:

application-prod.ymlyaml
spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: ${MAIL_USER}
    password: ${MAIL_PASS}
    properties:
      mail.smtp.auth: true
      mail.smtp.starttls.enable: true

O mesmo padrão do capítulo 23: SimpleMailMessage para texto plano; para HTML e anexos, use MimeMessage + MimeMessageHelper (ver documentação linkada acima).

HTML com template (Thymeleaf)

Evite concatenar HTML no Java. Com spring-boot-starter-thymeleaf, injete SpringTemplateEngine, processe um template em src/main/resources/templates/email/ com contexto (Context com variáveis) e passe o resultado para helper.setText(html, true). Base técnica: Spring Framework — E-mail e Thymeleaf — Using Thymeleaf.

SendGrid e outros provedores HTTP

Provedores como SendGrid expõem API REST; o SDK Java monta Mail, Personalization e envia via HTTP. Boas práticas: chave em variável de ambiente, validação com @ConfigurationProperties e testes com mock HTTP. Referência da API: SendGrid API Reference (Twilio).

Perfil dev: não enviar e-mail real

Mantenha interface EmailService com implementações @Profile("prod") (SMTP ou SendGrid) e @Profile("dev") que só loga o corpo — padrão já sugerido no capítulo 10 (Profiles e Ambientes). Consulte Spring Boot — E-mail.

30
Exercício — Confirmação HTML + fake em dev

Objetivo

Enviar e-mail HTML de confirmação de matrícula em prod e apenas logar em dev.

Tarefa

  • Criar template Thymeleaf matricula-confirmada.html com nome do aluno e do curso.
  • Implementar EmailServiceProd com JavaMailSender + HTML renderizado.
  • Implementar EmailServiceDev que escreve o HTML no log INFO.
✓ O que aprendemos
  • JavaMailSender é o caminho nativo Spring/Jakarta Mail para SMTP.
  • Templates (Thymeleaf, FreeMarker) separam layout de código.
  • SendGrid e similares usam API HTTP; úteis em escala e deliverability.
  • Profiles isolam envio real de ambientes de desenvolvimento.
Parte 9 — Cache e performance
Capítulo 31

Spring Cache — da memória ao Redis

A abstração de cache do Spring aplica cache em métodos, reduzindo leituras repetidas ao banco ou a serviços remotos. O Spring Boot auto-configura o CacheManager quando você habilita o suporte e escolhe um provedor (simples, Caffeine, Redis…). Referência: Spring Boot — Caching.

Habilitando o cache

Em uma classe de configuração (não necessariamente na classe principal — em testes, prefira isolar), use @EnableCaching. O guia do Boot recomenda não colar @EnableCaching na classe @SpringBootApplication se isso tornar o cache obrigatório em todos os testes de fatia; prefira uma @Configuration dedicada.

CacheConfig.javajava
package br.com.dfcode.eduspring.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig { }
pom.xml — starterxml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

@Cacheable, @CachePut e @CacheEvict

  • @Cacheable — antes de executar o método, consulta o cache pela chave; em acerto, retorna sem executar o corpo (padrão cache-aside do lado da aplicação).
  • @CachePut — sempre executa o método e atualiza o cache (útil após gravação quando o retorno é o estado atualizado).
  • @CacheEvict — remove entradas em atualizações ou exclusões para evitar dados obsoletos; combine com allEntries ou chaves específicas.
CursoService.java (trecho)java
import org.springframework.cache.annotation.*;

@Service
public class CursoService {

    @Cacheable(value = "cursos", key = "#id")
    public CursoDto buscarPorId(Long id) {
        return cursoRepository.findById(id).map(CursoDto::from).orElseThrow();
    }

    @CacheEvict(value = "cursos", key = "#dto.id")
    public CursoDto atualizar(CursoDto dto) {
        // 1) mapear dto -> entidade, salvar no repositório, 2) retornar DTO atualizado
        return dto;
    }
}

Provedores: simples, Caffeine e Redis

Sem biblioteca extra, o Boot usa um ConcurrentHashMap — ótimo para aprender, ruim para produção multi-instância (cada JVM tem seu mapa). Caffeine é uma escolha forte em processo único (TTL, tamanho máximo via spring.cache.caffeine.spec). Redis compartilha cache entre instâncias: adicione spring-boot-starter-data-redis, configure Redis e propriedades como spring.cache.type=redis e spring.cache.redis.time-to-live (ex.: 10m). Referência: Spring Boot — Caching, Spring Data Redis e Redis — documentação.

application.yml — Redis como cacheyaml
spring:
  data:
    redis:
      host: localhost
      port: 6379
  cache:
    type: redis
    redis:
      time-to-live: 600000
    cache-names: cursos, alunos
🚨 Consistência

Cache introduz staleness. Defina TTL, invalide com @CacheEvict nas mutações e monitore memória ou chaves no Redis. Em múltiplas instâncias, cache em memória local pode servir respostas diferentes — Redis ou outro store distribuído alinha melhor o comportamento.

Testes

Para integração sem cache real, use spring.cache.type=none ou @AutoConfigureCache conforme a documentação do Boot, garantindo testes determinísticos.

31
Exercício — Cache em listagem

Objetivo

Cachear a listagem de cursos ativos e invalidar ao cadastrar um novo curso.

Tarefa

  • Criar cache cursosAtivos em um método listarAtivos().
  • Evict ao salvar novo Curso.
  • Medir (logs ou Actuator) uma segunda chamada sem hit no banco.
✓ O que aprendemos
  • @EnableCaching ativa o processamento das anotações de cache.
  • @Cacheable / @CacheEvict modelam leitura e invalidação.
  • O Boot escolhe o CacheManager pelo classpath; Redis exige configuração explícita e TTL.
  • Discutimos trade-offs de cache local vs distribuído: TTL, invalidação e escolha de store alinhados à documentação do Spring Boot sobre caching.