Spring Framework

Inversão de controle e injeção de dependência no Spring

Iniciante9 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

O trabalho central do Spring é montar o seu grafo de objetos para você. Ao final deste guia, você vai entender o que é o container IoC, como o ApplicationContext descobre e liga os beans, por que a injeção por construtor é o estilo recomendado, como resolver ambiguidades com qualifiers e a que corresponde, na configuração Java moderna, a configuração XML de projetos mais antigos.

O que você vai aprender

  • Explicar o que significa inversão de controle e por que um container compensa essa indireção
  • Declarar beans com @Configuration, @Bean e component scan
  • Escolher entre injeção por construtor, por setter e por campo e justificar a escolha
  • Resolver dependências ambíguas com @Qualifier, @Primary e injeção de coleções
  • Reconhecer configuração da era XML e traduzi-la para configuração Java

Toda aplicação não trivial é um grafo de objetos: um controller web precisa de um service, o service precisa de um repository, o repository precisa de um data source e o data source precisa de credenciais. Alguém tem de montar esse grafo, na ordem certa e com as implementações certas para o ambiente atual. A resposta do Spring Framework é um container que assume a criação dos objetos e entrega a cada um os colaboradores que ele declara. Essa única ideia — inversão de controle (IoC), concretizada por meio da injeção de dependência (DI) — é a base sobre a qual o resto do framework, e todo o Spring Boot, foi construído. Este guia trata do Spring Framework 6.x sobre Java 17 ou superior.

O problema que o container resolve

Imagine um service que envia confirmações de pedido. Sem um container, ele cria os próprios colaboradores:

public class OrderService {
    private final OrderRepository repository = new JdbcOrderRepository(DataSources.production());
    private final Mailer mailer = new SmtpMailer("smtp.internal", 587);

    public void confirm(long orderId) {
        Order order = repository.find(orderId);
        mailer.send(order.customerEmail(), "Order " + orderId + " confirmed");
    }
}

A classe funciona, mas tomou em silêncio quatro decisões que não são da conta dela: qual implementação de repository usar, de onde vem o data source, qual transporte de e-mail usar e como ele está configurado. Não dá para fazer teste unitário de confirm sem um banco de dados e um servidor SMTP. Não dá para trocar o mailer por um baseado em fila sem editar este arquivo. Toda classe que segue esse padrão vira uma pequena factory das próprias dependências, com tudo fixo no código.

A inversão de controle inverte a seta. OrderService deixa de procurar ou construir aquilo de que precisa e passa a declarar:

public class OrderService {
    private final OrderRepository repository;
    private final Mailer mailer;

    public OrderService(OrderRepository repository, Mailer mailer) {
        this.repository = repository;
        this.mailer = mailer;
    }

    public void confirm(long orderId) { /* unchanged */ }
}

Agora a classe depende de dois papéis expressos como interfaces, e algo de fora decide quais objetos concretos ocupam esses papéis. Em um teste, esse algo é você, passando fakes. Em produção, é o container do Spring. A própria classe não tem nenhum import do Spring — isso é proposital, e um bom sinal.

O que é o container

O container é um objeto que lê definições de beans — receitas que descrevem quais classes instanciar, como configurá-las e do que elas dependem — e as usa para montar um grafo de objetos gerenciados, chamados beans. A interface central é ApplicationContext. Quando você cria um, ele:

  1. Carrega as definições de beans das suas classes de configuração (ou de XML, ou de pacotes escaneados).
  2. Executa os bean factory post-processors, que podem alterar as definições (a resolução de placeholders de propriedades, por exemplo).
  3. Instancia cada bean singleton, resolvendo recursivamente as dependências de cada um, na ordem que elas impõem.
  4. Aplica os bean post-processors a cada nova instância (é aqui que entram @PostConstruct, os proxies de AOP e os interceptadores de transação).
  5. Publica um evento de refresh e fica pronto para atender chamadas a getBean — embora código bem projetado raramente chame getBean diretamente.

