DevTools

Cheatsheet Spring Boot

Framework Java para aplicações enterprise e APIs REST

Voltar às linguagens
Spring Boot
66 cards encontrados
Categorias:
Versões:

Setup e CLI


8 cards
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=true

devtools 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


8 cards
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/1

Cada 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


8 cards
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


7 cards
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, number

PageRequest.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


7 cards
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


7 cards
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: /api

Formato 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


7 cards
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/info

Actuator 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=validate

Flyway 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=DEBUG

SLF4J é 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


7 cards
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


7 cards
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.