Inyección de dependencias en Spring Framework
Spring Framework es el framework por excelencia de cara a desarrollar aplicaciones en Java. Una de sus principales características es la inyección de dependencias (Dependency Injection o DI) de unos componentes con otros. En este post voy a hablar de como ha ido evolucionando Spring en como maneja la inyección de dependecias[1] y como debería evolucionar nuestro código para que sea más legible, más flexible, más testable, más robusto y, en definitiva, mejor código.
Inyección mediante @Autowired
Seguramente la anotación más utilizada y conocida en Spring. Esta anotación nos permite inyectar en un componente cualquier otro bean definido en el contexto de Spring. Por ejemplo, si tenemos un Repository y un Service, y queremos inyectar el primero en el segundo, bastaría con un @Autowired y el tipo que queremos inyectar:
@Repository
public MiRepository {
....
}
@Service
public MiService {
@Autowire
private MiRepository repository;
}
La inyección de componentes con @Autowired es siempre por tipo. Si deseamos que, además del tipo, el bean que se inyecte sea un bean con un id concreto, deberemos incluir la anotacion @Qualifier:
@Service
public MiService {
@Autowire
@Qualifier("miRepo")
private MiRepository repository;
}
Esta manera de inyectar beans en componentes es ampliamente utilizada, pero tiene sus desventajas:
-
El código está 100% ligado a Spring Framework. Spring es capaz de settear los fields con @Autowired mediante el api de reflection, pero nosotros desde fuera no podemos hacerlo. Esto puede llegar a ser una limitación en ciertas situaciones.
-
El código queda rígido si tenemos que incluir en él los @Qualifier. Nos resta flexibilidad.
-
Necesitamos librerías externas para hacer test unitarios con JUnit. Un ejemplo sería Mockito y el @InjectMocks para que Mockito nos haga una inyección de cada @Autowired con un @Mock.
Inyección por setter
Una pequeña mejora respecto al punto anterior sería la inyección por método. Este tipo de inyección nos permite tener el siguiente código:
@Service
public MiService {
private MiRepository repository;
@Autowired
public void setRepository(MiRepository repository) {
this.repository = repository;
}
}
Con esta variante, conseguimos que Spring inyecte del mismo modo el bean en nuestro componente y, además, tenemos acceso al setter y podemos establecer desde cualquier parte del código el MiRepository que queramos. Esto es muy útil para nuestros tests unitarios. Por otro lado, Spring no tiene que obtener por reflection el field y hacerlo accesible para poder establecer el valor, lo cual es más eficiente.
Sin embargo, no todo va a ser bonito… La principal desventaja que le encontramos a este tipo de inyección es que el bean no es inmutable, una característica deseable en todos nuestros beans. Con esto quiero recalcar que, una vez creado el bean, cualquier parte de nuestro código podría inyectar el componente, hacerle un setRepository y alterar el comportamiento que tenía nuestro componente al inicializar el contexto de Spring.
Inyección por constructor
Una mejora respecto al punto anterior es la inyección de dependencias por constructor. En este caso, en vez de anotar con @Autowired, debemos crear un constructor con todos los componentes que queremos inyectar. En nuestro ejemplo sería:
@Service
public MiService {
private final MiRepository repository;
public MiService(MiRepository repository) {
this.repository = repository;
}
}
Al indicar que el field es final, indicamos en Java que ese field no se puede modificar y debe ser inicializado al instanciar la clase (esto es por el significado que le da Java al tag final). Cuando Spring Framework descubre un componente con constructor, hace una búsqueda en el contexto para encontrar beans del tipo de cada argumento especificado en el constructor y hacer la inyección correctamente.
Las ventajas de esta aproximación son:
-
La inyección por constructor es código 100% Java, no tiene ninguna anotación de Spring en lo que se refiere a inyección de dependencias.
-
Nuestro bean es inmutable.
-
No contiene métodos setter innecesarios.
-
Nuestros test unitarios son fácilmente testables porque nuestro componente de Spring es 100% Java.
De hecho, creo que la única "desventaja" (nótese las comillas) sería que el constructor, cuando el componente tiene muchos beans como dependencias, se hace demasiado extenso. En líneas de código, en el caso de tener por ejemplo 3 inyecciones pasaríamos de 13 líneas de código:
@Service
public MiService {
@Autowire
private XRepository xRepository;
@Autowire
private YRepository yRepository;
@Autowire
private ZRepository zRepository;
}
a 20 líneas (si queremos ser curiosos a la hora de formatear nuestro código):
@Service
public MiService {
private final XRepository xRepository;
private final YRepository yRepository;
private final ZRepository zRepository;
public MiService(
XRepository xRepository,
YRepository yRepository,
ZRepository zRepository) {
this.xRepository = xRepository;
this.yRepository = yRepository;
this.zRepository = zRepository;
}
}
Además, si tenemos un analizador de cobertura de nuestros test unitarios (como JaCoCo), el código de nuestro constructor debe tener los test correspondientes para asegurar que está bien inicializado.
Inyección por constructor + Lombok
Y aquí es cuando entra en juego una de las librerías que más código boilerplate[2] ayuda a reducir en Java. Lombok es una librería que nos permite, mediante anotaciones, indicar métodos o constructores que queremos tener en nuestras clases, pero que no queremos añadir en nuestro código fuente. Automáticamente se encargará de generar ese código en tiempo de compilación por nosotros, dejando nuestras clases Java lo más limpias posible.
En este último apartado, indico la aproximación que más me gusta y que suelo utilizar en mis proyectos:
@Service
@RequiredArgsConstructor
public MiService {
private final XRepository xRepository;
private final YRepository yRepository;
private final ZRepository zRepository;
}
Con la anotación @RequiredArgsConstructor Lombok se ocupa de crear el constructor que hemos definido en el apartado anterior, pero reduciendo al máximo el número de líneas de nuestro componente. Además, si revisamos el javadoc de dicha anotación, podemos forzar a que verifique que los argumentos no sean nulos:
@Service
@RequiredArgsConstructor
public MiService {
private final @NonNull XRepository xRepository;
private final @NonNull YRepository yRepository;
private final @NonNull ZRepository zRepository;
}
De este modo, nuestro componente ha quedado lo más robusto posible, inmutable, con el menor número de líneas y 100% funcional.
Inyección por constructor + Lombok + @Qualifier [TRUCO EXTRA]
Para acabar, os vamos a explicar un caso más especial y que, a priori, podríamos pensar que con Lombok no se puede resolver. Si necesitas inyectar un bean con un id específico, tal como comentamos en el ejemplo inicial, junto a la anotación de @Autowired tenemos también la anotación @Qualifier para indicar que el bean que queremos inyectar tiene que ser el que hemos indicado. En el momento que delegamos en Lombok para crear nuestro constructor con la anotación @RequiredArgsConstructor, perdemos la posibilidad de indicar ese @Qualifier manualmente en el parámetro correspondiente.
Sin embargo, Lombok cuenta con la propiedad lombok.copyableAnnotations que permite propagar ciertas anotaciones desde el field de nuestra clase al constructor autogenerado. De este modo, añadiendo la siguiente configuración de Lombok en su lombok.config[3]:
lombok.copyableAnnotations=org.springframework.beans.factory.annotation.Qualifier
Y fijando la anotación en nuestra clase:
@Service
@RequiredArgsConstructor
public MiService {
@Qualifier("xRepository")
private final @NonNull Repository xRepository;
private final @NonNull YRepository yRepository;
}
Obtenemos la aproximación deseada. ¿Fácil verdad? Como único requisito, deberemos utilizar una versión de Lombok igual o superior a la 1.18.4.
Espero haberte ayudado en dejar tu código más limpio a partir de ahora. Una de las principales razones para migrarnos de los @Autowired por field son los test unitarios. Si todavía no haces testing con JUnit, es el momento de empezar, y si tienes cualquier tipo de pregunta, no dudes en escribirla en los comentarios.