As duas implementações que você mais vai encontrar são AnnotationConfigApplicationContext, para uso standalone ou em testes, e AnnotationConfigWebApplicationContext (ou AnnotationConfigServletWebServerApplicationContext, no Boot) para aplicações web.

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    OrderService service = context.getBean(OrderService.class);
    service.confirm(42L);
}   // close() destroys singletons and runs @PreDestroy callbacks

Declarando beans com configuração Java

Uma classe de configuração é uma classe anotada com @Configuration cujos métodos @Bean produzem, cada um, um bean. O nome do método vira o nome do bean, a menos que você o sobrescreva.

@Configuration
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        var ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:postgresql://db:5432/shop");
        ds.setUsername("shop");
        ds.setPassword(System.getenv("DB_PASSWORD"));
        return ds;
    }

    @Bean
    public OrderRepository orderRepository(DataSource dataSource) {   // parameter is injected
        return new JdbcOrderRepository(dataSource);
    }

    @Bean
    public Mailer mailer() {
        return new SmtpMailer("smtp.internal", 587);
    }

    @Bean
    public OrderService orderService(OrderRepository repository, Mailer mailer) {
        return new OrderService(repository, mailer);
    }
}

Os parâmetros dos métodos são resolvidos por tipo a partir do container, então um método @Bean nunca precisa chamar outro método @Bean para obter uma dependência. Ele pode — orderRepository(dataSource()) — e o Spring ainda assim devolve a única instância gerenciada em vez de um objeto novo, porque as classes @Configuration são estendidas com CGLIB em tempo de execução para interceptar essas chamadas. Essa interceptação é conveniente, mas invisível, e é por isso que passar as dependências como parâmetros é o estilo mais claro.

Os métodos @Bean brilham com classes que não são suas: um pool de conexões, um cliente de terceiros, um Clock. Para as suas próprias classes, o component scan exige menos cerimônia.

Component scan

Anote suas classes com uma anotação de estereótipo e diga ao container quais pacotes escanear:

@Service
public class OrderService {
    private final OrderRepository repository;
    private final Mailer mailer;

    public OrderService(OrderRepository repository, Mailer mailer) {
        this.repository = repository;
        this.mailer = mailer;
    }
}

@Repository
public class JdbcOrderRepository implements OrderRepository { /* … */ }

@Configuration
@ComponentScan("com.example.shop")
public class AppConfig { /* only third-party beans left here */ }

@Component é o estereótipo genérico; @Service, @Repository, @Controller e @RestController são especializações. Na prática, são quase idênticos — um @Service escaneado é só um @Component —, mas @Repository também habilita a tradução de exceções de acesso a dados, e os demais documentam a intenção e permitem filtrar os scans. Escaneie o pacote mais estreito que contenha o seu código: escanear com ou uma string vazia puxa toda biblioteca do classpath que por acaso use estereótipos do Spring e deixa a inicialização mais lenta.

Os três estilos de injeção

O Spring pode injetar por um construtor, por um setter ou diretamente em um campo. Eles não são intercambiáveis.

EstiloComoPrósContras
ConstrutorAs dependências são parâmetros do construtorOs campos podem ser final; o objeto nunca fica construído pela metade; funciona sem Spring nos testes; excesso de parâmetros é um problema de design visívelUm pouco mais de digitação (nenhuma com records/Lombok)
Setter@Autowired em um setterDependências opcionais; reconfigurável depois da construçãoO objeto pode existir em estado inválido; mutabilidade
Campo@Autowired em um campo privadoO mais curtoNão pode ser final; invisível para quem chama; testes precisam de reflection ou do container; esconde listas de dependências que só crescem

O time do Spring recomenda injeção por construtor para dependências obrigatórias e injeção por setter para as opcionais. A injeção por campo aparece em inúmeros tutoriais porque é curta, mas acopla a classe a um container baseado em reflection e impossibilita um simples new em um teste unitário. Com um único construtor, @Autowired é desnecessário — o container o usa implicitamente.

Há várias formas de expressar dependências opcionais. Escolha uma e seja consistente:

@Service
public class ReportService {
    private final ReportStore store;
    private final Optional<AuditSink> audit;          // empty if no AuditSink bean
    private final ObjectProvider<Formatter> formatters; // lazy, may be many, may be none

