Pular para o conteúdo principal

TP5.2 - Beans e Escopos

Objetivos​

Ao final deste exercício, você será capaz de:

  1. ✅ Criar Beans com @Bean e @Configuration
  2. ✅ Entender a diferença entre Singleton e Prototype
  3. ✅ Utilizar @PostConstruct e @PreDestroy
  4. ✅ 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ão
  • counter compartilhado 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 reportId e data

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​

ConceitoSintaxeUso
Singleton@Service (padrão)Serviços sem estado, 1 instância
Prototype@Scope("prototype")Objetos com estado, N instâncias
@PostConstructMétodo anotadoInicialização após injeção
@PreDestroyMétodo anotadoLimpeza antes da destruição
@BeanDentro de @ConfigurationClasses externas, config complexa
Bean condicional@ConditionalOnPropertyBeans 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​

  1. Qual a diferença entre Singleton e Prototype?
  2. Quando são chamados @PostConstruct e @PreDestroy?
  3. Por que usar @Bean ao invés de @Service?
  4. Como criar um Bean condicional?

Desafio Bônus 🎯​

Crie um sistema de cache com TTL (Time To Live):

  1. @Service @Scope("singleton") CacheManager
  2. Método put(key, value, ttlSeconds)
  3. Método get(key) que retorna null se expirado
  4. @PostConstruct para inicializar
  5. @PreDestroy para 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

⬅️ TP anterior: Injeção Avançada

⬅️ Voltar ao Módulo 5