TP5.2 - Beans e Escopos
Objetivos
Ao final deste exercício, você será capaz de:
- ✅ Criar Beans com @Bean e @Configuration
- ✅ Entender a diferença entre Singleton e Prototype
- ✅ Utilizar @PostConstruct e @PreDestroy
- ✅ Criar Beans condicionais
Duração estimada: 1h30
Pré-requisitos
- ✅ Capítulo 3 do Módulo 5 concluído
- ✅ TP5.1 concluído
- ✅ Maven 3.9+ e Java 17+
Preparação: Novo Projeto
Criar o Projeto
Via Spring Initializr (https://start.spring.io/):
- Project: Maven
- Spring Boot: 3.2.0
- Group:
com.example - Artifact:
beans-demo - Dependencies: Spring Web
Estrutura:
src/main/java/com/example/beans/
├── BeansDemoApplication.java
├── service/
│ ├── CounterService.java
│ ├── ReportGenerator.java
│ └── CacheService.java
├── config/
│ └── AppConfig.java
└── controller/
└── TestController.java
Exercício 1: Singleton vs Prototype
Objetivo
Entender a diferença entre um Bean Singleton (1 instância) e Prototype (várias instâncias).
Etapa 1.1: Criar CounterService (Singleton por padrão)
Arquivo: service/CounterService.java
package com.example.beans.service;
import org.springframework.stereotype.Service;
@Service // Singleton por padrão
public class CounterService {
private int counter = 0;
public void increment() {
counter++;
System.out.println("Counter incrementado: " + counter);
}
public int getCounter() {
return counter;
}
}
Pontos-chave:
@Service→ Singleton por padrãocountercompartilhado entre todos os usos
Etapa 1.2: Testar o Singleton
Arquivo: controller/TestController.java
package com.example.beans.controller;
import com.example.beans.service.CounterService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
public class TestController {
private final CounterService counterService;
public TestController(CounterService counterService) {
this.counterService = counterService;
}
@GetMapping("/singleton")
public String testSingleton() {
counterService.increment();
return "Counter: " + counterService.getCounter() +
" (Singleton - mesma instância)";
}
}
Etapa 1.3: Testar
Inicie:
mvn clean install -DskipTests spring-boot:run
Teste várias vezes:
http://localhost:8080/api/test/singleton
Resultado:
1ª requisição: Counter: 1
2ª requisição: Counter: 2
3ª requisição: Counter: 3
...
✅ É a MESMA instância! O contador continua aumentando.
Etapa 1.4: Criar ReportGenerator (Prototype)
Arquivo: service/ReportGenerator.java
package com.example.beans.service;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@Scope("prototype") // ← Nova instância a cada vez!
public class ReportGenerator {
private String reportId;
private List<String> data = new ArrayList<>();
private LocalDateTime createdAt;
public ReportGenerator() {
this.reportId = "REPORT-" + System.currentTimeMillis();
this.createdAt = LocalDateTime.now();
System.out.println("Nova instância ReportGenerator criada: " + reportId);
}
public void addData(String item) {
data.add(item);
}
public String getReport() {
return String.format("Report %s criado em %s com %d itens",
reportId, createdAt, data.size());
}
}
Pontos-chave:
@Scope("prototype")→ Nova instância a cada injeção- Cada instância tem seu próprio
reportIdedata
Etapa 1.5: Testar o Prototype
Adicione em TestController.java:
@RestController
@RequestMapping("/api/test")
public class TestController {
private final CounterService counterService;
private final ApplicationContext context; // ← Para recuperar Prototypes
public TestController(CounterService counterService,
ApplicationContext context) {
this.counterService = counterService;
this.context = context;
}
// ... método singleton ...
@GetMapping("/prototype")
public String testPrototype() {
// Recuperar 2 instâncias
ReportGenerator report1 = context.getBean(ReportGenerator.class);
ReportGenerator report2 = context.getBean(ReportGenerator.class);
// Modificar report1
report1.addData("Item 1");
report1.addData("Item 2");
// Verificar os 2
return "Report1: " + report1.getReport() + "<br>" +
"Report2: " + report2.getReport() + "<br>" +
"(São instâncias DIFERENTES)";
}
}
Teste:
http://localhost:8080/api/test/prototype
Resultado:
Report1: Report REPORT-1234567890 criado em 2025-11-28T10:00:00 com 2 itens
Report2: Report REPORT-1234567891 criado em 2025-11-28T10:00:01 com 0 itens
✅ Instâncias DIFERENTES! Cada uma tem seu próprio estado.
Exercício 2: Ciclo de Vida com @PostConstruct e @PreDestroy
Objetivo
Executar código após a injeção e antes da destruição.
Etapa 2.1: Criar CacheService com Hooks de Ciclo de Vida
Arquivo: service/CacheService.java
package com.example.beans.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class CacheService {
private Map<String, String> cache;
// Construtor
public CacheService() {
System.out.println("1. Construtor CacheService chamado");
// ⚠️ Não inicialize aqui se depender de outros beans!
}
// Chamado APÓS a injeção de todas as dependências
@PostConstruct
public void init() {
System.out.println("2. @PostConstruct - Inicialização do cache");
cache = new HashMap<>();
cache.put("key1", "value1");
cache.put("key2", "value2");
System.out.println(" Cache inicializado com " + cache.size() + " elementos");
}
// Chamado ANTES da destruição do bean
@PreDestroy
public void cleanup() {
System.out.println("3. @PreDestroy - Limpando o cache");
if (cache != null) {
System.out.println(" Salvando " + cache.size() + " elementos");
cache.clear();
}
System.out.println(" Cache limpo");
}
public void put(String key, String value) {
cache.put(key, value);
System.out.println("Adicionado ao cache: " + key + " = " + value);
}
public String get(String key) {
return cache.get(key);
}
public int size() {
return cache.size();
}
}
Etapa 2.2: Testar
Adicione em TestController.java:
private final CacheService cacheService;
@GetMapping("/lifecycle")
public String testLifecycle() {
cacheService.put("test", "valor");
return "Cache size: " + cacheService.size() + " - Veja o console para o ciclo de vida";
}
Inicie a aplicação:
Console na inicialização:
1. Construtor CacheService chamado
2. @PostConstruct - Inicialização do cache
Cache inicializado com 2 elementos
Teste:
http://localhost:8080/api/test/lifecycle
Console:
Adicionado ao cache: test = valor
Pare a aplicação (Ctrl+C):
Console ao parar:
3. @PreDestroy - Limpando o cache
Salvando 3 elementos
Cache limpo
✅ Ciclo de vida completo testado!
Exercício 3: Criar Beans com @Bean e @Configuration
Objetivo
Criar Beans para classes que você não controla (bibliotecas externas).
Etapa 3.1: Criar a Configuração
Arquivo: config/AppConfig.java
package com.example.beans.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.format.DateTimeFormatter;
import java.util.Random;
@Configuration
public class AppConfig {
// Bean 1: DateTimeFormatter (classe Java padrão)
@Bean
public DateTimeFormatter dateFormatter() {
System.out.println("Criação do Bean dateFormatter");
return DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
}
// Bean 2: Random (classe Java padrão)
@Bean
public Random randomGenerator() {
System.out.println("Criação do Bean randomGenerator");
return new Random();
}
// Bean 3: Bean personalizado com dependência
@Bean
public ReportFormatter reportFormatter(DateTimeFormatter dateFormatter) {
System.out.println("Criação do Bean reportFormatter");
// O Spring injeta automaticamente dateFormatter!
return new ReportFormatter(dateFormatter);
}
}
Etapa 3.2: Criar ReportFormatter
Arquivo: service/ReportFormatter.java
package com.example.beans.service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ReportFormatter {
private final DateTimeFormatter dateFormatter;
public ReportFormatter(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
public String formatReport(String title, String content) {
String timestamp = LocalDateTime.now().format(dateFormatter);
return String.format("=== %s ===\nDate: %s\n%s",
title, timestamp, content);
}
}
Etapa 3.3: Testar
Adicione em TestController.java:
private final Random random;
private final ReportFormatter reportFormatter;
@GetMapping("/config-beans")
public String testConfigBeans() {
int randomNumber = random.nextInt(100);
String report = reportFormatter.formatReport(
"Test Report",
"Número aleatório: " + randomNumber
);
return report.replace("\n", "<br>");
}
Teste:
http://localhost:8080/api/test/config-beans
Resultado:
=== Test Report ===
Date: 28/11/2025 10:30:45
Número aleatório: 42
✅ Beans criados via @Configuration funcionando!
Exercício 4: Beans Condicionais (Bônus)
Objetivo
Criar Beans apenas em certas condições.
Etapa 4.1: Bean Condicional Simples
Adicione em AppConfig.java:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@Configuration
public class AppConfig {
// ... outros beans ...
// Bean criado apenas se a propriedade estiver definida
@Bean
@ConditionalOnProperty(name = "app.feature.advanced", havingValue = "true")
public String advancedFeature() {
return "Feature Avançada Ativada!";
}
}
Etapa 4.2: Ativar a Feature
Arquivo: src/main/resources/application.properties
app.feature.advanced=true
Etapa 4.3: Testar
Adicione em TestController.java:
import org.springframework.beans.factory.annotation.Autowired;
@RestController
@RequestMapping("/api/test")
public class TestController {
@Autowired(required = false) // ← Opcional!
private String advancedFeature;
@GetMapping("/conditional")
public String testConditional() {
if (advancedFeature != null) {
return "✅ " + advancedFeature;
} else {
return "❌ Feature avançada desativada";
}
}
}
Teste com app.feature.advanced=true:
✅ Feature Avançada Ativada!
Teste com app.feature.advanced=false:
❌ Feature avançada desativada
Recapitulação
O que Você Aprendeu
| Conceito | Sintaxe | Uso |
|---|---|---|
| Singleton | @Service (padrão) | Serviços sem estado, 1 instância |
| Prototype | @Scope("prototype") | Objetos com estado, N instâncias |
| @PostConstruct | Método anotado | Inicialização após injeção |
| @PreDestroy | Método anotado | Limpeza antes da destruição |
| @Bean | Dentro de @Configuration | Classes externas, config complexa |
| Bean condicional | @ConditionalOnProperty | Beans conforme configuração |
Comparação Singleton vs Prototype
// Singleton (padrão)
@Service
public class SingletonService {
private int state = 0; // ⚠️ Compartilhado!
}
// Prototype
@Service
@Scope("prototype")
public class PrototypeService {
private int state = 0; // ✅ Próprio de cada instância
}
Quando Usar Cada Um?
Singleton ✅ (padrão):
- Serviços sem estado
- Repositories
- Controllers
- 99% dos casos!
Prototype ⚠️ (raro):
- Objetos com estado mutável
- Objetos temporários
- Relatórios, processadores de arquivos
Autoavaliação
- Qual a diferença entre Singleton e Prototype?
- Quando são chamados @PostConstruct e @PreDestroy?
- Por que usar @Bean ao invés de @Service?
- Como criar um Bean condicional?
Desafio Bônus 🎯
Crie um sistema de cache com TTL (Time To Live):
@Service @Scope("singleton")CacheManager- Método
put(key, value, ttlSeconds) - Método
get(key)que retorna null se expirado @PostConstructpara inicializar@PreDestroypara salvar o cache
Bônus: Use @Scheduled para limpar automaticamente as entradas expiradas.
Próximo Passo
Avance para o TP3 para aprender sobre configuração por profiles!
➡️ TP5.3: Configuração por Profiles