Ciclo de vida e escopos de beans

TL;DR

Todo bean Spring passa por fases ordenadas — instanciação, injeção de propriedades, callbacks Aware, BeanPostProcessor, inicialização (@PostConstruct) e, ao final, destruição (@PreDestroy). O escopo define quantas instâncias existem e por quanto tempo: singleton (padrão) cria uma única; prototype cria uma nova a cada solicitação; request/session/application/websocket limitam a instância à duração do contexto web correspondente. O ponto de tensão clássico é injetar um bean de vida curta (prototype, request) em um singleton — a solução passa por ObjectProvider, @Lookup ou scoped proxy com proxyMode.

O que é

O ciclo de vida de um bean é a sequência de etapas que o container executa desde a criação até a destruição do objeto. O escopo complementa essa ideia definindo a política de reúso: quantas instâncias coexistem e qual é o gatilho para criar ou descartar cada uma.

Juntos, ciclo de vida e escopo determinam quando o código de inicialização roda, se o bean é compartilhado entre threads, se precisa ser thread-safe e como recursos são liberados.

Por que importa

Um serviço que abre uma conexão no construtor e nunca a fecha vaza recursos silenciosamente. Um singleton que guarda estado mutável por requisição tem condição de corrida. Um prototype injetado ingenuamente em um singleton é sempre o mesmo objeto — exatamente o oposto do esperado.

Entender o ciclo de vida resolve o “onde coloco este código de inicialização?” (@PostConstruct, não no construtor). Entender escopos resolve o “por que meu bean de sessão tem dados de outro usuário?” (singleton injetando session-scoped sem proxy).

Como funciona

O ciclo de vida (instanciação → populate → *Aware → BeanPostProcessor → @PostConstruct/init → BPP → pronto → @PreDestroy/destroy)

O container executa estas etapas em ordem para cada bean gerenciado:

  1. Instanciação — o construtor é chamado.
  2. Populate properties — dependências e @Value são injetados via setter ou campo.
  3. Interfaces Aware — o container chama setBeanName(), setApplicationContext() e outras interfaces Aware que o bean implementar, entregando infraestrutura interna.
  4. BeanPostProcessor.postProcessBeforeInitialization() — ponto de extensão antes da inicialização (usado por AOP, @Autowired, etc.).
  5. Inicialização — executa nesta ordem: @PostConstruct, InitializingBean.afterPropertiesSet(), init-method customizado.
  6. BeanPostProcessor.postProcessAfterInitialization() — ponto de extensão após a inicialização; é aqui que proxies AOP são criados.
  7. Bean pronto — disponível para uso pela aplicação.
  8. Destruição — executa nesta ordem: @PreDestroy, DisposableBean.destroy(), destroy-method customizado.

Callbacks recomendados

Prefira @PostConstruct e @PreDestroy (JSR-250): são padrão Jakarta, não acoplam o código ao Spring e são suficientes para a grande maioria dos casos.

Inicialização e proxies AOP

Os callbacks de inicialização rodam no bean bruto, antes do proxy AOP ser criado. Isso é intencional: garante que a inicialização opere no objeto real, não no proxy.

Escopos (singleton default, prototype, request/session/application/websocket)

EscopoInstânciasDuraçãoContexto
singletonUma por containerVida do containerQualquer
prototypeUma por solicitaçãoAté o cliente descartarQualquer
requestUma por requisição HTTPDuração da requisiçãoWeb
sessionUma por sessão HTTPDuração da sessãoWeb
applicationUma por ServletContextVida da aplicação webWeb
websocketUma por sessão WebSocketDuração da sessão WSWeb/STOMP

singleton é o padrão. Significa “um por container Spring”, não “um por JVM” (distinção em relação ao GoF Singleton).

prototype entrega uma nova instância a cada getBean() ou injeção. O container não chama callbacks de destruição em prototypes — a responsabilidade de limpeza é do código cliente.

Os escopos web (request, session, application, websocket) só funcionam em contextos que expõem o estado HTTP ao container (ex.: com DispatcherServlet, RequestContextListener ou RequestContextFilter). No Spring Boot, isso é configurado automaticamente.

Exemplo de declaração:

@RequestScope
@Component
public class CarrinhoDaRequisicao {
    private final List<String> itens = new ArrayList<>();
    // Nova instância por request; descartada ao final
}
 
@SessionScope
@Component
public class PreferenciasDoUsuario {
    private String idioma = "pt-BR";
    // Uma instância por sessão HTTP
}

Prototype em singleton: o problema e as saídas (ObjectProvider, @Lookup, scoped proxy)

Quando um bean singleton declara uma dependência prototype, o Spring a injeta uma única vez na criação do singleton. A partir daí, o singleton reutiliza sempre a mesma instância — o comportamento prototype se perde.

@Component
public class Processador {         // singleton (padrão)
 
    @Autowired
    private Tarefa tarefa;         // prototype — mas injetado apenas uma vez!
 
    public void processar() {
        tarefa.executar();         // sempre a mesma instância: bug silencioso
    }
}

Há três saídas:

Saída 1 — ObjectProvider<T> (recomendada para prototypes)

@Component
public class Processador {
 
    private final ObjectProvider<Tarefa> tarefaProvider;
 
