lang://
Arquivo

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 a getBean.
  • 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:

  1. Instanciação: o construtor é chamado.
  2. Injeção de dependências: propriedades e construtores anotados são preenchidos.
  3. Callbacks de "aware": se o bean implementar interfaces como BeanNameAware ou ApplicationContextAware, o Spring injeta essas informações antes da inicialização.
  4. Pós-processamento (antes da inicialização): BeanPostProcessor.postProcessBeforeInitialization.
  5. Inicialização, nesta ordem: método anotado com @PostConstruct, depois afterPropertiesSet() (se o bean implementar InitializingBean), depois o init-method customizado, se declarado.
  6. Pós-processamento (depois da inicialização): BeanPostProcessor.postProcessAfterInitialization. É aqui, por exemplo, que proxies AOP costumam ser criados.
  7. Bean pronto para uso: fica disponível no container até o contexto ser fechado.
  8. Destruição, ao encerrar o contexto: @PreDestroy, depois destroy() (se implementar DisposableBean), depois o destroy-method customizado.
@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.