1. Setup e Estrutura de Projeto
@SpringBootApplication é o ponto de entrada de toda aplicação Spring Boot. Ela combina três anotações em uma: @Configuration (classe de configuração), @ComponentScan (varre o pacote atual e sub-pacotes por beans) e @EnableAutoConfiguration (ativa a mágica do auto-configure baseado no classpath).
@SpringBootApplication
public class PedidosApplication {
public static void main(String[] args) {
SpringApplication.run(PedidosApplication.class, args);
}
}Estrutura de projeto recomendada (pacotes por feature, não por camada):
src/main/java/com/empresa/pedidos/
├── PedidosApplication.java
├── pedido/
│ ├── PedidoController.java
│ ├── PedidoService.java
│ ├── PedidoRepository.java
│ ├── Pedido.java // @Entity
│ ├── PedidoRequest.java // record DTO de entrada
│ └── PedidoResponse.java // record DTO de saída
├── pagamento/
│ ├── PagamentoController.java
│ └── ...
└── shared/
├── GlobalExceptionHandler.java
└── SecurityConfig.javaMaven — dependência mínima:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>Gradle (build.gradle.kts):
plugins {
id("org.springframework.boot") version "3.3.4"
id("io.spring.dependency-management") version "1.1.6"
kotlin("jvm") version "1.9.25"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}2. application.yml — Propriedades Comuns
O application.yml é o arquivo de configuração principal. Use variáveis de ambiente com ${VAR_NAME} e defina valores padrão com ${VAR_NAME:valor_padrao}. A hierarquia de configuração, do menor para o maior precedência: defaults do Spring Boot, application.yml, application-{profile}.yml, variáveis de ambiente, argumentos de linha de comando.
spring:
application:
name: pedidos-service
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:pedidos}
username: ${DB_USER:dev}
password: ${DB_PASS:dev}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate # nunca use create/update em produção
show-sql: false
properties:
hibernate:
format_sql: true
default_schema: public
jdbc:
batch_size: 50 # inserções em lote
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=300s
server:
port: ${PORT:8080}
shutdown: graceful # aguarda requests em andamento antes de parar
compression:
enabled: true
min-response-size: 1024
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
logging:
level:
com.empresa.pedidos: DEBUG
org.hibernate.SQL: DEBUG3. @ConfigurationProperties — Configuração Tipada
Em vez de @Value espalhado, agrupe propriedades relacionadas em uma classe ou record tipado. O @ConfigurationProperties faz o binding automático e o @Validated aplica Bean Validation nas propriedades.
// Classe de propriedades tipada — preferir record (Java 16+)
@ConfigurationProperties(prefix = "pedidos.pagamento")
@Validated
public record PagamentoProperties(
@NotBlank String gatewayUrl,
@NotBlank String apiKey,
@Min(1000) @Max(30000) int timeoutMs,
@Min(1) int maxRetries,
boolean sandboxMode
) {}# application.yml
pedidos:
pagamento:
gateway-url: https://gateway.pagamento.com/v2
api-key: ${PAYMENT_API_KEY}
timeout-ms: 5000
max-retries: 3
sandbox-mode: false// Habilitar e injetar
@SpringBootApplication
@EnableConfigurationProperties(PagamentoProperties.class)
public class PedidosApplication { ... }
@Service
public class PagamentoService {
private final PagamentoProperties props;
public PagamentoService(PagamentoProperties props) { this.props = props; }
public void processar(Pagamento p) {
if (props.sandboxMode()) {
log.info("Sandbox: simulando pagamento para {}", p.id());
return;
}
// usa props.gatewayUrl(), props.apiKey() etc.
}
}4. Profiles — Configuração por Ambiente
Profiles permitem ativar beans e configurações específicos por ambiente (dev, test, prod). Use @Profile nos beans e arquivos application-{profile}.yml para sobrescrever propriedades.
# application-dev.yml
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop # recria o schema a cada reinício em dev
datasource:
url: jdbc:h2:mem:devdb # banco em memória em dev
logging:
level:
com.empresa: TRACE# application-prod.yml
spring:
datasource:
url: jdbc:postgresql://prod-db:5432/pedidos
jpa:
show-sql: false
logging:
level:
root: WARN// Bean ativo apenas em dev — ex.: dados de seed
@Component
@Profile("dev")
public class DevDataLoader implements CommandLineRunner {
private final PedidoRepository repo;
public DevDataLoader(PedidoRepository repo) { this.repo = repo; }
@Override
public void run(String... args) {
repo.saveAll(PedidoFixtures.sample());
log.info("Dev data carregado: {} pedidos", repo.count());
}
}
// Ativar perfil em testes
@SpringBootTest
@ActiveProfiles("test")
class PedidoServiceTest { ... }5. @RestController Completo
Cada método de um controller mapeia um endpoint HTTP. Use injeção por construtor (sem @Autowired), que é mais testável. ResponseEntity dá controle total sobre status HTTP e headers.
@RestController
@RequestMapping("/api/pedidos")
@Tag(name = "Pedidos", description = "CRUD de pedidos") // OpenAPI
public class PedidoController {
private final PedidoService service;
public PedidoController(PedidoService service) {
this.service = service;
}
// GET com paginação e filtro
@GetMapping
public ResponseEntity<Page<PedidoResponse>> listar(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String status,
Pageable pageable) {
return ResponseEntity.ok(service.listar(status, pageable));
}
// GET por ID
@GetMapping("/{id}")
public ResponseEntity<PedidoResponse> buscarPorId(@PathVariable UUID id) {
return ResponseEntity.ok(service.buscarPorId(id));
}
// POST — cria recurso e retorna 201 com Location header
@PostMapping
public ResponseEntity<PedidoResponse> criar(@RequestBody @Valid CriarPedidoRequest req) {
PedidoResponse criado = service.criar(req);
URI location = URI.create("/api/pedidos/" + criado.id());
return ResponseEntity.created(location).body(criado);
}
// PUT — substitui o recurso completo
@PutMapping("/{id}")
public ResponseEntity<PedidoResponse> atualizar(
@PathVariable UUID id,
@RequestBody @Valid AtualizarPedidoRequest req) {
return ResponseEntity.ok(service.atualizar(id, req));
}
// PATCH — atualização parcial
@PatchMapping("/{id}/status")
public ResponseEntity<Void> atualizarStatus(
@PathVariable UUID id,
@RequestBody @Valid AtualizarStatusRequest req) {
service.atualizarStatus(id, req.status());
return ResponseEntity.noContent().build(); // 204
}
// DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletar(@PathVariable UUID id) {
service.deletar(id);
return ResponseEntity.noContent().build(); // 204
}
// Busca com múltiplos parâmetros
@GetMapping("/busca")
public ResponseEntity<List<PedidoResponse>> buscar(
@RequestParam String clienteId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate de,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate ate) {
return ResponseEntity.ok(service.buscarPorClienteEPeriodo(clienteId, de, ate));
}
}6. Bean Validation — Validação de Entrada
As anotações da Jakarta Validation marcam as restrições nos DTOs. O @Valid no parâmetro do controller dispara a validação; em caso de falha, lança MethodArgumentNotValidException. Use @Valid em objetos aninhados para validação recursiva.
public record CriarPedidoRequest(
@NotBlank(message = "clienteId é obrigatório")
String clienteId,
@NotEmpty
@Size(min = 1, max = 50, message = "Pedido deve ter entre 1 e 50 itens")
List<@Valid ItemPedidoRequest> itens,
@Email(message = "Email de contato inválido")
String emailContato,
@DecimalMin(value = "0.0", inclusive = true)
@DecimalMax(value = "100.0")
BigDecimal descontoPercent,
@Future(message = "Data de entrega deve ser no futuro")
LocalDate dataEntrega
) {}
public record ItemPedidoRequest(
@NotBlank String sku,
@Min(1) @Max(100) int quantidade,
@NotNull @Positive BigDecimal precoUnitario
) {}Constraint customizada:
// 1. Definir a anotação
@Documented
@Constraint(validatedBy = CpfCnpjValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CpfCnpj {
String message() default "CPF ou CNPJ inválido";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. Implementar o validador
public class CpfCnpjValidator implements ConstraintValidator<CpfCnpj, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true; // use @NotNull separado
String digits = value.replaceAll("[^0-9]", "");
return digits.length() == 11 ? validarCpf(digits) : validarCnpj(digits);
}
}
// 3. Usar
public record ClienteRequest(
@NotBlank @CpfCnpj String documento
) {}7. ResponseEntity e Respostas HTTP
ResponseEntity<T> encapsula o status, os headers e o corpo. Use os métodos estáticos do builder para expressar intenção claramente.
// Respostas mais comuns
ResponseEntity.ok(body) // 200 com corpo
ResponseEntity.ok().build() // 200 sem corpo
ResponseEntity.created(location).body(body) // 201 com Location
ResponseEntity.accepted().body(body) // 202 Accepted (operação assíncrona)
ResponseEntity.noContent().build() // 204 sem corpo
ResponseEntity.notFound().build() // 404 sem corpo
ResponseEntity.badRequest().body(problemDetail) // 400 com ProblemDetail
// Customizando headers
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(total))
.header("X-Correlation-Id", correlationId)
.contentType(MediaType.APPLICATION_JSON)
.body(lista);
// Stream de arquivo
@GetMapping("/relatorio/{id}/download")
public ResponseEntity<Resource> downloadRelatorio(@PathVariable UUID id) {
Resource arquivo = service.gerarRelatorio(id);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"relatorio-" + id + ".pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(arquivo);
}8. @RestControllerAdvice — Tratamento Global de Erros
@RestControllerAdvice intercepta exceções de qualquer controller. A partir do Spring 6 (Boot 3+), use ProblemDetail que implementa o padrão RFC 9457 (antigo RFC 7807).
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 404 — recurso não encontrado
@ExceptionHandler(RecursoNaoEncontradoException.class)
public ProblemDetail handleNotFound(RecursoNaoEncontradoException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Recurso não encontrado");
pd.setProperty("recursoId", ex.getRecursoId());
return pd;
}
// 422 — validação de entrada
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.UNPROCESSABLE_ENTITY);
pd.setTitle("Dados de entrada inválidos");
List<String> erros = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
pd.setProperty("erros", erros);
return pd;
}
// 409 — conflito de estado de negócio
@ExceptionHandler(ConflictoDeNegocioException.class)
public ProblemDetail handleConflict(ConflictoDeNegocioException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
pd.setTitle("Conflito de negócio");
return pd;
}
// 400 — parâmetro de URL inválido (ex: UUID mal formatado)
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ProblemDetail handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
String msg = "Parâmetro '%s' com valor '%s' é inválido".formatted(ex.getName(), ex.getValue());
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, msg);
}
// 500 — fallback genérico
@ExceptionHandler(Exception.class)
public ProblemDetail handleGeneric(Exception ex, HttpServletRequest req) {
log.error("Erro não tratado em {}", req.getRequestURI(), ex);
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Erro interno do servidor");
}
}9. Spring Data JPA — Entities e Repositórios
@Entity mapeia a classe para uma tabela. JpaRepository fornece CRUD, paginação e ordenação prontos. Query methods derivam SQL do nome do método; @Query permite JPQL ou SQL nativo para casos mais complexos.
@Entity
@Table(name = "pedidos",
indexes = @Index(name = "idx_pedidos_cliente_id", columnList = "cliente_id"))
@EntityListeners(AuditingEntityListener.class) // auditoria automática
public class Pedido {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "cliente_id", nullable = false)
private String clienteId;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private StatusPedido status;
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<ItemPedido> itens = new ArrayList<>();
@Column(precision = 12, scale = 2)
private BigDecimal valorTotal;
@CreatedDate
@Column(updatable = false)
private Instant criadoEm;
@LastModifiedDate
private Instant atualizadoEm;
// getters/setters ou use Lombok @Getter @Setter
}
@Entity
@Table(name = "itens_pedido")
public class ItemPedido {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pedido_id", nullable = false)
private Pedido pedido;
@Column(nullable = false)
private String sku;
@Min(1)
private int quantidade;
@Column(precision = 10, scale = 2)
private BigDecimal precoUnitario;
}public interface PedidoRepository extends JpaRepository<Pedido, UUID> {
// Derivação de nome de método — Spring gera o JPQL automaticamente
List<Pedido> findByClienteId(String clienteId);
List<Pedido> findByStatusAndClienteId(StatusPedido status, String clienteId);
Page<Pedido> findByStatus(StatusPedido status, Pageable pageable);
boolean existsByClienteIdAndStatus(String clienteId, StatusPedido status);
long countByStatus(StatusPedido status);
// JPQL — para queries com joins ou agregações
@Query("SELECT p FROM Pedido p JOIN FETCH p.itens WHERE p.id = :id")
Optional<Pedido> findByIdComItens(@Param("id") UUID id);
// JPQL com projeção — retorna apenas os campos necessários
@Query("SELECT new com.empresa.pedidos.PedidoResumo(p.id, p.status, p.valorTotal) " +
"FROM Pedido p WHERE p.clienteId = :clienteId ORDER BY p.criadoEm DESC")
List<PedidoResumo> findResumosByClienteId(@Param("clienteId") String clienteId);
// SQL nativo — quando JPQL não é suficiente
@Query(value = """
SELECT p.* FROM pedidos p
WHERE p.criado_em BETWEEN :de AND :ate
AND p.status = :status
ORDER BY p.valor_total DESC
LIMIT :limite
""", nativeQuery = true)
List<Pedido> findTopPedidosPorPeriodo(
@Param("de") Instant de,
@Param("ate") Instant ate,
@Param("status") String status,
@Param("limite") int limite
);
// Atualização em massa com @Modifying
@Modifying
@Query("UPDATE Pedido p SET p.status = :novoStatus WHERE p.status = :statusAtual AND p.criadoEm < :antes")
int cancelarPedidosAbandonados(
@Param("statusAtual") StatusPedido statusAtual,
@Param("novoStatus") StatusPedido novoStatus,
@Param("antes") Instant antes
);
}10. Relacionamentos JPA — Fetch Strategies e N+1
O problema N+1 ocorre quando você carrega uma lista de N entidades e, para cada uma, dispara uma query adicional para buscar a associação. A solução é usar JOIN FETCH na query ou @EntityGraph.
// ERRADO — LAZY por padrão, cada pedido dispara uma query para buscar itens
List<Pedido> pedidos = pedidoRepo.findAll();
for (Pedido p : pedidos) {
p.getItens().size(); // dispara SELECT para cada pedido! N+1!
}
// CERTO — JOIN FETCH carrega tudo em uma única query
@Query("SELECT DISTINCT p FROM Pedido p JOIN FETCH p.itens WHERE p.clienteId = :clienteId")
List<Pedido> findByClienteIdComItens(@Param("clienteId") String clienteId);
// CERTO — @EntityGraph para reutilizar sem escrever JPQL
@EntityGraph(attributePaths = {"itens", "pagamento"})
List<Pedido> findByClienteId(String clienteId);Mapeamentos completos:
// @OneToMany — Pedido tem vários Itens
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemPedido> itens = new ArrayList<>();
// @ManyToOne — Item pertence a um Pedido
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pedido_id")
private Pedido pedido;
// @ManyToMany — Pedido tem vários Cupons e vice-versa
@ManyToMany
@JoinTable(name = "pedidos_cupons",
joinColumns = @JoinColumn(name = "pedido_id"),
inverseJoinColumns = @JoinColumn(name = "cupom_id"))
private Set<Cupom> cupons = new HashSet<>();
// @OneToOne
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "pagamento_id", unique = true)
private Pagamento pagamento;11. @Transactional — Propagação e Isolamento
@Transactional demarca uma transação de banco. Por padrão, faz rollback automático em RuntimeException. Cuidado: só funciona em chamadas vindas de fora da classe (proxy Spring) — uma chamada interna não ativa a transação.
@Service
@Transactional(readOnly = true) // padrão read-only para queries (melhor performance)
public class PedidoService {
// Sobrescreve para operações de escrita
@Transactional
public PedidoResponse criar(CriarPedidoRequest req) {
Pedido pedido = new Pedido(req);
pedidoRepo.save(pedido);
eventPublisher.publishEvent(new PedidoCriadoEvent(pedido.getId()));
return PedidoResponse.from(pedido);
}
// Rollback em checked exception — padrão é apenas RuntimeException
@Transactional(rollbackFor = PagamentoException.class)
public void confirmarPagamento(UUID pedidoId, DadosPagamento dados) throws PagamentoException {
Pedido pedido = pedidoRepo.findById(pedidoId).orElseThrow();
Pagamento pag = pagamentoGateway.cobrar(dados); // throws PagamentoException
pedido.marcarComoPago(pag.getId());
pedidoRepo.save(pedido);
}
// REQUIRES_NEW — abre nova transação, não interfere na transação pai
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrarTentativaPagamento(UUID pedidoId, String resultado) {
// se isso falhar, não faz rollback da transação do chamador
auditRepo.save(new AuditLog(pedidoId, resultado));
}
}Propagações mais usadas:
| Propagação | Comportamento |
|---|---|
REQUIRED (padrão) | Usa transação existente ou cria uma nova |
REQUIRES_NEW | Sempre cria nova transação, suspende a existente |
SUPPORTS | Usa transação se existir, senão executa sem |
NOT_SUPPORTED | Sempre executa sem transação, suspende a existente |
NEVER | Lança exceção se houver transação ativa |
MANDATORY | Lança exceção se não houver transação ativa |
Isolamentos:
| Isolamento | Protege contra |
|---|---|
READ_UNCOMMITTED | Nada (mais rápido, menos seguro) |
READ_COMMITTED (padrão PostgreSQL) | Dirty reads |
REPEATABLE_READ | Dirty reads + Non-repeatable reads |
SERIALIZABLE | Tudo (mais lento, mais seguro) |
12. Spring Security — SecurityFilterChain e JWT
A partir do Spring Security 6 (Boot 3+), a configuração usa SecurityFilterChain como bean. Não há mais WebSecurityConfigurerAdapter.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // habilita @PreAuthorize, @PostAuthorize etc.
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
public SecurityConfig(JwtAuthFilter jwtAuthFilter, UserDetailsService uds) {
this.jwtAuthFilter = jwtAuthFilter;
this.userDetailsService = uds;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // desabilita CSRF para APIs stateless
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/api/produtos/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public AuthenticationManager authManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}Filtro JWT:
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
String authHeader = req.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(req, res);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, user)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(req, res);
}
}UserDetails customizado:
@Entity
@Table(name = "usuarios")
public class Usuario implements UserDetails {
@Id @GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String email;
private String senha;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "usuario_roles")
@Enumerated(EnumType.STRING)
private Set<Role> roles;
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r -> new SimpleGrantedAuthority("ROLE_" + r.name())).toList();
}
@Override public String getPassword() { return senha; }
@Override public String getUsername() { return email; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired(){ return true; }
@Override public boolean isEnabled() { return true; }
}@PreAuthorize nos métodos:
@Service
public class PedidoService {
@PreAuthorize("hasRole('ADMIN') or #clienteId == authentication.name")
public List<PedidoResponse> listarPorCliente(String clienteId) { ... }
@PreAuthorize("hasRole('ADMIN')")
public void cancelar(UUID id) { ... }
@PostAuthorize("returnObject.clienteId == authentication.name or hasRole('ADMIN')")
public PedidoResponse buscarPorId(UUID id) { ... }
}13. Spring Security — OAuth2 Resource Server com JWT
Quando há um Authorization Server externo (Keycloak, Auth0, Okta), configure o Spring Boot apenas como Resource Server — ele valida os tokens mas não os emite.
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://keycloak.empresa.com/realms/pedidos
# ou: jwk-set-uri: https://keycloak.empresa.com/realms/pedidos/protocol/openid-connect/certs@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter())))
.build();
}
// Extrai roles do claim aninhado "realm_access.roles" do Keycloak
// ATENÇÃO: JwtGrantedAuthoritiesConverter só suporta claims de nível raiz.
// Para o Keycloak (claim aninhado), é necessário um Converter customizado.
@Bean
public JwtAuthenticationConverter jwtConverter() {
// Converter customizado para extrair roles aninhadas do Keycloak
Converter<Jwt, Collection<GrantedAuthority>> realmRolesConverter = jwt -> {
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess == null) return List.of();
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.getOrDefault("roles", List.of());
return roles.stream()
.map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role))
.toList();
};
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(realmRolesConverter);
return converter;
}
// Alternativa: usar claim plano "scp" ou "scope" (padrão OAuth2/OIDC)
// — JwtGrantedAuthoritiesConverter funciona diretamente aqui
@Bean
public JwtAuthenticationConverter scopeBasedConverter() {
JwtGrantedAuthoritiesConverter grantedConverter = new JwtGrantedAuthoritiesConverter();
grantedConverter.setAuthoritiesClaimName("scp"); // claim plano — funciona
grantedConverter.setAuthorityPrefix("SCOPE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(grantedConverter);
return converter;
}
}14. Testing — @WebMvcTest, @DataJpaTest, @SpringBootTest
O Spring Boot oferece três camadas de teste com contextos progressivamente maiores.
// @WebMvcTest — testa apenas a camada de controller; mais rápido
@WebMvcTest(PedidoController.class)
class PedidoControllerTest {
@Autowired
private MockMvcTester mvc; // MockMvcTester (Boot 3.4+) ou @Autowired MockMvc
@MockitoBean // substitui @MockBean no Boot 3.4+
private PedidoService service;
@Test
void deveRetornar201AoCriarPedido() {
var req = new CriarPedidoRequest("cliente-1", List.of(new ItemPedidoRequest("SKU-1", 2, new BigDecimal("50.00"))), null, null, null);
var resp = new PedidoResponse(UUID.randomUUID(), "cliente-1", StatusPedido.ABERTO, new BigDecimal("100.00"));
given(service.criar(any())).willReturn(resp);
assertThat(mvc.post().uri("/api/pedidos")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"clienteId":"cliente-1","itens":[{"sku":"SKU-1","quantidade":2,"precoUnitario":50.00}]}"""))
.hasStatus(HttpStatus.CREATED)
.bodyJson().extractingPath("$.status").isEqualTo("ABERTO");
}
@Test
void deveRetornar422ComCampoInvalido() {
assertThat(mvc.post().uri("/api/pedidos")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"clienteId":"","itens":[]}"""))
.hasStatus(HttpStatus.UNPROCESSABLE_ENTITY);
}
}// @DataJpaTest — testa apenas a camada JPA; usa H2 em memória por padrão
@DataJpaTest
@ActiveProfiles("test")
class PedidoRepositoryTest {
@Autowired
private PedidoRepository repo;
@Autowired
private TestEntityManager em;
@Test
void deveEncontrarPedidosPorCliente() {
var pedido = new Pedido("cliente-42", StatusPedido.ABERTO);
em.persistAndFlush(pedido);
List<Pedido> resultado = repo.findByClienteId("cliente-42");
assertThat(resultado).hasSize(1);
assertThat(resultado.get(0).getClienteId()).isEqualTo("cliente-42");
}
}// Testcontainers — banco real em container Docker
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
class PedidoIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private PedidoService service;
@Test
void deveProcessarFluxoCompletoDeCompra() {
var req = CriarPedidoRequest.builder()
.clienteId("cliente-1")
.itens(List.of(new ItemPedidoRequest("SKU-ABC", 3, new BigDecimal("29.90"))))
.build();
PedidoResponse resp = service.criar(req);
assertThat(resp.status()).isEqualTo(StatusPedido.ABERTO);
assertThat(resp.valorTotal()).isEqualByComparingTo("89.70");
}
}15. Spring Cache — @Cacheable, @CacheEvict, @CachePut
Cache declarativo com anotações. Suporta vários backends: Caffeine (in-process), Redis (distribuído), EhCache etc. A chave é gerada a partir dos parâmetros por padrão, mas pode ser customizada via SpEL.
@Service
@CacheConfig(cacheNames = "pedidos") // nome do cache padrão para a classe
public class PedidoService {
// Cache na leitura — executa o método apenas se não houver cache
@Cacheable(key = "#id", unless = "#result == null")
public PedidoResponse buscarPorId(UUID id) {
return pedidoRepo.findById(id)
.map(PedidoResponse::from)
.orElseThrow(() -> new RecursoNaoEncontradoException(id));
}
// Atualiza o cache após salvar
@CachePut(key = "#result.id")
@Transactional
public PedidoResponse atualizar(UUID id, AtualizarPedidoRequest req) {
Pedido pedido = pedidoRepo.findById(id).orElseThrow();
pedido.atualizar(req);
return PedidoResponse.from(pedidoRepo.save(pedido));
}
// Invalida o cache ao deletar
@CacheEvict(key = "#id")
@Transactional
public void deletar(UUID id) {
pedidoRepo.deleteById(id);
}
// Invalida todo o cache de uma vez
@CacheEvict(allEntries = true)
public void invalidarTodosOsPedidos() {}
}# Caffeine (in-process — recomendado para instância única)
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=5000,expireAfterWrite=600s,recordStats
# Redis (distribuído — para múltiplas instâncias)
spring:
cache:
type: redis
data:
redis:
host: ${REDIS_HOST:localhost}
port: 637916. Spring Events — ApplicationEvent e @TransactionalEventListener
Events desacoplam o publicador do consumidor dentro da mesma JVM. @TransactionalEventListener garante que o handler só executa após o commit da transação, evitando processar eventos de transações que possam fazer rollback.
// 1. Definir o evento (record Java — imutável por padrão)
public record PedidoCriadoEvent(UUID pedidoId, String clienteId, BigDecimal valorTotal) {}
// 2. Publicar o evento no serviço
@Service
@RequiredArgsConstructor
public class PedidoService {
private final ApplicationEventPublisher eventPublisher;
private final PedidoRepository repo;
@Transactional
public PedidoResponse criar(CriarPedidoRequest req) {
Pedido pedido = repo.save(new Pedido(req));
// O evento só será processado após o commit da transação
eventPublisher.publishEvent(new PedidoCriadoEvent(pedido.getId(), pedido.getClienteId(), pedido.getValorTotal()));
return PedidoResponse.from(pedido);
}
}
// 3. Consumir o evento
@Component
@Slf4j
public class NotificacaoListener {
private final EmailService emailService;
private final EstoqueService estoqueService;
// Executa APÓS o commit — seguro para side effects
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onPedidoCriado(PedidoCriadoEvent event) {
emailService.enviarConfirmacao(event.clienteId(), event.pedidoId());
log.info("Email de confirmação enviado para pedido {}", event.pedidoId());
}
// Outro listener para o mesmo evento — executa de forma independente
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async // em thread separada para não bloquear
public void atualizarEstoque(PedidoCriadoEvent event) {
estoqueService.reservar(event.pedidoId());
}
}17. @Scheduled — Tarefas Agendadas
@EnableScheduling habilita o agendamento. @Scheduled marca o método que será executado. Use cron para horários precisos, fixedDelay quando quer aguardar o fim da execução anterior, e fixedRate para intervalos fixos independentes.
@SpringBootApplication
@EnableScheduling
public class PedidosApplication { ... }
@Component
@Slf4j
public class PedidoScheduler {
private final PedidoService service;
private final RelatorioService relatorioService;
// Cron — executa todo dia às 02:00
@Scheduled(cron = "0 0 2 * * *")
public void cancelarPedidosAbandonados() {
int cancelados = service.cancelarAbandonados(Duration.ofHours(48));
log.info("Pedidos cancelados por abandono: {}", cancelados);
}
// fixedDelay — aguarda 30s após o fim da execução anterior
@Scheduled(fixedDelay = 30_000, initialDelay = 10_000)
public void sincronizarStatusPagamento() {
service.sincronizarPagamentosPendentes();
}
// fixedRate — executa a cada 5 minutos, independente de quanto durou
@Scheduled(fixedRate = 300_000)
public void atualizarMetricas() {
relatorioService.atualizarCache();
}
// Cron com zona de horário — importante para servidores em UTC
@Scheduled(cron = "0 0 8 * * MON-FRI", zone = "America/Sao_Paulo")
public void enviarRelatorioDiario() {
relatorioService.enviarEmailDiario();
}
}Referência rápida de expressão cron (6 campos):
Segundo Minuto Hora DiaMes Mes DiaSemana
0 0 2 * * * → todo dia às 02:00:00
0 */15 * * * * → a cada 15 minutos
0 0 8-18 * * MON-FRI → horário comercial, de hora em hora
0 0 0 1 * * → primeiro dia do mês à meia-noite18. Spring Actuator — Monitoramento e Health Indicators
O Actuator expõe endpoints HTTP para monitorar e gerenciar a aplicação em produção. Integra-se nativamente com Prometheus/Grafana via Micrometer.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers,env
base-path: /internal # prefixo para proteger com firewall/gateway
endpoint:
health:
show-details: when-authorized
show-components: when-authorized
info:
env:
enabled: true
metrics:
tags:
application: ${spring.application.name} # tag em todas as métricas
environment: ${spring.profiles.active:default}Health Indicator customizado:
@Component
public class GatewayPagamentoHealthIndicator implements HealthIndicator {
private final PagamentoGatewayClient client;
@Override
public Health health() {
try {
PingResponse ping = client.ping();
if (ping.isOk()) {
return Health.up()
.withDetail("latencyMs", ping.getLatencyMs())
.withDetail("version", ping.getVersion())
.build();
}
return Health.down()
.withDetail("motivo", "Gateway retornou status " + ping.getStatus())
.build();
} catch (Exception ex) {
return Health.down(ex)
.withDetail("motivo", "Timeout ou erro de conexão")
.build();
}
}
}Métricas customizadas com Micrometer:
@Service
@RequiredArgsConstructor
public class PedidoService {
private final MeterRegistry meterRegistry;
@Transactional
public PedidoResponse criar(CriarPedidoRequest req) {
PedidoResponse resp = // ...
// Contador com tags
meterRegistry.counter("pedidos.criados",
"status", "sucesso",
"origem", req.origem()).increment();
// Timer — mede duração
Timer timer = meterRegistry.timer("pedidos.processamento",
"tipo", req.tipo().name());
// use timer.record() em volta do processamento
return resp;
}
}19. WebFlux — Programação Reativa
WebFlux é a alternativa reativa ao Spring MVC. Usa Reactor (Mono/Flux) e funciona com servidores não-bloqueantes (Netty por padrão). Ideal quando há muita I/O concorrente.
@RestController
@RequestMapping("/api/pedidos")
public class PedidoReativoController {
private final PedidoReativoService service;
@GetMapping("/{id}")
public Mono<PedidoResponse> buscar(@PathVariable UUID id) {
return service.buscar(id)
.switchIfEmpty(Mono.error(new RecursoNaoEncontradoException(id)));
}
@GetMapping
public Flux<PedidoResponse> listar(@RequestParam(defaultValue = "0") int page) {
return service.listar(page);
}
@PostMapping
public Mono<ResponseEntity<PedidoResponse>> criar(@RequestBody @Valid Mono<CriarPedidoRequest> req) {
return req.flatMap(service::criar)
.map(criado -> ResponseEntity.created(URI.create("/api/pedidos/" + criado.id())).body(criado));
}
}Operadores essenciais do Reactor:
// map — transformação síncrona (sem IO)
Mono<PedidoResponse> resp = mono.map(PedidoResponse::from);
// flatMap — transformação assíncrona (retorna outro Mono/Flux)
Mono<Pagamento> pag = pedidoMono.flatMap(p -> pagamentoService.cobrar(p));
// zip / zipWith — combina dois Monos em paralelo
Mono<DetalhePedido> detalhe = Mono.zip(
pedidoRepo.findById(id),
clienteRepo.findById(clienteId),
(pedido, cliente) -> new DetalhePedido(pedido, cliente)
);
// onErrorResume — fallback quando falha
Mono<Pedido> seguro = pedidoRepo.findById(id)
.onErrorResume(TimeoutException.class, ex -> cacheRepo.findById(id));
// retry com backoff exponencial
Mono<Order> resiliente = externalApi.fetch(id)
.retryWhen(Retry.backoff(3, Duration.ofMillis(200)).maxBackoff(Duration.ofSeconds(2)));
// filter + switchIfEmpty
Flux<Pedido> ativos = pedidoRepo.findAll()
.filter(p -> p.getStatus() == StatusPedido.ATIVO)
.switchIfEmpty(Flux.error(new RuntimeException("Nenhum pedido ativo")));
// collectList — converte Flux para Mono<List>
Mono<List<PedidoResponse>> lista = service.listarTodos()
.map(PedidoResponse::from)
.collectList();R2DBC — JPA reativo:
public interface PedidoR2dbcRepository extends R2dbcRepository<Pedido, UUID> {
Flux<Pedido> findByClienteId(String clienteId);
Mono<Pedido> findByIdAndClienteId(UUID id, String clienteId);
}20. Spring Batch — Job, Step, Reader/Processor/Writer
Spring Batch é ideal para processamento em lote: importação de CSVs, relatórios, migrações. A unidade de execução é o Job composto de Steps. Cada Step tem um ItemReader, ItemProcessor e ItemWriter.
@Configuration
@EnableBatchProcessing
public class ImportacaoPedidosBatch {
@Bean
public Job importarPedidosJob(JobRepository jobRepo, Step importarStep) {
return new JobBuilder("importar-pedidos", jobRepo)
.start(importarStep)
.build();
}
@Bean
public Step importarStep(JobRepository jobRepo,
PlatformTransactionManager txManager,
ItemReader<PedidoCsv> reader,
ItemProcessor<PedidoCsv, Pedido> processor,
ItemWriter<Pedido> writer) {
return new StepBuilder("importar-step", jobRepo)
.<PedidoCsv, Pedido>chunk(100, txManager) // processa em lotes de 100
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
.skip(ValidationException.class).skipLimit(50) // pula até 50 erros
.build();
}
@Bean
@StepScope // scope de step — permite injetar parâmetros do Job
public FlatFileItemReader<PedidoCsv> csvReader(
@Value("#{jobParameters['arquivo']}") String arquivo) {
return new FlatFileItemReaderBuilder<PedidoCsv>()
.name("pedido-csv-reader")
.resource(new FileSystemResource(arquivo))
.delimited().delimiter(",")
.names("clienteId", "sku", "quantidade", "preco")
.targetType(PedidoCsv.class)
.linesToSkip(1) // pula header
.build();
}
@Bean
public ItemProcessor<PedidoCsv, Pedido> pedidoProcessor(PedidoValidator validator) {
return csv -> {
validator.validate(csv);
return Pedido.fromCsv(csv);
};
}
@Bean
public JpaItemWriter<Pedido> pedidoWriter(EntityManagerFactory emf) {
return new JpaItemWriterBuilder<Pedido>()
.entityManagerFactory(emf)
.build();
}
}21. Anotações — Referência Rápida
| Anotação | Para que serve |
|---|---|
@SpringBootApplication | Entry point — @Configuration + @ComponentScan + @EnableAutoConfiguration |
@RestController | @Controller + @ResponseBody — retorna JSON por padrão |
@RequestMapping | Prefixo de rota para a classe inteira |
@GetMapping / @PostMapping / @PutMapping / @PatchMapping / @DeleteMapping | Mapeamento de verbos HTTP |
@PathVariable | Extrai variável da URL (ex: /{id}) |
@RequestParam | Extrai parâmetro de query string |
@RequestBody | Desserializa o corpo JSON da request |
@Valid | Dispara Bean Validation no argumento |
@Service | Bean de lógica de negócio |
@Repository | Bean de acesso a dados + tradução de exceções JPA |
@Component | Bean genérico gerenciado pelo Spring |
@Configuration | Classe de configuração com @Bean |
@Bean | Declara um bean dentro de uma @Configuration |
@Transactional | Demarca transação; rollback em RuntimeException |
@Cacheable | Armazena resultado em cache |
@CacheEvict | Remove entrada do cache |
@Scheduled | Executa em agendamento (cron, fixedRate, fixedDelay) |
@Async | Executa em thread pool separado |
@EventListener | Consome ApplicationEvent |
@TransactionalEventListener | Consome evento apenas após commit da transação |
@PreAuthorize | Autorização por SpEL antes de executar o método |
@Profile | Bean ativo apenas em determinado perfil |
@Value("${prop}") | Injeta valor de properties/environment |
@ConfigurationProperties(prefix) | Binding tipado de grupo de propriedades |
@ConditionalOnProperty | Ativa bean somente se a propriedade tiver valor específico |
@MockitoBean | Substitui bean por mock em testes (@MockBean até Boot 3.3) |
22. Tabela de Versões
| Versão | Ano | Principais novidades |
|---|---|---|
| 2.7.x | 2022 | Última linha 2.x; Spring Framework 5; Java 8+ suportado; namespace javax.*; Spring Security 5; spring.security.oauth2.client melhorado |
| 3.0 | 2022 | Spring Framework 6; Java 17 mínimo; javax.* → jakarta.* (breaking change); suporte a GraalVM native image; @HttpExchange para HTTP clients declarativos; Spring Security 6 |
| 3.2 | 2023 | Suporte nativo a Virtual Threads (Project Loom); RestClient — novo HTTP client síncrono fluente substituto do RestTemplate; JdbcClient — JDBC fluente; @ConditionalOnThreading |
| 3.3 | 2024 | CDS (Class Data Sharing) out-of-the-box para startup mais rápido; Service Connections melhorado com Testcontainers; integração automática com Docker Compose em dev; @MockitoBean substitui @MockBean |
| 3.4 | 2024 | MockMvcTester (API fluente AssertJ para testes MVC); estruturação aprimorada de logs; @ConditionalOnMissingBean melhorado; melhorias no Actuator; suporte a ConnectionDetails para mais providers |
| 4.0 | 2025 | Java 17 mínimo confirmado + suporte Java 21; Spring Framework 7; suporte completo a Jakarta EE 11; revisão da API de auto-configuration; melhorias em observability (OTEL nativo) |
23. WebFlux — RouterFunction, WebClient e Server-Sent Events
Spring WebFlux é o framework web reativo do Spring, introduzido no Spring Framework 5. Diferentemente do Spring MVC (que bloqueia threads em I/O), o WebFlux usa o modelo de programação reativo baseado no Reactor (Project Reactor) — sem bloqueio, com back-pressure. A dependência troca spring-boot-starter-web por spring-boot-starter-webflux.
Dependência (Gradle):
implementation("org.springframework.boot:spring-boot-starter-webflux")@RestController reativo com Mono<T> e Flux<T>:
Mono<T> representa 0 ou 1 resultado; Flux<T> representa 0 ou N resultados. O Spring WebFlux suporta exatamente as mesmas anotações MVC — a diferença está nos tipos de retorno.
@RestController
@RequestMapping("/produtos")
public class ProdutoController {
private final ProdutoRepository repository;
public ProdutoController(ProdutoRepository repository) {
this.repository = repository;
}
@GetMapping
public Flux<Produto> listarTodos() {
return repository.findAll();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Produto>> buscarPorId(@PathVariable Long id) {
return repository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Produto> criar(@RequestBody @Valid Mono<ProdutoRequest> request) {
return request
.map(req -> new Produto(req.nome(), req.preco()))
.flatMap(repository::save);
}
}RouterFunction — alternativa funcional ao MVC:
Em vez de anotações, você define rotas como funções puras. Útil quando quer evitar reflexão ou prefere composição funcional.
@Configuration
public class ProdutoRouter {
@Bean
public RouterFunction<ServerResponse> routes(ProdutoHandler handler) {
return RouterFunctions.route()
.GET("/produtos", handler::listarTodos)
.GET("/produtos/{id}", handler::buscarPorId)
.POST("/produtos", handler::criar)
.build();
}
}
@Component
public class ProdutoHandler {
private final ProdutoRepository repository;
public ProdutoHandler(ProdutoRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listarTodos(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(repository.findAll(), Produto.class);
}
public Mono<ServerResponse> buscarPorId(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return repository.findById(id)
.flatMap(produto -> ServerResponse.ok().bodyValue(produto))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> criar(ServerRequest request) {
return request.bodyToMono(ProdutoRequest.class)
.map(req -> new Produto(req.nome(), req.preco()))
.flatMap(repository::save)
.flatMap(salvo -> ServerResponse.status(HttpStatus.CREATED).bodyValue(salvo));
}
}WebClient — substituto reativo do RestTemplate:
O WebClient é não-bloqueante, fluente e totalmente integrado com o Reactor. O Spring Boot auto-configura um WebClient.Builder para injeção.
@Service
public class PagamentoClient {
private final WebClient webClient;
public PagamentoClient(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("https://api.pagamentos.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
public Mono<PagamentoResponse> processar(PagamentoRequest request) {
return webClient.post()
.uri("/processar")
.bodyValue(request)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, resp ->
resp.bodyToMono(String.class).map(body -> new PagamentoException(body)))
.bodyToMono(PagamentoResponse.class)
.timeout(Duration.ofSeconds(10))
.retryWhen(Retry.backoff(3, Duration.ofMillis(200)));
}
public Flux<Produto> listarProdutos() {
return webClient.get()
.uri("/produtos")
.retrieve()
.bodyToFlux(Produto.class);
}
}Server-Sent Events com Flux:
SSE (Server-Sent Events) permite que o servidor empurre dados para o cliente continuamente. Com WebFlux, basta retornar Flux<T> com o media type correto.
@GetMapping(value = "/eventos", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamEventos() {
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> ServerSentEvent.<String>builder()
.id(String.valueOf(seq))
.event("mensagem")
.data("Evento número " + seq)
.build());
}
// Exemplo com dados de negócio — stream de cotações
@GetMapping(value = "/cotacoes/{ativo}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<CotacaoEvent> streamCotacoes(@PathVariable String ativo) {
return cotacaoService.streamPorAtivo(ativo)
.delayElements(Duration.ofMillis(500));
}Quando usar WebFlux vs MVC:
| Situação | Recomendação |
|---|---|
| Muitas conexões simultâneas com I/O (APIs externas, WebSockets, SSE) | WebFlux — threads não ficam bloqueadas |
| CRUD simples com banco relacional (JDBC) | MVC — JDBC é bloqueante; WebFlux não ajuda |
| Banco reativo (R2DBC, MongoDB reativo) | WebFlux — aproveita o pipeline reativo de ponta a ponta |
| Equipe com experiência em programação reativa | WebFlux |
| Integração com libs que bloqueiam threads | MVC — misturar bloqueante com reativo é perigoso |
24. Spring Security — JWT Avançado: @PreAuthorize e Claims do Token
Com spring-boot-starter-oauth2-resource-server, o Spring Boot configura automaticamente sua API para validar tokens JWT emitidos por um Authorization Server (Keycloak, Okta, Auth0, etc.). A configuração mínima é apenas uma propriedade no application.yml.
Dependência:
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")Configuração JWT via application.yml:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://auth.empresa.com/realms/app/protocol/openid-connect/certs
# Alternativa: issuer-uri (faz discovery automático do jwk-set-uri)
# issuer-uri: https://auth.empresa.com/realms/appSecurityConfig com JWT:
@Configuration
@EnableMethodSecurity // habilita @PreAuthorize, @PostAuthorize, @Secured
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/produtos/**").hasAuthority("SCOPE_produtos:read")
.requestMatchers(HttpMethod.POST, "/produtos").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))
)
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter rolesConverter = new JwtGrantedAuthoritiesConverter();
rolesConverter.setAuthoritiesClaimName("realm_access.roles"); // claim do Keycloak
rolesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> roles = rolesConverter.convert(jwt);
// combina roles + scopes do token
Collection<GrantedAuthority> scopes = new JwtGrantedAuthoritiesConverter().convert(jwt);
List<GrantedAuthority> all = new ArrayList<>(roles);
all.addAll(scopes);
return all;
});
return converter;
}
}@PreAuthorize com claims do token:
@RestController
@RequestMapping("/pedidos")
public class PedidoController {
// Verifica role do Keycloak
@GetMapping
@PreAuthorize("hasRole('USER')")
public List<PedidoResponse> listar(JwtAuthenticationToken auth) {
String userId = auth.getToken().getSubject(); // sub do JWT
return pedidoService.listarPorUsuario(userId);
}
// Verifica scope OAuth2
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_pedidos:write')")
public ResponseEntity<PedidoResponse> criar(@RequestBody @Valid PedidoRequest req) {
return ResponseEntity.status(HttpStatus.CREATED).body(pedidoService.criar(req));
}
// Acessa claim customizado do token
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @pedidoService.pertenceAoUsuario(#id, authentication.token.subject)")
public void deletar(@PathVariable Long id) {
pedidoService.deletar(id);
}
// Injeta dados do token diretamente
@GetMapping("/perfil")
public Map<String, Object> perfil(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"sub", jwt.getSubject(),
"email", jwt.getClaimAsString("email"),
"roles", jwt.getClaimAsStringList("realm_access")
);
}
}25. Spring Cloud — Essencial
Spring Cloud fornece padrões de microserviços (configuração centralizada, service discovery, circuit breaker, load balancing) sobre o ecossistema Spring Boot. Cada componente é uma dependência independente dentro do BOM spring-cloud-dependencies.
BOM do Spring Cloud (Gradle):
extra["springCloudVersion"] = "2024.0.1"
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}Config Server — configuração centralizada:
O Config Server serve arquivos de configuração (YAML/properties) para outros microserviços a partir de um repositório Git.
// Config Server — dependência
implementation("org.springframework.cloud:spring-cloud-config-server")@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}# application.yml do Config Server
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/empresa/config-repo
default-label: main
search-paths: "{application}" # pasta por serviço# bootstrap.yml dos microserviços clientes (ou application.yml com spring.config.import)
spring:
application:
name: pedidos-service
config:
import: "configserver:http://localhost:8888"Eureka — Service Discovery:
O Eureka é um registro de serviços: microserviços se registram e descobrem outros pelo nome, sem endereços fixos.
// Servidor Eureka
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
// Cliente Eureka (microserviços)
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}# application.yml dos microserviços clientes
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: trueOpenFeign — HTTP client declarativo:
O Feign gera implementações de clientes HTTP em tempo de compilação a partir de interfaces anotadas. Integra automaticamente com Eureka (usa o nome do serviço como host).
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")@SpringBootApplication
@EnableFeignClients
public class PedidosApplication { ... }
@FeignClient(name = "estoque-service", path = "/estoque")
public interface EstoqueClient {
@GetMapping("/produtos/{sku}/disponibilidade")
DisponibilidadeResponse verificarDisponibilidade(@PathVariable String sku);
@PostMapping("/reservas")
ReservaResponse reservar(@RequestBody ReservaRequest request);
}
// Uso — injeção direta como qualquer bean Spring
@Service
public class PedidoService {
private final EstoqueClient estoque;
public PedidoService(EstoqueClient estoque) {
this.estoque = estoque;
}
public void criar(PedidoRequest req) {
DisponibilidadeResponse disp = estoque.verificarDisponibilidade(req.sku());
if (!disp.disponivel()) throw new EstoqueInsuficienteException(req.sku());
// ...
}
}Circuit Breaker com Resilience4j:
O Circuit Breaker evita que falhas em serviços dependentes cascateiem pelo sistema. Resilience4j é a implementação recomendada no Spring Cloud 2022+.
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")@Service
public class EstoqueService {
private final CircuitBreakerFactory cbFactory;
private final EstoqueClient client;
public EstoqueService(CircuitBreakerFactory cbFactory, EstoqueClient client) {
this.cbFactory = cbFactory;
this.client = client;
}
public DisponibilidadeResponse verificar(String sku) {
CircuitBreaker cb = cbFactory.create("estoque");
return cb.run(
() -> client.verificarDisponibilidade(sku),
throwable -> new DisponibilidadeResponse(false, "Serviço indisponível") // fallback
);
}
}# Configuração do Resilience4j
resilience4j:
circuitbreaker:
instances:
estoque:
sliding-window-size: 10
failure-rate-threshold: 50 # abre o circuito se 50% das chamadas falharem
wait-duration-in-open-state: 10s # tempo em que o circuito fica aberto
permitted-number-of-calls-in-half-open-state: 326. Actuator — Endpoints Customizados
O Spring Boot Actuator expõe informações de saúde e métricas da aplicação. Além dos endpoints embutidos (/health, /info, /metrics), você pode criar seus próprios endpoints e health indicators para necessidades específicas do negócio.
Criar um @Endpoint customizado:
Um @Endpoint pode ser acessado via HTTP, JMX ou ambos. @ReadOperation → GET, @WriteOperation → POST, @DeleteOperation → DELETE.
@Component
@Endpoint(id = "feature-flags") // acessível em /actuator/feature-flags
public class FeatureFlagsEndpoint {
private final FeatureFlagService service;
public FeatureFlagsEndpoint(FeatureFlagService service) {
this.service = service;
}
@ReadOperation
public Map<String, Boolean> listar() {
return service.getAll();
}
@ReadOperation
public FeatureFlag buscar(@Selector String nome) {
return service.getByName(nome);
}
@WriteOperation
public void ativar(@Selector String nome, boolean ativo) {
service.setAtivo(nome, ativo);
}
}Expor via application.yml:
management:
endpoints:
web:
exposure:
include: health, info, metrics, prometheus, feature-flags
endpoint:
health:
show-details: when-authorized
feature-flags:
cache:
time-to-live: 10sHealthIndicator customizado:
@Component
public class ServicoExternoHealthIndicator implements HealthIndicator {
private final ServicoExternoClient client;
public ServicoExternoHealthIndicator(ServicoExternoClient client) {
this.client = client;
}
@Override
public Health health() {
try {
StatusResponse status = client.status();
if (status.ok()) {
return Health.up()
.withDetail("latencia_ms", status.latencyMs())
.withDetail("versao", status.version())
.build();
}
return Health.down()
.withDetail("motivo", status.errorMessage())
.build();
} catch (Exception e) {
return Health.down(e)
.withDetail("motivo", "Timeout ou conexão recusada")
.build();
}
}
}InfoContributor customizado:
O endpoint /actuator/info agrega contribuições. Útil para expor versão do build, branch do Git, ou qualquer metadado da aplicação.
@Component
public class AppInfoContributor implements InfoContributor {
private final BuildProperties buildProperties;
private final GitProperties gitProperties;
public AppInfoContributor(BuildProperties build, GitProperties git) {
this.buildProperties = build;
this.gitProperties = git;
}
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("build", Map.of(
"versao", buildProperties.getVersion(),
"artefato", buildProperties.getArtifact(),
"timestamp", buildProperties.getTime()
));
builder.withDetail("git", Map.of(
"branch", gitProperties.getBranch(),
"commit", gitProperties.getShortCommitId(),
"data", gitProperties.getCommitTime()
));
}
}# Habilitar informações de build e git no /actuator/info
management:
info:
build:
enabled: true
git:
enabled: true
mode: full # simples ou full27. AOP — Aspect Oriented Programming
AOP (Programação Orientada a Aspectos) permite separar preocupações transversais — logging, segurança, auditoria, métricas — do código de negócio. O Spring AOP usa proxies JDK ou CGLIB; é baseado em tempo de execução (não compile-time como AspectJ). Para habilitá-lo, adicione a dependência abaixo (já inclusa no spring-boot-starter).
implementation("org.springframework.boot:spring-boot-starter-aop")Conceitos fundamentais:
| Termo | Significado |
|---|---|
| Aspect | Classe que encapsula o comportamento transversal (@Aspect) |
| Join Point | Ponto de execução onde o aspecto pode intervir (em Spring: sempre uma chamada de método) |
| Pointcut | Expressão que seleciona quais join points o aspecto intercepta |
| Advice | Código executado no join point (@Before, @After, @Around, etc.) |
| Weaving | Processo de aplicar o aspecto ao código alvo |
Anotações de advice:
@Aspect
@Component
public class AuditoriaAspect {
private static final Logger log = LoggerFactory.getLogger(AuditoriaAspect.class);
// Executa ANTES do método — não pode alterar o retorno
@Before("execution(* com.empresa.pedidos.service.*.*(..))")
public void antes(JoinPoint jp) {
log.info("Chamando: {}.{}",
jp.getTarget().getClass().getSimpleName(),
jp.getSignature().getName());
}
// Executa DEPOIS — sempre, mesmo se lançar exceção
@After("execution(* com.empresa.pedidos.service.*.*(..))")
public void depois(JoinPoint jp) {
log.debug("Concluído: {}", jp.getSignature().toShortString());
}
// Executa somente se o método retornar normalmente — acessa o valor de retorno
@AfterReturning(
pointcut = "execution(* com.empresa.pedidos.service.PedidoService.criar(..))",
returning = "resultado"
)
public void aposRetorno(JoinPoint jp, Object resultado) {
log.info("Pedido criado com sucesso: {}", resultado);
}
// Executa somente se o método lançar exceção — acessa a exceção
@AfterThrowing(
pointcut = "execution(* com.empresa.pedidos.service.*.*(..))",
throwing = "ex"
)
public void aposExcecao(JoinPoint jp, Exception ex) {
log.error("Falha em {}: {}", jp.getSignature().toShortString(), ex.getMessage());
}
}@Around — controle total sobre o join point:
@Around é o advice mais poderoso: você decide se o método original será chamado, pode alterar os argumentos e o valor de retorno, e medir o tempo de execução.
@Aspect
@Component
public class TempoExecucaoAspect {
private static final Logger log = LoggerFactory.getLogger(TempoExecucaoAspect.class);
// Intercepta métodos anotados com @MedirTempo (anotação customizada)
@Around("@annotation(com.empresa.pedidos.shared.aop.MedirTempo)")
public Object medirTempo(ProceedingJoinPoint pjp) throws Throwable {
long inicio = System.currentTimeMillis();
String metodo = pjp.getSignature().toShortString();
try {
Object resultado = pjp.proceed(); // executa o método original
long duracao = System.currentTimeMillis() - inicio;
log.info("[TEMPO] {} completou em {}ms", metodo, duracao);
return resultado;
} catch (Throwable ex) {
long duracao = System.currentTimeMillis() - inicio;
log.warn("[TEMPO] {} falhou após {}ms", metodo, duracao);
throw ex;
}
}
}
// Anotação marcadora
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MedirTempo {}
// Uso
@Service
public class RelatorioService {
@MedirTempo
public RelatorioResponse gerarRelatorio(FiltroRequest filtro) {
// lógica demorada
}
}Pointcut expressions — referência rápida:
@Aspect
@Component
public class ExemplosPointcut {
// Qualquer método público em qualquer classe
@Pointcut("execution(public * *(..))")
public void qualquerMetodoPublico() {}
// Qualquer método no pacote service (um nível)
@Pointcut("execution(* com.empresa.pedidos.service.*.*(..))")
public void qualquerServico() {}
// Qualquer método no pacote service e sub-pacotes (..)
@Pointcut("execution(* com.empresa.pedidos.service..*.*(..))")
public void qualquerServicoRecursivo() {}
// Métodos anotados com @Transactional
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void metodosTransacionais() {}
// Beans anotados com @Service
@Pointcut("within(@org.springframework.stereotype.Service *)")
public void classesService() {}
// Combinando com operadores lógicos
@Pointcut("classesService() && !@annotation(com.empresa.pedidos.shared.aop.SemAuditoria)")
public void servicosSemExcecao() {}
@Before("servicosSemExcecao()")
public void auditarServicos(JoinPoint jp) {
// aplica somente em @Service que não têm @SemAuditoria
}
}