Spring na Prática
Spring Data JPA e Spring Security
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.
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ódulo | O que faz | Quando usar |
|---|---|---|
| Spring Framework | O núcleo: IoC, DI, AOP, MVC | Base de tudo, sempre presente |
| Spring Boot | Configuração automática e embedded server | Todo projeto novo |
| Spring MVC | Camada web: controllers, REST, serialização | APIs e aplicações web |
| Spring Data JPA | Persistência com JPA/Hibernate simplificada | Projetos com banco relacional |
| Spring Security | Autenticação, autorização, JWT, OAuth2 | Qualquer app com controle de acesso |
| Spring Cloud | Config, Discovery, Gateway, Circuit Breaker | Arquiteturas de microsserviços |
| Spring AMQP | Mensageria com RabbitMQ | Comunicação assíncrona entre serviços |
(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.
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.
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 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.
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:
# 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
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
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?
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.
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:runou pela IDE). - Acesse
http://localhost:8080/actuator/healthno navegador.
Critério de aceite
O endpoint /actuator/health retorna {"status":"UP"} no navegador.
- O Spring Initializr gera a estrutura completa do projeto com dependências configuradas.
@SpringBootApplicationcombina 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.
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:
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:
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ção | Camada | Uso |
|---|---|---|
@Component | Genérico | Qualquer bean que não se encaixa nas outras |
@Service | Negócio | Classes com regras de negócio |
@Repository | Dados | Acesso ao banco; converte exceções JPA em DataAccessException |
@Controller | Web | Recebe requisições HTTP; retorna views |
@RestController | Web | @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:
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:
| Escopo | Comportamento | Exemplo de uso |
|---|---|---|
singleton | Uma instância para toda a aplicação (padrão) | Services, Repositories |
prototype | Nova instância a cada injeção | Builders, objetos com estado temporário |
request | Uma instância por requisição HTTP | Objetos que guardam dados da requisição |
session | Uma instância por sessão HTTP | Rascunho 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
@Beanou nome da classe em camelCase). - Anotação customizada — composta com
@Qualifier, deixa o código mais legível que strings soltas.
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;
}
}
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) { }
@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));
}
}
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.
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
RelogioServicecom o métodoLocalDateTime agora(). - Criar
RelogioServiceDev(anotado com@Profile("dev")) que retornaLocalDateTime.of(2024, 1, 1, 8, 0). - Criar
RelogioServiceProd(anotado com@Profile("prod")) que retornaLocalDateTime.now(). - Injetar
RelogioServiceem um controller e expor endpointGET /hora.
Critério de aceite
- Com
spring.profiles.active=dev:GET /horaretorna2024-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.
- 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,@Repositorye@Controllersão especializações semânticas de@Component.@Bean+@Configurationservem para configurar beans de bibliotecas externas.- O escopo padrão é Singleton — uma instância compartilhada para toda a aplicação.
@Primary,@Qualifiere qualifiers customizados resolvem ambiguidade entre implementações.List<Interface>no construtor recebe todas as implementações registradas.
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.
@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).
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
}
}
- 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/alunospara listar alunos de um curso.
Objetivo
Adicionar endpoint de busca com múltiplos filtros opcionais.
Tarefa
- Criar endpoint
GET /alunos/buscarque aceite@RequestParam(required = false)paranomeeemail. - Se nenhum parâmetro for informado, retornar todos os alunos.
- Se
nomefor 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
DispatcherServleté o Front Controller que orquestra todas as requisições HTTP. @RestControllerserializa automaticamente os retornos para JSON.@PathVariableextrai segmentos da URL;@RequestParamlê query strings;@RequestBodydeserializa o body.- Use substantivos no plural para rotas e verbos HTTP para ações.
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
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
// @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):
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á:
{
"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.
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.
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);
}
}
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.
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.
Criando uma validação customizada
// 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);
}
}
Objetivo
Aplicar validações no DTO de criação de curso.
Tarefa
- Criar
CriarCursoRequestcom campos:nome(obrigatório, 3-150 chars),cargaHoraria(min 4, max 400 horas),descricao(opcional, max 500 chars). - Configurar o
GlobalExceptionHandlerpara 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.
- Bean Validation valida dados de entrada antes de chegarem ao service.
@Validno controller ativa a validação; falhas lançamMethodArgumentNotValidException.@RestControllerAdvicecentraliza o tratamento de erros de toda a aplicação.ProblemDetailpadroniza erros no formato RFC 7807.ResponseEntityExceptionHandlercustomiza erros já tratados pelo Spring MVC.HttpMessageNotReadableException+ causaInvalidFormatExceptioncobre JSON com tipos errados.- Validações customizadas são criadas implementando
ConstraintValidator.
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ível | Característica | Exemplo |
|---|---|---|
| 0 — POX | HTTP como transporte apenas | POST /api com action no body |
| 1 — Recursos | URLs representam recursos | POST /cursos/criar |
| 2 — Verbos HTTP | Verbos HTTP para ações + Status codes corretos | POST /cursos → 201 Created |
| 3 — HATEOAS | Respostas incluem links para ações relacionadas | Response com _links |
ResponseEntity — Controle total da resposta
// 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.
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
// 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:
<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:
@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.
| Tipo | URI exemplo | Significado |
|---|---|---|
| Collection | GET /cursos | Conjunto de cursos (lista paginada ou completa). |
| Singleton | GET /cursos/{id} | Um curso específico — use @PathVariable Long id. |
| Sub-recurso | GET /cursos/{id}/matriculas | Relacionamento explícito na URL. |
Métodos HTTP — quando usar cada um
| Método | Uso | Idempotente? | Body típico |
|---|---|---|---|
GET | Ler recurso ou coleção | Sim | Não |
POST | Criar recurso (servidor define URI/id) | Não | Sim |
PUT | Substituição completa do recurso | Sim | Sim |
PATCH | Atualização parcial (só campos enviados) | Não* | Sim |
DELETE | Remover recurso | Sim | Nã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.
@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) { ... }
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
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.
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.
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).
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);
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).
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).
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);
}
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.
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);
}
}
const r = await fetch('http://localhost:8080/api/cursos', {
headers: { 'Accept': 'application/json' }
});
const cursos = await r.json();
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<>() {});
Objetivo
Implementar atualização parcial com regra de negócio.
Tarefa
- Criar endpoint
PATCH /cursos/{id}/statusque recebe{"ativo": false}. - Se o curso tiver alunos matriculados ativos, lançar
RegraVioladaExceptioncom mensagem descritiva. - O
GlobalExceptionHandlerdeve 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".
Objetivo
Consolidar três tópicos do capítulo num fluxo único sobre o recurso Curso.
Tarefa
- Content negotiation: expor
GET /cursos/{id}comproducesJSON e XML; testar comAccept: application/xmleapplication/json. - PATCH: criar
PATCH /cursos/{id}com DTO parcial (ex.: sónomeou sócargaHoraria); garantir que campos omitidos não zerem a entidade. - Cache: no mesmo GET, enviar
ETag+Cache-Control: max-age=60; validar304 Not ModifiedcomIf-None-Matchno 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.
- 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;
@JsonViewe 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 comfetcheRestClient. ResponseEntitycontrola status, headers e corpo; collection vazia costuma ser 200 +[].- Springdoc/OpenAPI documenta a API para times e integradores.
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.
<?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 / comando | Efeito |
|---|---|
mvn validate | Verifica se o POM está íntegro. |
mvn compile | Compila main para target/classes. |
mvn test | Roda testes (Surefire); usa dependências com escopo test. |
mvn package | Gera o JAR/WAR (sem instalar no repositório local). |
mvn verify | Inclui verificações pós-package (ex.: integração). |
mvn install | Instala o artefato no ~/.m2 para outros módulos consumirem. |
mvn clean package -DskipTests | Limpa target, empacota e pula testes (CI com cuidado). |
Escopos (<scope>): o que entra no classpath
| Escopo | Compile | Runtime | Test | Uso típico |
|---|---|---|---|---|
| compile (padrão) | Sim | Sim | Sim | Sua API e libs usadas no código principal (Spring Web, JPA). |
| provided | Sim | Não | Sim | O container já oferece em runtime (ex.: servlet API em WAR no Tomcat externo). |
| runtime | Não | Sim | Sim | Implementação trocável não referenciada no código-fonte (driver JDBC, Logback impl). |
| test | Não | Não | Sim | JUnit, Mockito, AssertJ — não vão para o JAR de produção. |
| import | Só em <dependencyManagement> (BOMs); não adiciona dependência sozinha. | |||
| system | Evite: jar local com systemPath — frágil e não portável. | |||
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 comospring-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.
Objetivo
Enxergar o que realmente entra no classpath de runtime do EduSpring.
Tarefa
- Executar
./mvnw dependency:treee localizar de onde vem o Hibernate (transitivo despring-boot-starter-data-jpa). - Adicionar propositalmente uma dependência de teste sem
scopetest, rodarpackagee inspecionar o JAR comjar tf target/*.jar | findstr junit(Windows) oujar tf ... | grep junit— depois corrigir comscope>test</scope>. - Listar apenas dependências de teste:
mvn dependency:list -DincludeScope=test.
- Maven modela o projeto no
pom.xmlcom GAV e herança dospring-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.
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
- Project: Maven ou Gradle — no ebook usamos Maven; o time pode padronizar Gradle Kotlin DSL se preferir.
- Language: Java (Kotlin e Groovy também são suportados).
- Spring Boot: escolha a linha estável suportada pelo seu time (evite SNAPSHOT em produção).
- Group / Artifact / Name / Package: exemplo
br.com.dfcode/eduspring— o package raiz deve ser o mesmo usado no@SpringBootApplicationpara o component scan funcionar. - Packaging: Jar para aplicações executáveis com
java -jar; War apenas se for implantar em servlet container externo. - Java: alinhe à versão LTS instalada (17, 21…).
Dependências recomendadas para o EduSpring
| Dependência | Para quê |
|---|---|
| Spring Web | REST + Tomcat embutido |
| Spring Data JPA | Repositórios e Hibernate |
| PostgreSQL Driver | Banco usado nos exemplos |
| Validation | Bean Validation nas APIs |
| Spring Boot Actuator | Saúde e métricas |
| Spring Boot DevTools | Restart rápido em desenvolvimento |
| Lombok | Menos boilerplate (opcional mas comum) |
Use Spring Configuration Processor se for criar @ConfigurationProperties — sua IDE ganha metadados para autocomplete no application.yml.
Depois de gerar o ZIP
- Descompacte e importe como Maven project na IDE.
- Execute
./mvnw spring-boot:run(Linux/macOS) oumvnw.cmd spring-boot:run(Windows). - Confirme
/actuator/healthse adicionou Actuator.
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.xmlem relação ao projeto completo.
- 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.
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:
// 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();
}
}
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:
| Starter | O que inclui |
|---|---|
spring-boot-starter-web | Spring MVC, Jackson, Tomcat embutido, Validation |
spring-boot-starter-data-jpa | Spring Data JPA, Hibernate, JDBC |
spring-boot-starter-security | Spring Security, filtros de autenticação |
spring-boot-starter-test | JUnit 5, Mockito, AssertJ, MockMvc |
spring-boot-starter-amqp | Spring AMQP, RabbitMQ client |
application.yml — Configuração legível
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.
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).
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.
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(initMethod = "abrir", destroyMethod = "fechar")
public MeuClienteGrpc meuCliente() {
return new MeuClienteGrpc();
}
@ConfigurationProperties — Configuração tipada
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; }
}
Objetivo
Criar um bean de configuração tipado para as regras de matrícula.
Tarefa
- Criar classe
MatriculaPropertiesmapeando o prefixoapp.matricula. - Adicionar no
application.yml:app.matricula.vagas-padrao: 30eapp.matricula.dias-prazo-pagamento: 5. - Injetar
MatriculaPropertiesnoMatriculaServicee 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.
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.
- Auto-configuration usa
@Conditionalpara configurar beans só quando necessário. - Starters agrupam dependências compatíveis em uma única declaração.
application.ymleapplication.propertiessã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.
@Valueinjeta propriedades pontuais;@ConfigurationPropertiesagrupa configurações tipadas.@PostConstruct/@PreDestroy,InitializingBean/DisposableBeaneinitMethodmodelam o ciclo de vida do bean.- Variáveis de ambiente têm prioridade alta sobre arquivos — essencial para produção.
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
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
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
// 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
# 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
Objetivo
Configurar os três ambientes do projeto EduSpring.
Tarefa
- Criar
application-dev.ymlcom H2 + show-sql + h2-console. - Criar
application-test.ymlcom H2 sem show-sql. - Criar
application-prod.ymlcom PostgreSQL via variáveis de ambiente. - Implementar
EmailServiceFakeeEmailServiceSmtpcom@Profilecorreto.
Critério de aceite
- Com profile
dev: console H2 acessível e SQL logado. - Com profile
prodsem variáveis de ambiente definidas: aplicação falha na inicialização com mensagem clara.
- Profiles permitem comportamentos distintos por ambiente sem alterar código.
- Arquivos
application-{profile}.ymlsobrescrevem as propriedades doapplication.ymlbase. @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.
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.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Endpoints principais
| Endpoint | O que mostra |
|---|---|
/actuator/health | Status de saúde (UP/DOWN) + verificações customizadas |
/actuator/info | Informações da aplicação (versão, ambiente, build) |
/actuator/metrics | Métricas: JVM, HTTP, banco de dados, custom |
/actuator/env | Todas as propriedades de configuração ativas |
/actuator/mappings | Lista de todos os endpoints HTTP registrados |
/actuator/beans | Todos os beans registrados no contexto Spring |
Configurando o Actuator com segurança
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
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();
}
}
Objetivo
Criar um health indicator que verifica a conectividade com um serviço externo.
Tarefa
- Criar
ViaCepHealthIndicatorque faz um GET emhttps://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 Actuator adiciona endpoints de monitoramento com uma única dependência.
- Em produção, exponha apenas
healtheinfopublicamente — proteja os demais. HealthIndicatorpermite adicionar verificações customizadas ao/actuator/health.- Load balancers usam o health endpoint para decidir se mantêm uma instância no pool.
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
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;
}
Configuração JPA no application.yml
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
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).
Objetivo
Mapear a entidade Professor e verificar o DDL gerado.
Tarefa
- Criar
Professor.javacom:id,nome(obrigatório),email(único),especialidade,ativo. - Usar
@Getter @Setter @Builderdo Lombok. - Rodar com
show-sql: truee verificar no console oCREATE TABLEgerado.
Critério de aceite
O log exibe CREATE TABLE professores (id BIGSERIAL PRIMARY KEY, nome VARCHAR(100) NOT NULL, ...) ao iniciar.
- JPA é a especificação; Hibernate é a implementação padrão no Spring Boot.
@Entity,@Table,@Id,@GeneratedValuee@Columnsão as anotações fundamentais de mapeamento.- Lombok elimina boilerplate, mas evite
@Dataem entidades por conta do lazy loading. ddl-auto: create-dropé útil em desenvolvimento; em produção usevalidate+ Flyway.
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
| Interface | Métodos incluídos | Quando 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 |
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();
}
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.
// 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.
-- 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;
$$;
// 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.
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);
}
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.
Objetivo
Criar queries no repositório de alunos.
Tarefa
- Criar
AlunoRepositorycom: 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
AlunoResumocom apenasid,nomeeemail.
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 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.
@Querycom JPQL usa nomes de classes e campos Java (não de tabelas).- Projeções retornam apenas os campos necessários, melhorando performance.
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).
| Valor | Comportamento | Uso 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_NEW | Sempre 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. |
MANDATORY | Exige transação ativa; senão, exceção. | Métodos internos que nunca devem ser a raiz da transação. |
NOT_SUPPORTED | Executa sem transação (suspende se houver). | Chamadas a recursos que não suportam transação JTA. |
@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.
@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.
}
}
@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
}
}
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.
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));
});
}
}
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.
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);
}
}
@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
}
}
}
@TransactionalA 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).
Objetivo
Garantir que um registro de auditoria seja salvo mesmo quando a operação principal falhar.
Tarefa
- Criar entidade
AuditoriaOperacaoe 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.
REQUIREDparticipa da transação existente ou cria uma nova.REQUIRES_NEWisola commit/rollback em uma transação separada.- Rollback padrão em
RuntimeException/Error; userollbackForpara exceções checadas. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()aborta a TX sem lançar exceção.TransactionTemplateoferece 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.
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
// 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:
{
"content": [ {"id":1,"nome":"Java Básico"}, ... ],
"pageable": { "pageNumber": 0, "pageSize": 10 },
"totalElements": 47,
"totalPages": 5,
"last": false,
"first": true
}
Auditoria Automática
@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
// 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) {
// ...
}
}
Objetivo
Implementar listagem paginada de cursos com múltiplos filtros.
Tarefa
- Endpoint
GET /cursosaceita:nome(optional),ativo(optional),page,size,sort. - Adicionar campos
criadoEmeatualizadoEmna entidadeCursovia@CreatedDate. - Adicionar
@Cacheableno métodobuscarPorId.
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).
Pageableé populado automaticamente pelo Spring MVC a partir dos query paramspage,sizeesort.Page<T>inclui metadados de paginação na resposta JSON automaticamente.@CreatedDatee@LastModifiedDatecom@EnableJpaAuditingpreenchem timestamps automaticamente.@Cacheablee@CacheEvictadicionam cache na camada de serviço sem código adicional.
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).
// 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) {}
@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.
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;
}
}
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) { ... }
// }
}
@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.
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
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.
ApplicationEventPublisher.publishEventnotifica todos os listeners compatíveis.@TransactionalEventListener(AFTER_COMMIT)evita efeitos colaterais quando a TX principal faz rollback.@Orderordena 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.
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()).
<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.
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();
}
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);
}
}
@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)
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);
}
}
}
}
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true) // CGLIB: necessário quando o alvo não tem interface
public class EduspringApplication { ... }
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.
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
@AfterThrowingque incremente um contador de erros separado.
@Pointcutreutiliza expressões; combine com&&,||,!.@Aroundcontrola antes/depois e deve chamarproceed()para executar o método real.@AfterReturning/@AfterThrowingrecebemreturning/throwingpara 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.
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:
SecurityFilterChain para APIs REST
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();
}
}
Objetivo
Adicionar Spring Security e verificar o comportamento dos endpoints.
Tarefa
- Adicionar
spring-boot-starter-securityao projeto. - Verificar que todos os endpoints retornam 401 sem configuração.
- Criar
SecurityConfigliberandoGET /cursos/**e/swagger-ui/**. - Verificar que o Swagger ainda funciona mas
POST /cursosretorna 401 em JSON.
Critério de aceite
GET /cursos retorna 200; POST /cursos retorna {"erro":"Token ausente ou inválido"} com status 401.
- 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.AuthenticationEntryPointeAccessDeniedHandlercustomizados retornam JSON em vez de páginas HTML.
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
@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
@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
// 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);
}
}
Objetivo
Implementar o endpoint de cadastro de usuários.
Tarefa
- Criar
POST /auth/registrarque 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$).
- 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
DaoAuthenticationProviderconecta o Spring Security ao seu banco de dados automaticamente.
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
.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
// 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) { ... }
}
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
@PreAuthorizenos 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.
requestMatchersdefine regras de acesso por padrão de URL e método HTTP.@PreAuthorizeoferece 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.
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:
// 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
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
<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>
@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("");
}
}
@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
}
}
Objetivo
Implementar o fluxo completo de autenticação JWT.
Tarefa
- Criar
POST /auth/loginque recebe email/senha e retorna o JWT. - Registrar o
JwtAuthFilterantes doUsernamePasswordAuthenticationFilternoSecurityConfig. - 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 /cursoscom role ADMIN → 201 Created. - Token expirado ou inválido → 401 Unauthorized.
- JWT tem três partes: header, payload e signature. O payload é legível mas não criptografado.
JwtServiceencapsula a geração e validação de tokens.JwtAuthFilterintercepta 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.
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
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
@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
// @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
// @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");
}
}
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
MockMvcpara todos os endpoints deCursoController. - Adicionar o plugin JaCoCo no
pom.xmle rodar./mvnw verify.
Critério de aceite
Relatório JaCoCo em target/site/jacoco/index.html mostra cobertura ≥ 80% nas classes-alvo.
- Testes unitários com Mockito são rápidos e não precisam do Spring Context.
@WebMvcTesttesta controllers de forma isolada, sem banco de dados.@DataJpaTesttesta repositórios com banco H2 sem inicializar a aplicação toda.MockMvcpermite testar endpoints HTTP com assertions em status, headers e body JSON.
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).
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
@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
@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
@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);
}
}
Objetivo
Enviar e-mail de boas-vindas ao novo aluno de forma assíncrona.
Tarefa
- Anotar o método
enviarConfirmacaocom@Async. - Habilitar
@EnableAsyncna aplicação. - No profile
dev, usarEmailServiceFakeque 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.
MultipartFilerecebe arquivos enviados via form-data; valide tipo e tamanho antes de processar.JavaMailSenderintegra com qualquer servidor SMTP com configuração simples noapplication.yml.@Scheduledagenda tarefas com cron ou intervalo fixo — ideal para relatórios e limpeza de dados.@Asyncexecuta métodos em threads separadas, desacoplando operações lentas do fluxo principal.
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.
@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+)
@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):
spring:
http:
client:
connect-timeout: 2s
read-timeout: 5s
Com RestTemplate, use RestTemplateBuilder.setConnectTimeout / setReadTimeout como no exemplo anterior.
WebClient — Cliente reativo (Spring WebFlux)
@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
// 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ística | RestTemplate | RestClient | WebClient | OpenFeign |
|---|---|---|---|---|
| Status | Manutenção | Recomendado (novo) | Ativo | Ativo (Cloud) |
| API | Métodos por verbo HTTP | Fluente (retrieve()) | Reativa (Mono/Flux) | Interface + anotações |
| Bloqueante? | Sim | Sim | Não (pode block()) | Sim |
| Boilerplate | Médio | Médio | Médio | Baixo |
| Quando usar | Legado / leitura | APIs síncronas novas | Stack reativa | Muitos clientes declarativos |
Objetivo
Integrar o EduSpring com a API ViaCEP e comparar duas APIs de cliente.
Tarefa
- Implementar
ViaCepRestClientcomRestClientapontando parahttps://viacep.com.bre URI/ws/{cep}/json/. - Implementar
ViaCepRestTemplateequivalente comRestTemplateBuilder(mesma URL). - Expor
GET /enderecos/{cep}escolhendo o backend via propriedadeapp.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.
RestTemplatepermanece em muitos projetos; configure timeouts comRestTemplateBuilder.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.WebClientintegra à stack reativa;OpenFeignreduz boilerplate quando há Spring Cloud.- Modele erros HTTP do serviço externo com exceções de domínio — facilita Circuit Breaker e testes.
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
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
<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>
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
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.
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:
resilience4j:
bulkhead:
instances:
servico-cursos:
maxConcurrentCalls: 8
maxWaitDuration: 200ms
@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.
Objetivo
Reproduzir o ciclo fechado → aberto → meio-aberto com Docker e Actuator.
Tarefa
- Subir
cursos-apiematriculas-apicom Compose; confirmarGET /matriculas/validar-curso/1(ou endpoint equivalente que chame o cliente) retorna 200. - Registrar baseline em
GET /actuator/circuitbreakers(estadoCLOSED). - Parar só o container
cursos-api; disparar 30 requisições concorrentes (ex.:hey -n 30 -c 10ou script) e observar transição paraOPENemcircuitbreakerse eventos emcircuitbreakerevents. - Medir latência: com CB aberto, tempo médio deve cair para milissegundos (fallback), não segundos.
- Subir novamente o
cursos-api, aguardarwaitDurationInOpenStatee validar retorno aHALF_OPEN/CLOSEDapó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.
- 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) eminimumNumberOfCallsdefinem quando a taxa de falha “vale” estatisticamente. @Retryem exceções de rede deve ser combinado com limites — retries infinitos só adiam o OPEN.@Bulkheadprotege 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.
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
| Conceito | Analogia | Papel |
|---|---|---|
| Producer | Remetente | Envia mensagens para o Exchange |
| Exchange | Agência de correios | Recebe mensagens e roteia para filas |
| Queue | Caixa postal | Armazena mensagens até o consumidor processar |
| Consumer | Destinatário | Lê e processa mensagens da fila |
| Binding | Endereço | Regra que conecta Exchange a uma Queue |
| Routing Key | CEP | Critério de roteamento da mensagem |
Configuração com Spring AMQP
@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
@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
@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());
}
}
}
}
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.
Objetivo
Implementar tratamento de mensagens que falham repetidamente.
Tarefa
- Configurar a DLQ
matriculas.dlqconforme 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-dlqque 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.
- 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.
@RabbitListenercom 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.
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
# 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
# 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
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
# 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
Objetivo
Colocar o EduSpring em produção, acessível via URL pública.
Tarefa
- Criar o
Dockerfilecom multi-stage build. - Testar localmente com
docker compose upe verificar todos os endpoints. - Fazer deploy no Railway (ou Render) com PostgreSQL como serviço.
- Verificar que
https://seu-app.up.railway.app/actuator/healthretornaUP.
Critério de aceite
- Aplicação acessível via URL pública HTTPS.
/actuator/healthretorna{"status":"UP"}.- Swagger UI acessível em
/swagger-ui.html. - Criar usuário via
/auth/registrare fazer login via/auth/loginfunciona.
- 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.
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.
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.rootelogging.level.com.dfcodenoapplication.ymlajustam 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 emWARN/ERRORcom stack trace antes de montar oProblemDetail. - Último recurso (JVM): em uma
@Configuration, registre um handler para threads que morrem sem tratamento — útil para workers ou código legado.
@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));
}
}
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:
- Criar conta e obter o customer token (identificador da sua conta no endpoint de ingestão).
- Definir o token como variável de ambiente (ex.:
LOGGLY_TOKEN) — nunca commitar no repositório. - 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):
<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:
# 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.
Objetivo
Padronizar logs no EduSpring com níveis corretos e profile prod menos barulhento que dev.
Tarefa
- Adicionar
Loggerestático emCursoServiceeMatriculaServicecomdebugem operações de leitura einfoem mutações. - Criar
logback-spring.xmlcom<springProfile name="dev">emDEBUGpara o pacote da aplicação. - Registrar (ou simular) envio de um evento
ERRORcom stack trace para o painel Loggly ou equivalente.
- 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.
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.
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.).
@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.
@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):
@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
@Scheduledsão interceptados por proxy; chamarthis.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
TaskSchedulercom mais threads viaSchedulingConfigurer(documentação Spring). - Tarefas pesadas: preferir fila (RabbitMQ, cap. 24) para não bloquear o scheduler único.
Objetivo
Consolidar cron + fixedDelay no EduSpring.
Tarefa
- Criar
@Component RelatorioSchedulercom@Scheduled(cron = "0 0 8 * * *", zone = "America/Sao_Paulo")que apenas registreINFO“relatório diário disparado”. - Adicionar segundo método com
fixedDelayde 2 minutos (ambientedevapenas) 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.
@EnableSchedulingativa o registro de triggers noTaskScheduler.fixedDelaymede a partir do fim;fixedRatea partir do início; cron usa seis campos no Spring.zonealinha horário de negócio ao fuso correto.- Proxies do Spring exigem cuidado com chamadas internas e dimensionamento do pool para jobs longos.
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.
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:
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.
Objetivo
Enviar e-mail HTML de confirmação de matrícula em prod e apenas logar em dev.
Tarefa
- Criar template Thymeleaf
matricula-confirmada.htmlcom nome do aluno e do curso. - Implementar
EmailServiceProdcomJavaMailSender+ HTML renderizado. - Implementar
EmailServiceDevque escreve o HTML no logINFO.
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.
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.
package br.com.dfcode.eduspring.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig { }
<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
allEntriesou chaves específicas.
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.
spring:
data:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
time-to-live: 600000
cache-names: cursos, alunos
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.
Objetivo
Cachear a listagem de cursos ativos e invalidar ao cadastrar um novo curso.
Tarefa
- Criar cache
cursosAtivosem um métodolistarAtivos(). - Evict ao salvar novo
Curso. - Medir (logs ou Actuator) uma segunda chamada sem hit no banco.
@EnableCachingativa o processamento das anotações de cache.@Cacheable/@CacheEvictmodelam leitura e invalidação.- O Boot escolhe o
CacheManagerpelo 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.