    public ReportService(ReportStore store, Optional<AuditSink> audit, ObjectProvider<Formatter> formatters) {
        this.store = store;
        this.audit = audit;
        this.formatters = formatters;
    }

    void run() {
        audit.ifPresent(a -> a.record("report"));
        formatters.orderedStream().forEach(Formatter::apply);
    }
}

ObjectProvider também resolve um problema clássico: quando um singleton precisa de um bean novo com escopo prototype a cada chamada, provider.getObject() pede ao container toda vez, em vez de guardar uma única instância desatualizada.

Resolvendo ambiguidades

A injeção é por tipo. Quando dois beans implementam Mailer, o container não consegue escolher e falha na inicialização com NoUniqueBeanDefinitionException. Você tem quatro ferramentas:

  • @Primary em um dos candidatos faz dele o padrão quando nada mais específico é pedido.
  • @Qualifier("smtp") no ponto de injeção indica o bean desejado (pelo nome do bean, ou por uma anotação de qualifier personalizada que você mesmo define para ter type safety).
  • Fallback pelo nome do parâmetro: se um parâmetro se chama smtpMailer e existe um bean com esse nome, ele vence — algo frágil, porque uma renomeação quebra a ligação silenciosamente.
  • Injeção de coleções: declare List<Mailer> ou Map<String, Mailer> para receber todos os candidatos, ordenados por @Order ou Ordered.
@Bean @Primary Mailer smtpMailer() { … }
@Bean Mailer queueMailer() { … }

public NotificationService(@Qualifier("queueMailer") Mailer mailer) { … }

O Spring Framework 6.2 adicionou @Fallback, o espelho de @Primary: um bean escolhido apenas quando não existe nenhum outro candidato — útil para padrões de biblioteca que as aplicações podem sobrescrever.

Tipos genéricos também participam da resolução: Repository<Order> e Repository<Customer> são alvos de injeção distintos, então muitas vezes você não precisa de qualifier nenhum se as suas interfaces tiverem parâmetros de tipo.

Do XML para a configuração Java

Bases de código mais antigas, e a maior parte do material de treinamento de Spring 3/4, definem beans em XML:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context">
    <context:component-scan base-package="com.example.shop"/>
    <bean id="mailer" class="com.example.shop.SmtpMailer">
        <constructor-arg value="smtp.internal"/>
        <constructor-arg value="587"/>
    </bean>
    <bean id="orderService" class="com.example.shop.OrderService">
        <constructor-arg ref="orderRepository"/>
        <constructor-arg ref="mailer"/>
        <property name="retryCount" value="3"/>   <!-- setter injection -->
    </bean>
</beans>

O mapeamento é mecânico: <bean> vira um método @Bean, <constructor-arg ref> vira um parâmetro do método, <property> vira uma chamada de setter, <context:component-scan> vira @ComponentScan e <import resource> vira um @Import de outra classe de configuração. O XML continua sendo carregado por ClassPathXmlApplicationContext e pode ser combinado via @ImportResource("classpath:legacy-beans.xml"), que é o caminho sensato para uma migração incremental. A era XML também dependia das anotações @Resource, da JSR-250, e @Inject, da JSR-330; ambas ainda funcionam, agora a partir dos pacotes jakarta.annotation e jakarta.inject, mas @Autowired é a escolha idiomática.

Erros comuns

Chamar getBean no código da aplicação. Passar o ApplicationContext de um lado para o outro e buscar beans pelo nome reintroduz o acoplamento de service locator que a DI deveria eliminar. Injete o que você precisa; se o conjunto de beans for dinâmico, injete ObjectProvider<T> ou Map<String, T>.

Usar new em uma classe que tem anotações do Spring. Um objeto que você mesmo constrói não é gerenciado: nada de injeção, nada de @Transactional, nada de @PostConstruct. Isso aparece como um NullPointerException em um campo que "deveria ter sido autowired".

Escanear demais. Um @ComponentScan sem argumentos escaneia o pacote da classe de configuração e todos os subpacotes. Coloque a sua configuração raiz no topo da hierarquia de pacotes do seu código, nunca acima dele.

