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:

  1. Instancia o objeto chamando o construtor escolhido, depois de resolver cada um dos seus argumentos. Se o construtor de A precisa de B, B é criado por completo antes de o construtor de A rodar.
  2. Preenche os pontos de injeção por setter e por campo (@Autowired, @Value, @Resource).
  3. 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

  1. Cada BeanPostProcessor registrado recebe postProcessBeforeInitialization(bean, name). É aqui que fica o processor que trata @PostConstruct.
  2. Os métodos @PostConstruct rodam, depois InitializingBean.afterPropertiesSet() e, em seguida, o método indicado em @Bean(initMethod = "…").
  3. Cada BeanPostProcessor recebe postProcessAfterInitialization(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.

ScopeInstânciasTempo de vidaDisponível em
singleton (padrão)Uma por containerO contexto inteiroQualquer lugar
prototypeUma por injeção ou por getBeanAté deixar de ser referenciada; sem callback de destruiçãoQualquer lugar
requestUma por requisição HTTPA requisiçãoContextos web
sessionUma por sessão HTTPA sessãoContextos web
applicationUma por ServletContextA aplicação webContextos web
websocketUma por sessão WebSocketA sessãoAplicaçõ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; @PostConstruct roda depois da injeção, nunca antes.
  • postProcessAfterInitialization pode 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.
  • @Import compõe a configuração, @Profile seleciona por ambiente, @PropertySource e @Value leem configurações, e @Conditional é o interruptor geral sobre o qual a autoconfiguração do Spring Boot é construída.
  • @Lazy e @DependsOn sã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.