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:
- Carrega as definições de beans das suas classes de configuração (ou de XML, ou de pacotes escaneados).
- Executa os bean factory post-processors, que podem alterar as definições (a resolução de placeholders de propriedades, por exemplo).
- Instancia cada bean singleton, resolvendo recursivamente as dependências de cada um, na ordem que elas impõem.
- Aplica os bean post-processors a cada nova instância (é aqui que entram
@PostConstruct, os proxies de AOP e os interceptadores de transação). - Publica um evento de refresh e fica pronto para atender chamadas a
getBean— embora código bem projetado raramente chamegetBeandiretamente.
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.
| Estilo | Como | Prós | Contras |
|---|---|---|---|
| Construtor | As dependências são parâmetros do construtor | Os 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ível | Um pouco mais de digitação (nenhuma com records/Lombok) |
| Setter | @Autowired em um setter | Dependências opcionais; reconfigurável depois da construção | O objeto pode existir em estado inválido; mutabilidade |
| Campo | @Autowired em um campo privado | O mais curto | Nã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:
@Primaryem 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
smtpMailere 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>ouMap<String, Mailer>para receber todos os candidatos, ordenados por@OrderouOrdered.
@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
@Beanpara 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
ApplicationContextlê definições de beans das classes@Configuratione 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,@Qualifierou 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ê
newem uma classe gerenciada pelo Spring nem chamegetBeanno 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.