Misturar injeção por campo e construtores. Uma classe que recebe algumas dependências pelo construtor e outras por campos @Autowired é confusa e impossível de testar. Leve tudo para o construtor.

Dependências circulares em construtores. A(B) e B(A) falham na inicialização. Não mude para injeção por campo só para calar o erro; separe o comportamento compartilhado em um terceiro bean ou use ObjectProvider<A> no raro caso legítimo de ligação tardia.

Dois beans, nenhum qualifier. Adicionar uma segunda implementação de uma interface — digamos, um CachingProductRepository que envolve o de JDBC — quebra todos os pontos de injeção existentes daquele tipo. Marque o padrão desejado com @Primary no momento em que adicionar o segundo bean.

Boas práticas

  • Mantenha as classes de domínio e de service livres de imports do Spring sempre que puder. Um construtor e uma interface bastam; a anotação de estereótipo é a única coisa específica do Spring de que elas precisam, e até ela pode ir para um método @Bean.
  • Prefira um único construtor por classe, com campos final. Se um construtor passar de cinco ou seis parâmetros, a classe está fazendo coisa demais.
  • Use métodos @Bean para tipos de terceiros e component scan para os seus.
  • Dê nomes aos beans de forma deliberada sempre que um tipo puder ter mais de um, e prefira anotações de qualifier personalizadas a nomes em string em bases de código grandes.
  • Deixe a inicialização falhar rápido. Um bean ausente descoberto no boot sai muito mais barato do que um descoberto na primeira requisição em produção, então evite @Autowired(required = false) a menos que a dependência seja realmente opcional.

Principais pontos

  • Inversão de controle significa que os objetos declaram suas dependências e o container as fornece; a injeção de dependência é o mecanismo.
  • O ApplicationContext lê definições de beans das classes @Configuration e dos componentes escaneados, monta o grafo de singletons na inicialização e aplica post-processors a cada bean.
  • Injeção por construtor é o estilo recomendado: imutável, testável sem Spring e transparente quanto às dependências de uma classe.
  • A injeção é por tipo; use @Primary, @Qualifier ou injeção de coleções quando vários beans compartilham um tipo.
  • A configuração XML ainda funciona e se mapeia um a um para configuração Java, que é o que todo código novo deve usar.
  • Nunca dê new em uma classe gerenciada pelo Spring nem chame getBean no código da aplicação; as duas coisas contornam o container.

Fontes e leituras complementares

Perguntas frequentes

Qual é a diferença entre BeanFactory e ApplicationContext?
BeanFactory é a interface mínima do container: ela sabe criar e ligar beans sob demanda. ApplicationContext a estende com tudo de que uma aplicação precisa — criação antecipada de singletons, publicação de eventos, message sources para i18n, carregamento de recursos e registro automático de post-processors. O código da aplicação deve sempre usar um ApplicationContext; BeanFactory existe para restrições extremas de footprint e para o funcionamento interno do framework.
Ainda preciso de @Autowired em um construtor?
Não quando a classe tem exatamente um construtor. Desde o Spring Framework 4.3, o container usa implicitamente um construtor único. Você só precisa de @Autowired para escolher um construtor entre vários, ou nos pontos de injeção por setter e por campo.
Por que o Spring diz que um bean tem uma dependência circular?
O bean A precisa de B no construtor e B precisa de A no dele, então nenhum dos dois pode ser criado primeiro; o container lança BeanCurrentlyInCreationException. A injeção por setter ou por campo pode, tecnicamente, quebrar o ciclo, porque o Spring injeta em um singleton construído pela metade, mas o ciclo quase sempre indica responsabilidades embaralhadas. Em vez disso, extraia a parte compartilhada para uma terceira classe.
A configuração XML ainda é suportada no Spring Framework 6?
Sim. Definições de beans em XML, os namespaces p e c e context:component-scan continuam funcionando e estão totalmente documentados. Código novo deve usar configuração Java, porque ela é verificada pelo compilador, segura para refatoração e é o que o Spring Boot e a documentação de referência pressupõem; mesmo assim, durante uma migração, um arquivo XML pode ser importado em uma configuração Java com @ImportResource.