Spring Framework
Inversión de control e inyección de dependencias en Spring
Principiante9 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
El trabajo esencial de Spring es construir tu grafo de objetos por ti. Al terminar esta guía entenderás qué es el contenedor IoC, cómo ApplicationContext descubre y conecta los beans, por qué la inyección por constructor es el estilo recomendado, cómo resolver ambigüedades con qualifiers y a qué equivale, en la configuración Java moderna, la configuración XML de los proyectos más antiguos.
Lo que aprenderás
- Explicar qué significa la inversión de control y por qué un contenedor compensa esa indirección
- Declarar beans con @Configuration, @Bean y el escaneo de componentes
- Elegir entre inyección por constructor, por setter y por campo, y justificar la elección
- Resolver dependencias ambiguas con @Qualifier, @Primary y la inyección de colecciones
- Reconocer configuración de la era XML y traducirla a configuración Java
Toda aplicación no trivial es un grafo de objetos: un controlador web necesita un servicio, el servicio necesita un repositorio, el repositorio necesita una fuente de datos y la fuente de datos necesita credenciales. Alguien tiene que construir ese grafo, en el orden correcto y con las implementaciones adecuadas para el entorno actual. La respuesta de Spring Framework es un contenedor que se encarga de crear los objetos y entrega a cada uno los colaboradores que declara. Esa única idea —la inversión de control (IoC), materializada mediante la inyección de dependencias (DI)— es la base sobre la que se construye el resto del framework, y todo Spring Boot. Esta guía se centra en Spring Framework 6.x sobre Java 17 o posterior.
El problema que resuelve el contenedor
Piensa en un servicio que envía confirmaciones de pedido. Sin contenedor, crea sus propios 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");
}
}
La clase funciona, pero ha tomado en silencio cuatro decisiones que no le corresponden: qué implementación de repositorio usar, de dónde sale la fuente de datos, qué transporte de correo emplear y cómo está configurado. No puedes hacer un test unitario de confirm sin una base de datos y un servidor SMTP. No puedes cambiar el mailer por uno basado en una cola sin editar este archivo. Cada clase que sigue este patrón se convierte en una pequeña factoría de sus propias dependencias, con todo fijado a fuego en el código.
La inversión de control le da la vuelta a la flecha. OrderService deja de buscar o construir lo que necesita y, en su lugar, lo declara:
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 */ }
}
Ahora la clase depende de dos roles expresados como interfaces, y algo externo decide qué objetos concretos cubren esos roles. En un test, ese algo eres tú, pasando fakes. En producción, es el contenedor de Spring. La clase en sí no tiene ningún import de Spring; es algo intencionado, y buena señal.
Qué es el contenedor
El contenedor es un objeto que lee definiciones de beans —recetas que describen qué clases instanciar, cómo configurarlas y de qué dependen— y las usa para construir un grafo de objetos gestionados, llamados beans. La interfaz central es ApplicationContext. Cuando creas uno, este:
- Carga las definiciones de beans desde tus clases de configuración (o desde XML, o desde paquetes escaneados).
- Ejecuta los bean factory post-processors, que pueden modificar las definiciones (por ejemplo, para resolver los placeholders de propiedades).
- Instancia cada bean singleton, resolviendo recursivamente sus dependencias en el orden que estas imponen.
- Aplica los bean post-processors a cada nueva instancia (aquí es donde se enganchan
@PostConstruct, los proxies de AOP y los interceptores de transacciones). - Publica un evento de refresco y queda listo para atender llamadas a
getBean, aunque el código bien diseñado rara vez llama agetBeandirectamente.
Las dos implementaciones que verás con más frecuencia son AnnotationConfigApplicationContext, para uso independiente o en tests, y AnnotationConfigWebApplicationContext (o AnnotationConfigServletWebServerApplicationContext, en Boot) para aplicaciones web.
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
OrderService service = context.getBean(OrderService.class);
service.confirm(42L);
} // close() destroys singletons and runs @PreDestroy callbacks
Declarar beans con configuración Java
Una clase de configuración es una clase anotada con @Configuration cuyos métodos @Bean producen cada uno un bean. El nombre del método pasa a ser el nombre del bean, salvo que lo cambies.
@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);
}
}
Los parámetros de los métodos se resuelven por tipo desde el contenedor, así que un método @Bean nunca tiene que llamar a otro método @Bean para obtener una dependencia. Puede hacerlo —orderRepository(dataSource())— y Spring seguirá devolviendo la única instancia gestionada en lugar de un objeto nuevo, porque las clases @Configuration se extienden con una subclase CGLIB generada en tiempo de ejecución para interceptar esas llamadas. Esa intercepción es cómoda pero invisible, y por eso pasar las dependencias como parámetros es el estilo más claro.
Los métodos @Bean brillan con clases que no son tuyas: un pool de conexiones, un cliente de terceros, un Clock. Para tus propias clases, el escaneo de componentes requiere menos ceremonia.
Escaneo de componentes
Anota tus clases con una anotación de estereotipo e indica al contenedor qué paquetes 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 es el estereotipo genérico; @Service, @Repository, @Controller y @RestController son especializaciones. En la práctica son casi idénticos —un @Service escaneado no es más que un @Component—, pero @Repository activa además la traducción de excepciones de acceso a datos, y los demás documentan la intención y permiten filtrar los escaneos. Escanea el paquete más acotado que contenga tu código: escanear com o una cadena vacía arrastra todas las librerías del classpath que usen estereotipos de Spring y ralentiza el arranque.
Los tres estilos de inyección
Spring puede inyectar a través de un constructor, de un setter o directamente en un campo. No son intercambiables.
| Estilo | Cómo | Ventajas | Inconvenientes |
|---|---|---|---|
| Constructor | Las dependencias son parámetros del constructor | Los campos pueden ser final; el objeto nunca queda a medio construir; funciona sin Spring en los tests; demasiados parámetros es un problema de diseño visible | Algo más de código (nada con records o Lombok) |
| Setter | @Autowired en un setter | Dependencias opcionales; reconfigurable tras la construcción | El objeto puede existir en un estado inválido; mutabilidad |
| Campo | @Autowired en un campo privado | El más corto | No puede ser final; invisible para quien llama; los tests necesitan reflexión o el contenedor; oculta listas de dependencias cada vez más largas |
El equipo de Spring recomienda la inyección por constructor para las dependencias obligatorias y la inyección por setter para las opcionales. La inyección por campo aparece en infinidad de tutoriales porque es corta, pero acopla la clase a un contenedor basado en reflexión e impide hacer un simple new en un test unitario. Con un único constructor, @Autowired sobra: el contenedor lo usa implícitamente.
Las dependencias opcionales admiten varios enfoques. Elige uno y sé coherente:
@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 también resuelve un problema clásico: cuando un singleton necesita un bean nuevo con scope prototype en cada llamada, provider.getObject() se lo pide al contenedor cada vez en lugar de retener una única instancia obsoleta.
Resolver ambigüedades
La inyección es por tipo. Cuando dos beans implementan Mailer, el contenedor no puede elegir y falla al arrancar con NoUniqueBeanDefinitionException. Tienes cuatro herramientas:
@Primaryen uno de los candidatos lo convierte en la opción por defecto cuando no se pide nada más específico.@Qualifier("smtp")en el punto de inyección indica el bean que quieres (por nombre de bean, o mediante una anotación de qualifier propia que definas tú para tener seguridad de tipos).- Fallback por el nombre del parámetro: si un parámetro se llama
smtpMailery existe un bean con ese nombre, gana ese bean; es frágil, porque un renombrado rompe la conexión sin avisar. - Inyección de colecciones: declara
List<Mailer>oMap<String, Mailer>para recibir todos los candidatos, ordenados según@OrderuOrdered.
@Bean @Primary Mailer smtpMailer() { … }
@Bean Mailer queueMailer() { … }
public NotificationService(@Qualifier("queueMailer") Mailer mailer) { … }
Spring Framework 6.2 añadió @Fallback, la imagen especular de @Primary: un bean que solo se elige cuando no existe ningún otro candidato, útil para valores por defecto de una librería que las aplicaciones pueden sobrescribir.
Los tipos genéricos también cuentan en la resolución: Repository<Order> y Repository<Customer> son destinos de inyección distintos, así que a menudo no necesitas ningún qualifier si tus interfaces llevan parámetros de tipo.
De XML a configuración Java
Los proyectos más antiguos, y la mayor parte del material de formación de Spring 3/4, definen los beans en 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>
La correspondencia es mecánica: <bean> se convierte en un método @Bean, <constructor-arg ref> en un parámetro del método, <property> en una llamada a un setter, <context:component-scan> en @ComponentScan y <import resource> en un @Import de otra clase de configuración. El XML se sigue cargando con ClassPathXmlApplicationContext y puede combinarse mediante @ImportResource("classpath:legacy-beans.xml"), que es el camino sensato para una migración gradual. La era XML también se apoyaba en las anotaciones @Resource de la JSR-250 e @Inject de la JSR-330; ambas siguen funcionando, ahora desde los paquetes jakarta.annotation y jakarta.inject, pero @Autowired es la opción idiomática.
Errores comunes
Llamar a getBean desde el código de aplicación. Pasar el ApplicationContext de un lado a otro y buscar beans por nombre reintroduce el acoplamiento del service locator que la DI pretendía eliminar. Inyecta lo que necesites; si el conjunto de beans es dinámico, inyecta ObjectProvider<T> o Map<String, T>.
Usar new con una clase que tiene anotaciones de Spring. Un objeto que construyes tú no está gestionado: no hay inyección, ni @Transactional, ni @PostConstruct. Se manifiesta como un NullPointerException en un campo que «debería haberse inyectado».
Escanear demasiado. Un @ComponentScan sin argumentos escanea el paquete de la clase de configuración y todos sus subpaquetes. Coloca tu configuración raíz en la cima de la jerarquía de paquetes de tu código, nunca por encima.
Mezclar inyección por campo y constructores. Una clase que recibe unas dependencias por constructor y otras mediante campos @Autowired resulta confusa e imposible de testear. Pásalo todo al constructor.
Dependencias circulares en constructores. A(B) y B(A) fallan al arrancar. No cambies a inyección por campo para silenciar el error; separa el comportamiento compartido en un tercer bean o usa ObjectProvider<A> en el raro caso legítimo de enlace tardío.
Dos beans, ningún qualifier. Añadir una segunda implementación de una interfaz —por ejemplo, un CachingProductRepository que envuelve al de JDBC— rompe todos los puntos de inyección existentes de ese tipo. Marca con @Primary la opción por defecto que quieras en el momento de añadir el segundo bean.
Buenas prácticas
- Mantén las clases de dominio y de servicio libres de imports de Spring siempre que puedas. Un constructor y una interfaz bastan; la anotación de estereotipo es lo único específico de Spring que necesitan, e incluso eso puede trasladarse a un método
@Bean. - Prefiere un solo constructor por clase, con campos
final. Si un constructor supera los cinco o seis parámetros, la clase hace demasiadas cosas. - Usa métodos
@Beanpara los tipos de terceros y el escaneo de componentes para los tuyos. - Pon nombre a los beans de forma deliberada cuando vaya a haber más de uno de un mismo tipo y, en bases de código grandes, prefiere anotaciones de qualifier propias a los nombres como cadenas.
- Deja que el arranque falle pronto. Un bean que falta y se detecta al arrancar sale mucho más barato que uno que se descubre en la primera petición en producción, así que evita
@Autowired(required = false)salvo que la dependencia sea de verdad opcional.
Ideas clave
- La inversión de control significa que los objetos declaran sus dependencias y el contenedor se las proporciona; la inyección de dependencias es el mecanismo.
ApplicationContextlee las definiciones de beans de las clases@Configurationy de los componentes escaneados, construye el grafo de singletons al arrancar y aplica los post-processors a cada bean.- La inyección por constructor es el estilo recomendado: inmutable, testeable sin Spring y transparente respecto a las dependencias de una clase.
- La inyección es por tipo; usa
@Primary,@Qualifiero la inyección de colecciones cuando varios beans comparten tipo. - La configuración XML sigue funcionando y se traslada uno a uno a configuración Java, que es lo que debería usar todo el código nuevo.
- Nunca hagas
newde una clase gestionada por Spring ni llames agetBeanen el código de aplicación: ambas cosas esquivan el contenedor.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Qué diferencia hay entre BeanFactory y ApplicationContext?
- BeanFactory es la interfaz mínima del contenedor: sabe crear y conectar beans bajo demanda. ApplicationContext la amplía con todo lo que necesita una aplicación: creación anticipada de singletons, publicación de eventos, fuentes de mensajes para i18n, carga de recursos y registro automático de post-processors. El código de aplicación debería usar siempre un ApplicationContext; BeanFactory existe para restricciones de memoria extremas y para el funcionamiento interno del framework.
- ¿Sigo necesitando @Autowired en un constructor?
- No cuando la clase tiene exactamente un constructor. Desde Spring Framework 4.3, el contenedor usa implícitamente un constructor único. Solo necesitas @Autowired para elegir un constructor entre varios, o en los puntos de inyección por setter y por campo.
- ¿Por qué Spring dice que un bean tiene una dependencia circular?
- El bean A necesita a B en su constructor y B necesita a A en el suyo, así que ninguno puede crearse primero; el contenedor lanza BeanCurrentlyInCreationException. La inyección por setter o por campo puede romper técnicamente el ciclo, porque Spring inyecta en un singleton a medio construir, pero el ciclo casi siempre indica que las responsabilidades están enredadas. En lugar de eso, extrae la parte compartida a una tercera clase.
- ¿Sigue siendo compatible la configuración XML en Spring Framework 6?
- Sí. Las definiciones de beans en XML, los namespaces p y c y context:component-scan siguen funcionando y están plenamente documentados. El código nuevo debería usar configuración Java, porque el compilador la comprueba, resiste las refactorizaciones y es lo que dan por sentado Spring Boot y la documentación de referencia; aun así, durante una migración se puede importar un archivo XML en una configuración Java con @ImportResource.