Cheatsheet Spring Boot
Framework Java para aplicações enterprise e APIs REST
Spring Boot
Setup e CLI
Criar projecto
# Spring Initializr (start.spring.io) ou CLI: spring init --build=maven \ --dependencies=web,jpa,mysql \ --java-version=21 minha-app cd minha-app ./mvnw spring-boot:run
spring init gera a estrutura do projecto. --dependencies define os starters iniciais. O mvnw é o Maven wrapper incluído. Aceda em localhost:8080.
Comandos Maven
./mvnw spring-boot:run # executar ./mvnw clean package # gerar JAR ./mvnw test # correr testes java -jar target/app.jar # executar JAR # Gradle alternativo: ./gradlew bootRun ./gradlew bootJar
spring-boot:run inicia em modo dev. clean package gera o JAR executável em target/. O JAR inclui o servidor embedded — basta java -jar.
Estrutura do projecto
src/main/java/com/app/ Application.java # main controller/ model/ repository/ service/ dto/ src/main/resources/ application.properties static/ templates/
Application.java é o ponto de entrada. A organização segue camadas: controller, service, repository e model. Recursos ficam em resources/.
Dependências Gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.5'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}build.gradle é a alternativa ao Maven. O plugin org.springframework.boot gere versões. runtimeOnly é para drivers só necessários em runtime. testImplementation para dependências de teste.
Classe principal
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}@SpringBootApplication combina @Configuration, @EnableAutoConfiguration e @ComponentScan. O método main inicia o servidor embedded Tomcat.
DevTools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
# application.properties:
spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=truedevtools activa restart automático ao alterar código. Inclui LiveReload para o browser. É marcado optional para não ser incluído no JAR final. Só útil em desenvolvimento.
Dependências Maven
<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>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>Cada starter agrupa dependências relacionadas. starter-web inclui Tomcat e Jackson. starter-data-jpa inclui Hibernate. O mysql-connector-j é o driver JDBC.
Requisitos Java
# Spring Boot 3.x requer: # - Java 17+ (recomendado 21) # - Jakarta EE (não javax) # Verificar versão: java -version # Maven wrapper actualiza: ./mvnw wrapper:wrapper -Dmaven=3.9.6
Spring Boot 3 migrou de javax.* para jakarta.*. Requer Java 17 no mínimo. Use Java 21 para virtual threads. O maven-wrapper dispensa instalação global do Maven.
Controllers REST
REST Controller
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@GetMapping
public List<Post> listar() {
return service.findAll();
}
}@RestController combina @Controller + @ResponseBody. @RequestMapping define a rota base. A injeção é por construtor. Retorna JSON automaticamente via Jackson.
ResponseEntity
@PostMapping
public ResponseEntity<Post> criar(@RequestBody PostDTO dto) {
Post post = service.create(dto);
return ResponseEntity
.status(HttpStatus.CREATED)
.header("X-Post-Id", post.getId().toString())
.body(post);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> apagar(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}ResponseEntity permite controlar status, headers e corpo. HttpStatus.CREATED retorna 201. noContent() retorna 204 sem corpo. Use quando precisar de headers customizados.
Métodos HTTP
@GetMapping // GET /api/posts
@GetMapping("/{id}") // GET /api/posts/1
@PostMapping // POST /api/posts
@PutMapping("/{id}") // PUT /api/posts/1
@PatchMapping("/{id}") // PATCH /api/posts/1
@DeleteMapping("/{id}") // DELETE /api/posts/1Cada anotação mapeia um verbo HTTP. @GetMapping para leitura, @PostMapping para criação. @PutMapping substitui tudo, @PatchMapping actualiza parcialmente. @DeleteMapping remove.
Exception handler global
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(NotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors()
.stream().map(e -> e.getDefaultMessage())
.collect(Collectors.joining(", "));
return new ErrorResponse(msg);
}
}@RestControllerAdvice intercepta excepções de todos os controllers. @ExceptionHandler define qual excepção tratar. @ResponseStatus define o código HTTP. Centraliza o tratamento de erros.
Path e Body
@GetMapping("/{id}")
public Post ver(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
public Post criar(@RequestBody @Valid PostDTO dto) {
return service.create(dto);
}@PathVariable extrai valores da URL. @RequestBody desserializa o JSON do corpo. @Valid activa a validação Bean Validation antes de entrar no método.
Headers e cookies
@GetMapping("/download")
public ResponseEntity<byte[]> download() {
byte[] dados = service.gerarPdf();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "application/pdf")
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=relatorio.pdf")
.body(dados);
}
@GetMapping("/perfil")
public String perfil(@CookieValue("sessao") String token) {
return service.getPerfil(token);
}HttpHeaders define headers de resposta. CONTENT_DISPOSITION força download. @CookieValue lê cookies do pedido. Use ResponseCookie para criar cookies com opções.
Request params
@GetMapping
public List<Post> pesquisar(
@RequestParam String termo,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String categoria
) {
return service.search(termo, page, size, categoria);
}@RequestParam lê query strings (?termo=x&page=0). defaultValue evita erro se ausente. required = false torna opcional. Ideal para filtros e paginação.
Validação de entrada
public record PostDTO(
@NotBlank(message = "Título obrigatório")
@Size(min = 5, max = 200)
String titulo,
@NotNull
@Positive
Integer categoriaId
) {}
// No controller:
@PostMapping
public Post criar(@RequestBody @Valid PostDTO dto) { ... }@NotBlank rejeita strings vazias. @Size limita comprimento. @Positive exige valor > 0. @Valid no controller activa a validação. Erros geram 400 Bad Request.
Entities e JPA
Definir Entity
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String titulo;
@Column(columnDefinition = "TEXT")
private String conteudo;
private LocalDateTime criadoEm;
@PrePersist
void prePersist() {
this.criadoEm = LocalDateTime.now();
}
}@Entity marca a classe como tabela JPA. @Id + @GeneratedValue define a chave auto-incremento. @Column personaliza a coluna. @PrePersist executa antes de inserir.
Relação ManyToMany
@Entity
public class Post {
@ManyToMany
@JoinTable(
name = "post_tags",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
}
@Entity
public class Tag {
@ManyToMany(mappedBy = "tags")
private Set<Post> posts = new HashSet<>();
}@ManyToMany cria tabela intermédia automática. @JoinTable define o nome e as FKs. Use Set para evitar duplicados. Apenas um lado é dono — o outro usa mappedBy.
Anotações de coluna
@Column(length = 200, nullable = false) private String titulo; @Column(unique = true) private String slug; @Lob private String textoGrande; @Enumerated(EnumType.STRING) private Status status; @Temporal(TemporalType.TIMESTAMP) private Date actualizadoEm;
@Column define constraints da coluna. @Lob para textos grandes (CLOB/BLOB). @Enumerated(STRING) guarda o nome do enum, não o ordinal. @Temporal para tipos de data legados.
DTOs com records
// Request DTO
public record CriarPostDTO(
@NotBlank String titulo,
@NotBlank String conteudo,
Long autorId
) {}
// Response DTO
public record PostResponse(
Long id,
String titulo,
String autorNome,
LocalDateTime criadoEm
) {
public static PostResponse from(Post p) {
return new PostResponse(
p.getId(), p.getTitulo(),
p.getAutor().getNome(), p.getCriadoEm());
}
}record (Java 16+) cria DTOs imutáveis sem boilerplate. Separe request de response. O método estático from() converte Entity em DTO. Nunca exponha Entities directamente na API.
Relação ManyToOne
@Entity
public class Comentario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String texto;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
}@ManyToOne indica muitos comentários para um post. FetchType.LAZY carrega só quando acedido (evita N+1). @JoinColumn define a FK na tabela. Sempre prefira LAZY.
Bean Validation
public class UserDTO {
@NotBlank(message = "Nome obrigatório")
@Size(min = 2, max = 100)
private String nome;
@Email(message = "Email inválido")
@NotBlank
private String email;
@Min(value = 18, message = "Idade mínima: 18")
private int idade;
@Pattern(regexp = "^[A-Z]{2}$")
private String pais;
}@NotBlank rejeita nulo e vazio. @Email valida formato. @Min/@Max para limites numéricos. @Pattern usa regex. A mensagem é retornada no erro 400.
Relação OneToMany
@Entity
public class Post {
@OneToMany(mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Comentario> comentarios = new ArrayList<>();
public void addComentario(Comentario c) {
comentarios.add(c);
c.setPost(this);
}
public void removeComentario(Comentario c) {
comentarios.remove(c);
c.setPost(null);
}
}@OneToMany é o lado inverso da relação. mappedBy aponta para o campo dono. cascade = ALL propaga operações. orphanRemoval apaga filhos removidos da lista. Mantenha métodos helper para sincronizar ambos os lados.
Auditoria JPA
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Post {
@CreatedDate
private LocalDateTime criadoEm;
@LastModifiedDate
private LocalDateTime actualizadoEm;
@CreatedBy
private String criadoPor;
}
// Activar:
@Configuration
@EnableJpaAuditing
public class AuditConfig {}@EnableJpaAuditing activa a auditoria global. @CreatedDate preenche na criação. @LastModifiedDate actualiza a cada save. @CreatedBy requer um AuditorAware bean.
Repositories
Interface base
@Repository
public interface PostRepository
extends JpaRepository<Post, Long> {
// Métodos herdados:
// save(), findById(), findAll(),
// delete(), count(), existsById()
}JpaRepository fornece CRUD completo sem código. O genérico é <Entity, TipoId>. @Repository é opcional (Spring detecta automaticamente). Inclui paginação e ordenação.
Paginação e ordenação
// No controller:
@GetMapping
public Page<Post> listar(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "criadoEm") String sort
) {
return repo.findAll(PageRequest.of(page, size,
Sort.by(sort).descending()));
}
// Resposta inclui:
// content[], totalElements, totalPages, numberPageRequest.of() define página e tamanho. Sort.by() ordena por campo. Page inclui metadados: total, páginas, actual. A paginação começa em 0. Serializa directo para JSON.
Query methods
public interface PostRepository
extends JpaRepository<Post, Long> {
List<Post> findByAtivoTrue();
List<Post> findByTituloContaining(String termo);
List<Post> findByAutorNomeAndAtivo(String nome, boolean ativo);
List<Post> findByViewsGreaterThanOrderByCriadoEmDesc(int min);
long countByAtivoTrue();
boolean existsBySlug(String slug);
Optional<Post> findBySlug(String slug);
}Spring gera a query pelo nome do método. findBy + campo + condição. Containing faz LIKE. OrderBy ordena. countBy e existsBy para agregações. Sem SQL manual.
Specifications (filtros dinâmicos)
public interface PostRepository extends
JpaRepository<Post, Long>,
JpaSpecificationExecutor<Post> {}
// Construir filtro dinâmico:
Specification<Post> spec = Specification.where(null);
if (titulo != null) {
spec = spec.and((root, q, cb) ->
cb.like(root.get("titulo"), "%" + titulo + "%"));
}
if (ativo != null) {
spec = spec.and((root, q, cb) ->
cb.equal(root.get("ativo"), ativo));
}
List<Post> resultados = repo.findAll(spec);JpaSpecificationExecutor activa filtros dinâmicos. Specification constrói predicados programaticamente. Ideal para pesquisas com múltiplos filtros opcionais. Combina com and()/or().
@Query JPQL
@Query("SELECT p FROM Post p WHERE p.views > :min " +
"AND p.ativo = true ORDER BY p.criadoEm DESC")
List<Post> findPopulares(@Param("min") int min);
@Query("SELECT new com.app.dto.PostResumoDTO(p.id, p.titulo) " +
"FROM Post p WHERE p.autor.id = :autorId")
List<PostResumoDTO> resumosPorAutor(@Param("autorId") Long id);@Query permite JPQL personalizado. :param são parâmetros nomeados com @Param. JPQL usa nomes de classes, não tabelas. SELECT new projecta directamente para DTOs.
Projections
// Interface-based projection:
public interface PostResumo {
Long getId();
String getTitulo();
String getAutorNome();
}
List<PostResumo> findByAtivoTrue();
// Closed projection (JPQL):
@Query("SELECT p.id AS id, p.titulo AS titulo " +
"FROM Post p")
List<PostResumo> listarResumos();Projections retornam apenas campos necessários. A interface define getters com alias. Reduz tráfego e memória vs. Entity completa. Use para listagens e relatórios. O Spring implementa a interface automaticamente.
Query nativa
@Query(value = "SELECT * FROM posts " +
"WHERE MATCH(titulo, conteudo) AGAINST(:termo)",
nativeQuery = true)
List<Post> pesquisaFullText(@Param("termo") String termo);
@Modifying
@Query(value = "UPDATE posts SET views = views + 1 WHERE id = :id",
nativeQuery = true)
void incrementarViews(@Param("id") Long id);nativeQuery = true usa SQL directo da BD. Útil para funcionalidades específicas (full-text, JSON). @Modifying é obrigatório para UPDATE/DELETE. Combine com @Transactional.
Services e DI
Service layer
@Service
public class PostService {
private final PostRepository repo;
private final AutorRepository autorRepo;
public PostService(PostRepository repo,
AutorRepository autorRepo) {
this.repo = repo;
this.autorRepo = autorRepo;
}
public List<Post> findAll() {
return repo.findAll();
}
}@Service marca a camada de negócio. Injeção por construtor é o padrão recomendado. O Spring cria o singleton automaticamente. Services orquestram repositories e regras de negócio.
@Component e @Bean
@Component // auto-detetado pelo ComponentScan
public class SlugGenerator {
public String gerar(String titulo) {
return titulo.toLowerCase().replaceAll("\\s+", "-");
}
}
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
@Scope("prototype")
public Notificacao notificacao() {
return new Notificacao();
}
}@Component regista classes no contexto. @Bean em @Configuration regista objectos de terceiros. @Scope("prototype") cria instância nova a cada injeção. Por defeito, beans são singletons.
CRUD no service
public Post create(CriarPostDTO dto) {
Autor autor = autorRepo.findById(dto.autorId())
.orElseThrow(() -> new NotFoundException("Autor não encontrado"));
Post post = new Post();
post.setTitulo(dto.titulo());
post.setConteudo(dto.conteudo());
post.setAutor(autor);
return repo.save(post);
}
public void delete(Long id) {
if (!repo.existsById(id)) {
throw new NotFoundException("Post não encontrado");
}
repo.deleteById(id);
}orElseThrow lança excepção se não existir. Valide regras de negócio antes de persistir. save() faz INSERT ou UPDATE conforme o ID. Separe validação da persistência.
Events e listeners
// Definir evento:
public record PostCriadoEvent(Post post) {}
// Publicar:
@Service
public class PostService {
private final ApplicationEventPublisher publisher;
public void create(PostDTO dto) {
Post post = repo.save(new Post(dto));
publisher.publishEvent(new PostCriadoEvent(post));
}
}
// Ouvir:
@Component
public class NotificarSubscritores {
@EventListener
public void onPostCriado(PostCriadoEvent event) {
// enviar emails, notificações...
}
}ApplicationEventPublisher publica eventos. @EventListener reage de forma desacoplada. Ideal para efeitos colaterais (emails, logs). Use @Async para processar em background.
@Transactional
@Transactional
public Post criarComComentarios(CriarPostDTO dto) {
Post post = repo.save(new Post(dto));
for (String texto : dto.comentarios()) {
comentarioRepo.save(new Comentario(post, texto));
}
return post;
// Se falhar, tudo é revertido (rollback)
}
@Transactional(readOnly = true)
public List<Post> listar() {
return repo.findAll();
}@Transactional garante atomicidade — tudo ou nada. Se uma excepção ocorrer, faz rollback. readOnly = true optimiza leituras. Coloque no service, não no controller.
Scheduled tasks
@Configuration
@EnableScheduling
public class ScheduleConfig {}
@Component
public class TarefasAgendadas {
@Scheduled(cron = "0 0 2 * * ?")
public void limparTokensExpirados() {
tokenRepo.deleteByExpiradoEmBefore(LocalDateTime.now());
}
@Scheduled(fixedRate = 60000) // a cada 60s
public void sincronizarCache() {
cacheService.refresh();
}
}@EnableScheduling activa o agendador. cron define expressão (2h da manhã). fixedRate executa a cada X ms. Útil para limpezas, syncs e relatórios. Não use em múltiplas instâncias sem lock.
Injeção de dependências
// Recomendado: construtor (implícito com um só construtor)
@Service
public class EmailService {
private final MailSender sender;
public EmailService(MailSender sender) {
this.sender = sender;
}
}
// Alternativa: @Autowired no campo (evitar)
@Autowired
private MailSender sender;
// Qualifier para múltiplas implementações:
@Autowired
@Qualifier("smtp")
private MailSender sender;Injeção por construtor é testável e imutável. @Autowired no campo dificulta testes. @Qualifier escolhe entre múltiplos beans do mesmo tipo. Com um construtor, @Autowired é opcional.
Configuração
application.properties
server.port=8080 spring.application.name=minha-app spring.datasource.url=jdbc:mysql://localhost:3306/minha_bd spring.datasource.username=root spring.datasource.password=secret spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
application.properties centraliza a configuração. ddl-auto=validate verifica schema sem alterar. show-sql loga queries em dev. Em produção use validate ou none, nunca update.
CORS
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}CORS permite pedidos de outros domínios. allowedOrigins restringe origens (nunca use * com credentials). maxAge faz cache do preflight. Essencial para APIs consumidas por frontends separados.
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/minha_bd
username: root
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
server:
port: 8080
servlet:
context-path: /apiFormato YAML alternativo ao properties. ${DB_PASSWORD} lê de variáveis de ambiente. context-path prefixa todas as rotas. Indentação define hierarquia. Escolha um formato e mantenha consistência.
Variáveis de ambiente
# application.properties com placeholders:
spring.datasource.password=${DB_PASSWORD}
app.jwt.secret=${JWT_SECRET}
app.mail.api-key=${MAIL_API_KEY:default-key}
# .env (com spring-dotenv):
DB_PASSWORD=super_secret
JWT_SECRET=chave-jwt-aqui
# Docker:
# docker run -e DB_PASSWORD=secret app${VAR} resolve de env, system properties ou args. :default define fallback. Nunca commite secrets no properties. Use spring-dotenv para ficheiros .env em dev. Em prod, use env vars ou Vault.
Profiles
# application-dev.properties spring.datasource.url=jdbc:h2:mem:dev spring.jpa.show-sql=true # application-prod.properties spring.datasource.url=jdbc:mysql://prod-server/bd spring.jpa.show-sql=false # Activar: spring.profiles.active=dev # Ou via CLI: java -jar app.jar --spring.profiles.active=prod
Profiles separam configuração por ambiente. Ficheiros seguem application-{profile}.properties. spring.profiles.active selecciona o activo. Ideal para dev, staging e prod. Pode combinar múltiplos perfis.
Beans condicionais
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnProperty(name = "app.cache.enabled",
havingValue = "true")
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("posts");
}
@Bean
@ConditionalOnMissingBean
public Clock clock() {
return Clock.systemDefaultZone();
}
}@ConditionalOnProperty cria o bean só se a propriedade existir. @ConditionalOnMissingBean evita duplicados. @Profile("dev") restringe por ambiente. Útil para configuração modular e testável.
@Value e @ConfigurationProperties
// Valor individual:
@Value("${app.upload.max-size:10MB}")
private String maxUpload;
// Grupo tipado:
@ConfigurationProperties(prefix = "app.mail")
public record MailProperties(
String host,
int port,
String from
) {}
// Activar:
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class AppConfig {}@Value injecta uma propriedade com default (:10MB). @ConfigurationProperties mapeia um prefixo para uma classe. Mais seguro e testável que múltiplos @Value. Valide com @Validated.
Avançado e Deploy
Actuator (health e métricas)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.properties:
management.endpoints.web.exposure.include=health,metrics,info
management.endpoint.health.show-details=always
# Endpoints:
# GET /actuator/health
# GET /actuator/metrics/jvm.memory.used
# GET /actuator/infoActuator expõe endpoints de monitorização. /health para load balancers e Kubernetes. /metrics integra com Prometheus/Grafana. show-details mostra BD, disco, etc. Proteja com Security.
Docker e deploy
# Build: ./mvnw clean package -DskipTests # Dockerfile: FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] # Build e run: docker build -t minha-app . docker run -p 8080:8080 \ -e DB_PASSWORD=secret minha-app
eclipse-temurin:21-jre-alpine é a imagem base leve. O JAR é auto-suficiente com Tomcat embedded. -e passa variáveis de ambiente. Use multi-stage build para optimizar. Publique no Docker Hub ou ECR.
Swagger / OpenAPI
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
# Aceder em:
# http://localhost:8080/swagger-ui.html
# http://localhost:8080/v3/api-docs
// Personalizar:
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("API Posts").version("1.0"));
}springdoc-openapi gera documentação automática. swagger-ui.html é a interface interactiva. Detecta endpoints, DTOs e validações. Anote com @Operation e @Schema para detalhes extra.
Flyway (migrações)
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
# src/main/resources/db/migration/
# V1__criar_posts.sql
# V2__add_coluna_slug.sql
# V3__criar_tabela_tags.sql
spring.flyway.enabled=true
spring.jpa.hibernate.ddl-auto=validateFlyway gere migrações de BD versionadas. Ficheiros seguem V{n}__descricao.sql. Execute em ordem, nunca edite aplicados. ddl-auto=validate confirma que o schema corresponde. Essencial para equipas e CI/CD.
Cache com @Cacheable
@Configuration
@EnableCaching
public class CacheConfig {}
@Service
public class PostService {
@Cacheable(value = "posts", key = "#id")
public Post findById(Long id) {
return repo.findById(id).orElseThrow();
}
@CacheEvict(value = "posts", key = "#post.id")
public Post update(Post post) {
return repo.save(post);
}
@CacheEvict(value = "posts", allEntries = true)
public void limparCache() {}
}@EnableCaching activa o suporte a cache. @Cacheable guarda resultado — chamadas seguintes não executam. @CacheEvict invalida ao actualizar. key usa SpEL. Use Redis em produção com spring-boot-starter-data-redis.
Logging e SLF4J
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class PostService {
private static final Logger log =
LoggerFactory.getLogger(PostService.class);
public Post create(PostDTO dto) {
log.info("Criar post: {}", dto.titulo());
try {
Post p = repo.save(new Post(dto));
log.debug("Post criado com id={}", p.getId());
return p;
} catch (Exception e) {
log.error("Falha ao criar post", e);
throw e;
}
}
}
# application.properties:
logging.level.com.app=DEBUGSLF4J é a API de logging padrão. Use {} como placeholder (evita concatenação). log.info para eventos, log.error com excepção para stacktrace. logging.level controla verbosidade por pacote. Use @Slf4j do Lombok para simplificar.
Async e @CompletableFuture
@Configuration
@EnableAsync
public class AsyncConfig {}
@Service
public class EmailService {
@Async
public CompletableFuture<Void> enviarAsync(String to) {
// operação demorada (envio de email)
mailSender.send(to, "Assunto", "Corpo");
return CompletableFuture.completedFuture(null);
}
}
// No controller:
emailService.enviarAsync("user@mail.com");
return ResponseEntity.accepted().build(); // 202@EnableAsync activa execução assíncrona. @Async corre em thread separada. O controller responde imediatamente com 202. CompletableFuture permite encadear resultados. Ideal para emails e notificações.
Segurança
Security config base
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm ->
sm.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}@EnableWebSecurity activa o Spring Security. csrf.disable() para APIs stateless. STATELESS não cria sessão HTTP. permitAll() liberta rotas públicas. O resto exige autenticação.
Login endpoint
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<TokenResponse> login(
@RequestBody @Valid LoginRequest req) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
req.email(), req.senha()));
String token = jwtService.generateToken(req.email());
return ResponseEntity.ok(new TokenResponse(token));
}
@PostMapping("/registo")
public ResponseEntity<Void> registar(
@RequestBody @Valid RegistoRequest req) {
userService.create(req);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}AuthenticationManager valida credenciais contra o UserDetailsService. Se falhar, lança BadCredentialsException. O token JWT é retornado ao cliente. Endpoints de auth devem ser permitAll().
Password encoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
// Registo:
String hash = encoder.encode("senha123");
user.setPassword(hash);
// Login:
boolean valido = encoder.matches("senha123", user.getPassword());BCryptPasswordEncoder gera hash com salt automático. O parâmetro 12 é o custo (força). matches() compara texto com hash. Nunca guarde passwords em texto simples. Cada hash é único mesmo para a mesma senha.
Regras por role
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/posts/**")
.hasAnyRole("ADMIN", "MODERADOR")
.anyRequest().authenticated()
);
// No controller (method security):
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }
@PreAuthorize("#id == authentication.principal.id")
public PerfilDTO getPerfil(Long id) { ... }hasRole() verifica authority ROLE_*. @PreAuthorize protege métodos individuais. #id acede a parâmetros via SpEL. Active com @EnableMethodSecurity. Combine URL rules com method security.
JWT - gerar token
@Service
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
public String generateToken(String email) {
return Jwts.builder()
.subject(email)
.issuedAt(new Date())
.expiration(new Date(
System.currentTimeMillis() + 86400000))
.signWith(getSigningKey())
.compact();
}
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
}Jwts.builder() constrói o token JWT. subject identifica o utilizador. expiration define validade (24h aqui). hmacShaKeyFor usa chave HMAC. Guarde o secret em env, nunca no código.
UserDetailsService
@Service
public class CustomUserDetailsService
implements UserDetailsService {
private final UserRepository repo;
@Override
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
User user = repo.findByEmail(email)
.orElseThrow(() ->
new UsernameNotFoundException(email));
return org.springframework.security
.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}UserDetailsService carrega utilizador para autenticação. loadUserByUsername é chamado pelo AuthenticationManager. Retorna UserDetails com roles. O Spring Security compara a password com o encoder.
JWT - validar (filtro)
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws Exception {
String token = extractToken(req);
if (token != null && jwtService.isValid(token)) {
String email = jwtService.getEmail(token);
var auth = new UsernamePasswordAuthenticationToken(
email, null, List.of());
SecurityContextHolder.getContext()
.setAuthentication(auth);
}
chain.doFilter(req, res);
}
}OncePerRequestFilter executa uma vez por pedido. Extrai o token do header Authorization: Bearer. Se válido, define a autenticação no SecurityContext. Registe o filtro antes do UsernamePasswordAuthenticationFilter.
Testes
Teste de integração
@SpringBootTest
class PostServiceTest {
@Autowired
private PostService service;
@Autowired
private PostRepository repo;
@Test
void criarPost() {
var dto = new CriarPostDTO("Título", "Texto", 1L);
Post post = service.create(dto);
assertNotNull(post.getId());
assertEquals("Título", post.getTitulo());
assertTrue(repo.existsById(post.getId()));
}
}@SpringBootTest sobe o contexto completo. Use para testes de integração reais. @Autowired injecta beans reais. assertNotNull e assertEquals são do JUnit 5. Mais lento mas mais realista.
Testcontainers
@SpringBootTest
@Testcontainers
class IntegrationTest {
@Container
static MySQLContainer<?> mysql =
new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test_db");
@DynamicPropertySource
static void config(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url",
mysql::getJdbcUrl);
registry.add("spring.datasource.username",
mysql::getUsername);
registry.add("spring.datasource.password",
mysql::getPassword);
}
}@Testcontainers sobe Docker containers para testes. MySQLContainer cria MySQL real efémero. @DynamicPropertySource injecta URL dinâmica. Requer Docker instalado. Testes mais fiáveis que H2.
MockMvc (testar API)
@WebMvcTest(PostController.class)
class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private PostService service;
@Test
void listarPosts() throws Exception {
when(service.findAll()).thenReturn(List.of(
new Post(1L, "Post 1")));
mockMvc.perform(get("/api/posts"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].titulo")
.value("Post 1"));
}
}@WebMvcTest carrega só a camada web. @MockBean substitui dependências por mocks. MockMvc simula pedidos HTTP sem servidor. jsonPath verifica campos do JSON. Rápido e isolado.
Testar com perfis
# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:test
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
// No teste:
@SpringBootTest
@ActiveProfiles("test")
class AppIntegrationTest {
@Test
void contextoCarrega() {
// Se chegar aqui, contexto OK
}
}@ActiveProfiles("test") usa config de teste. create-drop cria e apaga schema a cada run. H2 em memória é rápido para CI. Coloque em src/test/resources/. Separe sempre config de teste da produção.
Mockito (unit tests)
@ExtendWith(MockitoExtension.class)
class PostServiceUnitTest {
@Mock
private PostRepository repo;
@InjectMocks
private PostService service;
@Test
void criarPostSucesso() {
when(repo.save(any())).thenAnswer(inv -> {
Post p = inv.getArgument(0);
p.setId(1L);
return p;
});
Post resultado = service.create(dto);
assertEquals(1L, resultado.getId());
verify(repo).save(any());
}
}@ExtendWith(MockitoExtension.class) activa Mockito. @Mock cria dependências falsas. @InjectMocks injecta mocks no service. when/thenReturn define comportamento. verify confirma chamadas.
AssertJ e testes REST
@Test
void criarPostRetorna201() throws Exception {
String json = """
{"titulo": "Novo Post", "conteudo": "Texto aqui"}
""";
mockMvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").isNumber())
.andExpect(jsonPath("$.titulo").value("Novo Post"));
}
// Com RestTemplate/WebTestClient:
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(body.getTitulo()).contains("Post");mockMvc.perform() envia pedidos com body JSON. jsonPath valida estrutura da resposta. assertThat do AssertJ é mais legível. Text blocks (\"\"\") facilitam JSON inline. Combine status + body assertions.
Data JPA test
@DataJpaTest
class PostRepositoryTest {
@Autowired
private PostRepository repo;
@Autowired
private TestEntityManager em;
@Test
void findBySlug() {
em.persist(new Post("Post Teste", "slug-teste"));
Optional<Post> encontrado =
repo.findBySlug("slug-teste");
assertTrue(encontrado.isPresent());
assertEquals("Post Teste", encontrado.get().getTitulo());
}
}@DataJpaTest testa só a camada JPA com H2 em memória. TestEntityManager insere dados de teste. Cada teste faz rollback automático. Ideal para validar queries e constraints.