    public Processador(ObjectProvider<Tarefa> tarefaProvider) {
        this.tarefaProvider = tarefaProvider;
    }
 
    public void processar() {
        Tarefa tarefa = tarefaProvider.getObject(); // nova instância a cada chamada
        tarefa.executar();
    }
}

Saída 2 — @Lookup (injeção de método)

@Component
public abstract class Processador {
 
    public void processar() {
        Tarefa tarefa = criarTarefa();
        tarefa.executar();
    }
 
    @Lookup
    protected abstract Tarefa criarTarefa(); // Spring gera a implementação via CGLIB
}

Saída 3 — scoped proxy (para escopos web: request/session)

@RequestScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class ContextoDaRequisicao {
    // ...
}

O proxy é injetado no singleton; cada chamada ao proxy é roteada para a instância real do escopo corrente. Essa é a abordagem padrão para escopos web injetados em singletons.

Na prática

Cenário: serviço que abre um recurso na inicialização e o libera ao encerrar, combinado com uso de ObjectProvider para um bean prototype.

@Component
public class RelatorioService {
 
    private final ObjectProvider<RelatorioBuilder> builderProvider;
    private RelatorioConfig config;
 
    public RelatorioService(ObjectProvider<RelatorioBuilder> builderProvider) {
        this.builderProvider = builderProvider;
    }
 
    @PostConstruct
    public void init() {
        // Roda após injeção de dependências; container já está pronto
        this.config = RelatorioConfig.carregar();
        System.out.println("RelatorioService inicializado com config: " + config);
    }
 
    public String gerar(String tipo) {
        // Nova instância de RelatorioBuilder por chamada (escopo prototype)
        RelatorioBuilder builder = builderProvider.getObject();
        return builder.tipo(tipo).config(config).build();
    }
 
    @PreDestroy
    public void encerrar() {
        // Roda antes de o container descartar o bean
        if (config != null) {
            config.fechar();
        }
        System.out.println("RelatorioService encerrado");
    }
}

Pontos relevantes:

  • @PostConstruct garante que config seja carregado depois que todas as dependências foram injetadas.
  • @PreDestroy libera o recurso de forma ordenada quando o contexto fecha.
  • ObjectProvider.getObject() entrega uma instância nova de RelatorioBuilder a cada chamada — correto para um bean prototype stateful.

Armadilhas

(1) Estado mutável em singleton sem thread-safety

Singletons são compartilhados entre threads. Campos mutáveis sem sincronização provocam condições de corrida sutis.

@Service
public class ContadorService {          // singleton
    private int total = 0;             // PERIGOSO: threads concorrentes
 
    public void incrementar() {
        total++;                        // não é operação atômica
    }
}

Fix: use AtomicInteger, volatile com operações adequadas, ou redesenhe o serviço para ser stateless. Ver Concorrência e paralelismo.

(2) Prototype injetado ingenuamente em singleton

Como descrito na seção anterior, a injeção direta de um prototype em um singleton congela a instância. O sintoma típico é: estado acumulado entre chamadas quando deveria ser zerado.

@Autowired
private Tarefa tarefa; // prototype — mas o Spring injeta só na criação do singleton

Fix: use ObjectProvider<Tarefa>, @Lookup ou recupere pelo ApplicationContext.

(3) Lógica pesada no construtor em vez de @PostConstruct

O construtor roda antes de o container injetar dependências. Qualquer lógica que dependa de campos injetados lança NullPointerException ou usa valores ainda nulos.

@Service
public class RelatorioService {
 
    @Autowired
    private RelatorioDao dao;
 
    public RelatorioService() {
        List<?> dados = dao.listarTodos(); // ERRO: dao ainda é null aqui
    }
}

Fix: mova a lógica de inicialização para um método @PostConstruct. Para operações longas (carregamento de cache volumoso, conexões externas), considere SmartInitializingSingleton ou @EventListener(ContextRefreshedEvent.class) para não bloquear o lock de criação de singletons.

Em entrevista

Frase pronta (inglês)

“Every Spring bean goes through a well-defined lifecycle: instantiation, dependency injection, Aware callbacks, BeanPostProcessor hooks, then the init callback — @PostConstruct being the recommended one — and finally destruction via @PreDestroy. Scope controls how many instances exist: singleton gives one per container, prototype gives a new one per request, and the web scopes tie the instance to an HTTP request, session, or the ServletContext. The classic interview trap is the prototype-in-singleton problem: when a singleton holds a reference to a prototype bean, the prototype is injected only once and reused forever, defeating its purpose. The clean solutions are ObjectProvider<T>, which lets you pull a fresh instance on demand, @Lookup method injection, or a scoped proxy with proxyMode = TARGET_CLASS for web-scoped beans injected into singletons.”

Vocabulário

Termo PTTermo EN
ciclo de vida do beanbean lifecycle
escopo do beanbean scope
singleton (por container)singleton (per-container)
prototypeprototype
escopo de requisiçãorequest scope
escopo de sessãosession scope
proxy com escoposcoped proxy
callback de inicializaçãoinitialization callback
callback de destruiçãodestruction callback
injeção de métodomethod injection
provedor de objetoobject provider
interface conscienteAware interface

Veja também

Referências