Spring Framework
Spring: ciclo de vida dos beans, scopes e configuração avançada
Intermediário9 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Saber quando o container cria, inicializa e destrói um bean explica a maioria dos 'por que isto está null?' e 'por que isto roda duas vezes?'. Este guia percorre o ciclo de vida fase a fase, compara os scopes singleton, prototype e web e seus proxies, e apresenta a maquinaria de configuração — @Import, @Profile, @Value, @PropertySource, @Conditional, @Lazy — sobre a qual o Spring Boot constrói sua autoconfiguração.
O que você vai aprender
- Acompanhar um bean da definição à destruição e situar cada callback na fase certa
- Escolher o scope correto e saber quando um scoped proxy é necessário
- Injetar um prototype em um singleton sem congelar uma única instância
- Dividir a configuração com @Import, @Profile e @PropertySource e ler valores com @Value e Environment
- Escrever um BeanPostProcessor e explicar como o Spring usa esse mesmo gancho para criar proxies
A injeção de dependências diz o que um bean recebe. O ciclo de vida diz quando, e o scope diz quantos. Quase todo comportamento confuso do Spring — um campo injetado que está null dentro do construtor, um método de inicialização que roda duas vezes, um bean com scope request que quebra a inicialização, um prototype que nunca muda — é, no fundo, uma questão de ciclo de vida ou de scope. Este guia tem como foco o Spring Framework 6.x e parte do princípio de que você já sabe declarar beans com configuração Java.
O ciclo de vida, fase a fase
A vida de um bean se divide claramente em fases do container e fases da instância.
Fase de definição
Quando um ApplicationContext é atualizado (refresh), ele primeiro coleta as definições de beans — objetos de metadados que guardam a classe, o scope, os argumentos do construtor, os valores das propriedades, os nomes dos métodos de inicialização e destruição e a flag lazy. Nada foi instanciado ainda. É nesse momento que rodam as implementações de BeanFactoryPostProcessor, que podem adicionar, remover ou modificar definições. O mecanismo de placeholders que resolve ${db.url} na configuração é um deles; os import selectors da autoconfiguração do Spring Boot são outro.
Instanciação e preenchimento das propriedades
Para cada singleton não lazy, em uma ordem que respeita as dependências, o container então:
- Instancia o objeto chamando o construtor escolhido, depois de resolver cada um dos seus argumentos. Se o construtor de
Aprecisa deB,Bé criado por completo antes de o construtor deArodar. - Preenche os pontos de injeção por setter e por campo (
@Autowired,@Value,@Resource). - Chama as interfaces Aware, se implementadas:
BeanNameAware.setBeanName,BeanFactoryAware,ApplicationContextAware,EnvironmentAware,ResourceLoaderAware. Elas dão ao bean acesso às entranhas do container; são úteis em código de framework e um mau sinal em código de negócio.
Inicialização
- Cada
BeanPostProcessorregistrado recebepostProcessBeforeInitialization(bean, name). É aqui que fica o processor que trata@PostConstruct. - Os métodos
@PostConstructrodam, depoisInitializingBean.afterPropertiesSet()e, em seguida, o método indicado em@Bean(initMethod = "…"). - Cada
BeanPostProcessorrecebepostProcessAfterInitialization(bean, name). É aqui que acontece o auto-proxying de AOP: o processor pode devolver um objeto diferente — um proxy —, e é esse proxy, e não a sua instância, que vai para o registro de singletons e é injetado em outros lugares.
O bean agora está em uso. Beans SmartLifecycle recebem ainda start() quando o contexto inteiro termina de ser atualizado, que é o gancho certo para uma lógica do tipo "começar a consumir mensagens", que precisa de todos os outros beans prontos.
Destruição
Quando close() é chamado no contexto (ou quando dispara o shutdown hook da JVM registrado por registerShutdownHook()), os singletons são destruídos na ordem inversa das dependências: @PreDestroy, depois DisposableBean.destroy() e, por fim, o método @Bean(destroyMethod). Para métodos @Bean, o Spring infere automaticamente um método de destruição chamado close ou shutdown, então um bean HikariDataSource é fechado sem nenhuma configuração.
@Component
public class IndexWarmer implements SmartLifecycle {
private final SearchIndex index;
private volatile boolean running;
public IndexWarmer(SearchIndex index) { this.index = index; }
@PostConstruct
void checkConfig() { // injection is complete; safe to use `index`
index.validate();
}
@Override public void start() { index.warm(); running = true; } // whole context is ready
@Override public void stop() { running = false; }
@Override public boolean isRunning() { return running; }
@PreDestroy
void release() { index.close(); }
}
A regra que resolve a maioria dos bugs de ciclo de vida: com injeção por campo ou por setter, os campos injetados não estão disponíveis no construtor. Eles estão disponíveis no @PostConstruct. Com injeção por construtor a questão desaparece, o que é mais um argumento a favor dela.
Scopes
Um scope define quantas instâncias de uma definição de bean existem e quanto tempo cada uma vive.
| Scope | Instâncias | Tempo de vida | Disponível em |
|---|---|---|---|
singleton (padrão) | Uma por container | O contexto inteiro | Qualquer lugar |
prototype | Uma por injeção ou por getBean | Até deixar de ser referenciada; sem callback de destruição | Qualquer lugar |
request | Uma por requisição HTTP | A requisição | Contextos web |
session | Uma por sessão HTTP | A sessão | Contextos web |
application | Uma por ServletContext | A aplicação web | Contextos web |
websocket | Uma por sessão WebSocket | A sessão | Aplicações WebSocket |
Singleton aqui significa um por container, não um por JVM: dois contextos têm duas instâncias, e uma classe pode ser registrada duas vezes com nomes diferentes. Como os singletons são compartilhados entre threads, eles precisam ser stateless ou thread-safe. Guardar dados de uma requisição em um campo de singleton é um bug clássico e grave.
O scope prototype e a armadilha do singleton
Declarar @Scope("prototype") faz o container criar uma instância nova toda vez que o bean é solicitado. A armadilha é que um singleton o solicita exatamente uma vez — na própria criação — e depois guarda essa instância para sempre. Há três saídas:
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ReportBuilder { /* holds per-report mutable state */ }
@Service
public class ReportService {
private final ObjectProvider<ReportBuilder> builders;
public ReportService(ObjectProvider<ReportBuilder> builders) { this.builders = builders; }
public Report build(Query q) {
ReportBuilder builder = builders.getObject(); // new instance each call
return builder.with(q).build();
}
}
ObjectProvider é a mais limpa. @Lookup em um método abstrato faz o Spring gerar uma subclasse cujo método devolve um bean novo. Um scoped proxy (veja abaixo) também funciona, mas foi pensado para os scopes web.
Scopes web e scoped proxies
Um bean com scope request não pode ser injetado diretamente em um singleton: na inicialização não existe requisição, portanto não há nada para injetar, e o container falha com uma mensagem dizendo que não há requisição vinculada à thread. A solução é injetar um proxy que resolve o objeto real de scope request a cada chamada de método:
@Component
@RequestScope // shorthand for @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestAudit {
private final List<String> events = new ArrayList<>();
public void record(String e) { events.add(e); }
public List<String> events() { return events; }
}
@Service
public class CheckoutService {
private final RequestAudit audit; // actually a CGLIB proxy
public CheckoutService(RequestAudit audit) { this.audit = audit; }
public void checkout() { audit.record("checkout"); }
}
@RequestScope, @SessionScope e @ApplicationScope configuram o modo de proxy para você. O proxy é uma subclasse do alvo (TARGET_CLASS) ou uma implementação de interface (INTERFACES); em ambos os casos, a classe precisa ser "proxiável" — não pode ser final e precisa de um construtor acessível. Beans com scope session devem ser Serializable se as sessões puderem ser persistidas ou replicadas.
Recursos de configuração que moldam o contexto
Dividir e importar
Aplicações reais espalham a configuração em várias classes, por responsabilidade: DataConfig, WebConfig, SecurityConfig. Junte-as com @Import, que é explícito e funciona fora dos pacotes escaneados:
@Configuration
@Import({DataConfig.class, MessagingConfig.class})
public class AppConfig { }
@Import também aceita um ImportSelector — uma classe que decide em tempo de execução quais configurações importar —, que é o mecanismo por trás de toda anotação @Enable… e da autoconfiguração do Spring Boot.
@Configuration(proxyBeanMethods = false) desliga a geração de subclasses via CGLIB descrita no guia de injeção de dependências. As chamadas entre métodos @Bean passam então a criar objetos novos em vez de devolver o gerenciado, então você precisa passar as dependências como parâmetros. As próprias autoconfigurações do Spring Boot usam esse modo "lite" para inicializar mais rápido, e ele é um padrão razoável para o seu código depois que você adota o estilo com parâmetros.
Perfis
Um perfil (profile) é uma fatia nomeada da configuração, ativada conforme o ambiente. Qualquer @Component, @Configuration ou @Bean pode levar @Profile:
@Configuration
public class DataConfig {
@Bean
@Profile("local")
DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
@Bean
@Profile("!local") // expressions: !, &, |
DataSource pooledDataSource(Environment env) {
var ds = new HikariDataSource();
ds.setJdbcUrl(env.getRequiredProperty("db.url"));
return ds;
}
}
Ative perfis com a propriedade de sistema ou a variável de ambiente spring.profiles.active, ou programaticamente com context.getEnvironment().setActiveProfiles("local") antes do refresh. Quando nenhum perfil está ativo, são registrados os beans do perfil default. Perfis servem para ambientes; não os use para ligar e desligar funcionalidades — para isso, a ferramenta certa é uma propriedade com @Conditional.
Propriedades, Environment e @Value
A abstração Environment unifica as fontes de propriedades — propriedades de sistema, variáveis de ambiente, arquivos de propriedades e o que mais você adicionar — em uma única ordem de busca. @PropertySource adiciona um arquivo:
@Configuration
@PropertySource("classpath:app.properties")
public class AppConfig {
@Bean
Mailer mailer(@Value("${mail.host}") String host,
@Value("${mail.port:587}") int port, // default after the colon
@Value("#{systemProperties['user.name']}") String user) { // SpEL
return new SmtpMailer(host, port, user);
}
}
@Value lida com valores simples, com conversão de tipo. Para configuração estruturada — um grupo de configurações relacionadas, com validação —, o @ConfigurationProperties do Spring Boot é muito melhor; em código puro do Framework, injete Environment e leia um prefixo, ou faça o binding manualmente. Nunca deixe segredos hardcoded em arquivos de propriedades versionados; leia-os do ambiente.
Condições e inicialização lazy
@Conditional(SomeCondition.class) registra um bean somente quando o método matches da condição retorna true. Escrever condições diretamente é raro em código de aplicação, mas entendê-las desmistifica o @ConditionalOnMissingBean do Boot — é o mesmo gancho, com uma condição já pronta.
@Lazy adia a criação de um singleton até o primeiro uso. É legítimo para um bean pesado que a maioria das requisições nunca toca, e como forma de quebrar uma referência circular teimosa injetando um proxy lazy. Não é uma estratégia de desempenho: ele transfere o custo da inicialização para a primeira requisição azarada e esconde erros de configuração até lá.
@DependsOn("flywayMigration") força uma ordem de criação onde não existe relação de injeção — o caso clássico é "rodar a migração do schema antes de a entity manager factory ser inicializada".
Estendendo o container
Um BeanPostProcessor enxerga cada bean depois da instanciação e pode envolvê-lo ou modificá-lo. O suporte do próprio Spring a @Transactional, @Cacheable, @Async e AOP é todo feito com post-processors que devolvem um proxy a partir de postProcessAfterInitialization. Escrever um post-processor bem simples você mesmo torna o mecanismo concreto:
@Component
public class TimingPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (!bean.getClass().isAnnotationPresent(Timed.class)) {
return bean; // leave everything else untouched
}
var factory = new ProxyFactory(bean); // spring-aop
factory.addAdvice((MethodInterceptor) invocation -> {
long start = System.nanoTime();
try {
return invocation.proceed();
} finally {
System.out.printf("%s took %d µs%n", invocation.getMethod().getName(),
(System.nanoTime() - start) / 1_000);
}
});
return factory.getProxy();
}
}
Disso decorrem duas consequências para quem usa proxies: o objeto injetado nos outros beans é o proxy, então bean.getClass() informa uma classe gerada, e as chamadas que um bean faz aos seus próprios métodos passam totalmente por fora do proxy. Os dois pontos são tratados em profundidade no guia de AOP.
Um FactoryBean<T> é o outro ponto de extensão clássico: um bean cuja função é produzir outro bean do tipo T, usado quando a construção é complexa demais para um método @Bean — LocalContainerEntityManagerFactoryBean e as factories de repositórios do Spring Data são exemplos. Pedir ao container o nome do bean devolve o produto; prefixar o nome com & devolve a própria factory.
Erros comuns
Usar um campo injetado no construtor. Com injeção por campo, ali o campo ainda está null. Mova a lógica para @PostConstruct ou, melhor ainda, passe a usar injeção por construtor.
Singletons com estado. Um @Service com um campo private List<Order> current é compartilhado por todas as threads de requisição. Mantenha o estado no escopo do método, em um bean com scope request ou em uma estrutura explícita e thread-safe.
Injetar um bean com scope request sem proxy. A inicialização falha porque não existe requisição. Use @RequestScope ou defina proxyMode.
Esperar callbacks de destruição em prototypes. Eles nunca disparam. Feche os recursos você mesmo.
Registrar um BeanPostProcessor que depende de beans comuns. Os post-processors são criados antes de todo o resto, então os beans que eles puxam são instanciados cedo demais e deixam de passar pelos outros post-processors — quando isso acontece, o Spring registra no log "is not eligible for getting processed by all BeanPostProcessors". Mantenha os post-processors sem dependências ou use ObjectProvider dentro deles.
Colocar @Profile em um método de uma configuração lite e esquecer a alternativa. Se todos os candidatos para um tipo dependem de perfil e nenhum perfil corresponde, a injeção falha na inicialização. Tenha sempre um padrão incondicional ou com !profile, ou torne a dependência opcional.
Principais conclusões
- Um bean passa pela definição, instanciação, preenchimento das propriedades, callbacks Aware, hooks dos post-processors, callbacks de inicialização e, por fim, pela destruição;
@PostConstructroda depois da injeção, nunca antes. postProcessAfterInitializationpode substituir um bean por um proxy — é assim que transações, cache e AOP se acoplam.- Singleton é uma instância por container, compartilhada entre threads; prototype é uma por solicitação ao container, sem callback de destruição; scopes web precisam de um scoped proxy para serem injetados em singletons.
- Injete
ObjectProvider<T>quando um singleton precisar de um prototype novo a cada chamada. @Importcompõe a configuração,@Profileseleciona por ambiente,@PropertySourcee@Valueleem configurações, e@Conditionalé o interruptor geral sobre o qual a autoconfiguração do Spring Boot é construída.@Lazye@DependsOnsão correções pontuais, não padrões; falhar rápido na inicialização é uma qualidade.
Fontes e leituras complementares
Perguntas frequentes
- O que roda primeiro, @PostConstruct ou afterPropertiesSet?
- @PostConstruct, depois InitializingBean.afterPropertiesSet e, por fim, o @Bean(initMethod) ou o init-method do XML. Os três rodam depois que a injeção de dependências termina e antes de o bean ser entregue a outros beans, então qualquer um deles pode usar com segurança os campos injetados. Use @PostConstruct, a não ser que você tenha um motivo para acoplar a classe às interfaces do Spring.
- Por que meu bean prototype é sempre a mesma instância?
- Porque ele foi injetado uma única vez em um singleton, e singletons são criados uma única vez. O container não tem oportunidade de criar um prototype novo a cada chamada. Injete ObjectProvider<T> e chame getObject() quando precisar de uma instância nova, ou declare o prototype com um scoped proxy para que cada chamada de método passe por um proxy que resolve um novo alvo.
- O Spring chama @PreDestroy em beans prototype?
- Não. O container cria o prototype, faz a injeção, executa os callbacks de inicialização e depois se esquece dele; como nunca rastreia a instância, não tem como destruí-la. Se um prototype segura recursos, a responsabilidade de liberá-los é do cliente que o obteve, ou você o registra em um bean post-processor próprio que rastreie as instâncias.
- Qual a diferença entre @Profile e @Conditional?
- @Profile é uma condição específica e embutida: o bean só é registrado quando um dos perfis indicados está ativo no Environment. @Conditional é o mecanismo geral — você fornece uma classe Condition que pode inspecionar qualquer coisa (uma propriedade, uma classe no classpath, um bean existente), e @ConditionalOnMissingBean, @ConditionalOnProperty e companhia, do Spring Boot, são todos construídos sobre ele.