
002 · 2026-07-24
Spring Boot: container, beans e ciclo de vida
Sumário
Quem começa em Spring costuma aprender as anotações antes de entender o que realmente acontece por trás delas. Este post é a introdução que eu queria ter lido antes: o que é o container, o que é um bean e como o Spring gerencia o ciclo de vida deles, com um pouco mais de profundidade do que a introdução padrão costuma dar.
O container de IoC
Spring é, na essência, um container de Inversão de Controle (IoC). Em vez
de o seu código criar e conectar objetos manualmente com new, você descreve
os componentes da aplicação e deixa o container instanciá-los, configurá-los e
conectá-los entre si.
A interface central é a ApplicationContext. Ela lê a configuração (anotações,
classes @Configuration ou XML, hoje em dia quase sempre as duas primeiras),
constrói o grafo de dependências e mantém os objetos gerenciados durante todo
o ciclo de vida da aplicação.
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);Na prática, com Spring Boot você quase nunca instancia o contexto
manualmente. Quem faz isso por trás dos panos, ao rodar
SpringApplication.run(...), é o próprio @SpringBootApplication.
Antes de qualquer bean existir
Antes de o container instanciar o primeiro bean "normal" da aplicação, ele já
processou toda a configuração através de um BeanFactoryPostProcessor.
Diferente do BeanPostProcessor (que atua sobre instâncias já criadas, como
você vai ver na seção de ciclo de vida mais abaixo), o BeanFactoryPostProcessor
atua sobre as definições de bean, a metadata que descreve como cada bean
deveria ser construído, antes de qualquer bean ser de fato instanciado.
O exemplo mais comum é o PropertySourcesPlaceholderConfigurer, responsável
por resolver placeholders como ${jdbc.url} nas definições de bean,
trocando-os pelos valores vindos do application.properties ou de outras
fontes de propriedade. Quando você usa @Value("${minha.propriedade}"), é
esse mecanismo que está trabalhando por baixo.
@Configuration
public class AppConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholder() {
return new PropertySourcesPlaceholderConfigurer();
}
}Repare no static no método @Bean. Isso é necessário porque um
BeanFactoryPostProcessor precisa ser instanciado muito cedo, antes de o
container processar as anotações @Configuration normalmente. Declará-lo
como método estático evita que a classe de configuração inteira acabe sendo
inicializada antes da hora.
O que é um bean
Um bean é qualquer objeto cujo ciclo de vida é gerenciado pelo container. As formas mais comuns de declarar um são:
@Component
public class UserService {
// Spring detecta via component scan e registra como bean
}@Configuration
public class AppConfig {
@Bean
public UserService userService(UserRepository repository) {
return new UserService(repository);
}
}@Service, @Repository e @Controller são especializações de @Component,
semanticamente diferentes, mas tecnicamente idênticas para o container.
Escopos de bean
Por padrão, todo bean é singleton: uma única instância por container, compartilhada entre todos os pontos que a injetam. Os escopos mais comuns são:
singleton(padrão): uma instância por container.prototype: uma nova instância a cada injeção ou chamada agetBean.request/session: escopo web, uma instância por requisição ou sessão HTTP.
@Component
@Scope("prototype")
public class RelatorioBuilder {
// uma instância nova sempre que for injetado
}Um problema comum aparece quando um bean singleton depende de um bean
request ou session. Como o singleton só é criado uma vez, ele não pode
simplesmente injetar uma instância desses escopos menores, que mudam a cada
requisição. A solução do Spring é o proxyMode: em vez de injetar o bean
real, o container injeta um proxy que resolve a instância correta do escopo
menor toda vez que um método é chamado.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CarrinhoDeCompras {
// o singleton que injeta este bean recebe um proxy,
// não a instância real do request atual
}O ciclo de vida de um bean
Esse é o ponto que costuma confundir mais gente. O container não apenas cria o bean e esquece dele, existe uma sequência bem definida de etapas:
- Instanciação: o construtor é chamado.
- Injeção de dependências: propriedades e construtores anotados são preenchidos.
- Callbacks de "aware": se o bean implementar interfaces como
BeanNameAwareouApplicationContextAware, o Spring injeta essas informações antes da inicialização. - Pós-processamento (antes da inicialização):
BeanPostProcessor.postProcessBeforeInitialization. - Inicialização, nesta ordem: método anotado com
@PostConstruct, depoisafterPropertiesSet()(se o bean implementarInitializingBean), depois oinit-methodcustomizado, se declarado. - Pós-processamento (depois da inicialização):
BeanPostProcessor.postProcessAfterInitialization. É aqui, por exemplo, que proxies AOP costumam ser criados. - Bean pronto para uso: fica disponível no container até o contexto ser fechado.
- Destruição, ao encerrar o contexto:
@PreDestroy, depoisdestroy()(se implementarDisposableBean), depois odestroy-methodcustomizado.
@Component
public class ConexaoExterna implements InitializingBean, DisposableBean {
@PostConstruct
public void aoIniciar() {
System.out.println("1. @PostConstruct");
}
@Override
public void afterPropertiesSet() {
System.out.println("2. afterPropertiesSet");
}
@PreDestroy
public void antesDeFechar() {
System.out.println("3. @PreDestroy");
}
@Override
public void destroy() {
System.out.println("4. destroy()");
}
}Um detalhe pouco divulgado, mas descrito na documentação oficial: os callbacks
de inicialização, incluindo o @PostConstruct, rodam dentro do lock de
criação do singleton, e o bean só é considerado totalmente pronto depois que
esse callback retorna. Na prática isso significa que buscar outro bean que
também está em criação de dentro de um @PostConstruct pode travar a
inicialização da aplicação inteira. Se um bean precisa esperar outros beans
estarem completamente prontos antes de rodar alguma lógica, o caminho mais
seguro é ouvir o evento ApplicationReadyEvent em vez de fazer isso dentro
do próprio @PostConstruct.
Na prática, o dia a dia usa quase sempre só @PostConstruct e @PreDestroy.
As interfaces InitializingBean/DisposableBean acoplam o código ao Spring e
são mais úteis para quem escreve bibliotecas do que aplicações finais.
Ordem entre beans: depends-on e SmartLifecycle
Tudo isso cobre o ciclo de vida de um bean isolado, mas uma aplicação real tem
dezenas ou centenas deles, e a ordem entre eles também importa. Por padrão, o
Spring decide essa ordem pelo grafo de dependências: se o bean A depende do
bean B, B é criado e inicializado antes de A, e destruído depois dele. Quando
não existe uma dependência direta mas a ordem ainda importa, dá para forçar
isso com @DependsOn:
@Component
@DependsOn("conexaoExterna")
public class ServicoDeMigracao {
// garante que "conexaoExterna" já foi inicializado antes deste bean
}Para beans que precisam de um start()/stop() explícito, como um listener
de fila ou um scheduler, o Spring oferece a interface SmartLifecycle. Ela
adiciona o conceito de fase, via getPhase(): fases menores sobem primeiro e
descem por último, fases maiores sobem por último e descem primeiro. É assim
que um listener de mensageria consegue subir só depois que a infraestrutura
de banco de dados já está pronta, e parar antes dela no shutdown.
Por que isso importa
Entender esse ciclo evita bugs sutis: dependências que ainda não foram
injetadas quando o construtor roda, conexões que vazam porque o cleanup nunca
foi implementado, beans prototype usados como se fossem singleton, ou um
@PostConstruct que trava a subida da aplicação ao tentar acessar outro bean
ainda em criação. Quando um bean se comporta de um jeito que não bate com o
esperado, geralmente dá para apontar uma etapa específica desse ciclo como
causa, em vez de tratar como uma mágica desconhecida do framework.