Introducción a la inversión de control e inyección de dependencia con Spring

1. Información general

En este artículo, presentaremos los conceptos de IoC (inversión de control) y DI (inyección de dependencia), y luego veremos cómo se implementan en el marco de Spring.

2. ¿Qué es la inversión de control?

La inversión de control es un principio en la ingeniería de software mediante el cual el control de objetos o partes de un programa se transfiere a un contenedor o marco. Se utiliza con mayor frecuencia en el contexto de la programación orientada a objetos.

A diferencia de la programación tradicional, en la que nuestro código personalizado realiza llamadas a una biblioteca, IoC habilita un marco para tomar el control del flujo de un programa y realizar llamadas a nuestro código personalizado. Para habilitar esto, los marcos usan abstracciones con comportamiento adicional incorporado. Si queremos agregar nuestro propio comportamiento, necesitamos extender las clases del marco o agregar nuestras propias clases.

Las ventajas de esta arquitectura son:

  • desacoplar la ejecución de una tarea de su implementación
  • facilitando el cambio entre diferentes implementaciones
  • mayor modularidad de un programa
  • Mayor facilidad para probar un programa al aislar un componente o burlarse de sus dependencias y permitir que los componentes se comuniquen a través de contratos.

La inversión del control se puede lograr a través de varios mecanismos, tales como: patrón de diseño de estrategia, patrón de localizador de servicios, patrón de fábrica e inyección de dependencia (DI).

A continuación, veremos DI.

3. ¿Qué es la inyección de dependencia?

La inyección de dependencia es un patrón a través del cual implementar IoC, donde el control que se invierte es la configuración de las dependencias del objeto.

El acto de conectar objetos con otros objetos, o "inyectar" objetos en otros objetos, lo realiza un ensamblador en lugar de los objetos mismos.

Así es como crearía una dependencia de objeto en la programación tradicional:

public class Store { private Item item; public Store() { item = new ItemImpl1(); } }

En el ejemplo anterior, necesitamos instanciar una implementación de la interfaz Item dentro de la propia clase Store .

Al usar DI, podemos reescribir el ejemplo sin especificar la implementación de Item que queremos:

public class Store { private Item item; public Store(Item item) { this.item = item; } }

En las siguientes secciones, veremos cómo podemos proporcionar la implementación de Item a través de metadatos.

Tanto IoC como DI son conceptos simples, pero tienen profundas implicaciones en la forma en que estructuramos nuestros sistemas, por lo que vale la pena comprenderlos bien.

4. El contenedor Spring IoC

Un contenedor de IoC es una característica común de los marcos que implementan IoC.

En el marco de Spring, el contenedor de IoC está representado por la interfaz ApplicationContext . El contenedor Spring es responsable de crear instancias, configurar y ensamblar objetos conocidos como beans , así como de administrar su ciclo de vida.

El marco de Spring proporciona varias implementaciones de la interfaz ApplicationContext : ClassPathXmlApplicationContext y FileSystemXmlApplicationContext para aplicaciones independientes, y WebApplicationContext para aplicaciones web.

Para ensamblar beans, el contenedor usa metadatos de configuración, que pueden estar en forma de configuración XML o anotaciones.

Aquí hay una forma de instanciar manualmente un contenedor:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

Para establecer el atributo del elemento en el ejemplo anterior, podemos usar metadatos. Luego, el contenedor leerá estos metadatos y los usará para ensamblar beans en tiempo de ejecución.

La inyección de dependencia en Spring se puede realizar a través de constructores, establecedores o campos.

5. Inyección de dependencia basada en constructores

En el caso de la inyección de dependencias basada en constructores, el contenedor invocará un constructor con argumentos, cada uno de los cuales representa una dependencia que queremos establecer.

Spring resuelve cada argumento principalmente por tipo, seguido por el nombre del atributo y el índice para la desambiguación. Veamos la configuración de un bean y sus dependencias usando anotaciones:

@Configuration public class AppConfig { @Bean public Item item1() { return new ItemImpl1(); } @Bean public Store store() { return new Store(item1()); } }

La anotación @Configuration indica que la clase es una fuente de definiciones de beans . Además, podemos agregarlo a múltiples clases de configuración.

La anotación @Bean se utiliza en un método para definir un bean. Si no especificamos un nombre personalizado, el nombre del bean será por defecto el nombre del método.

Para un bean con el alcance singleton predeterminado , Spring primero verifica si ya existe una instancia en caché del bean y solo crea una nueva si no existe. Si usamos el alcance del prototipo , el contenedor devuelve una nueva instancia de bean para cada llamada al método.

Otra forma de crear la configuración de los beans es mediante la configuración XML:

6. Inyección de dependencia basada en el setter

Para la DI basada en establecedores, el contenedor llamará a los métodos de establecimiento de nuestra clase, después de invocar un constructor sin argumentos o un método de fábrica estático sin argumentos para instanciar el bean. Creemos esta configuración usando anotaciones:

@Bean public Store store() { Store store = new Store(); store.setItem(item1()); return store; }

También podemos usar XML para la misma configuración de beans:

Constructor-based and setter-based types of injection can be combined for the same bean. The Spring documentation recommends using constructor-based injection for mandatory dependencies, and setter-based injection for optional ones.

7. Field-Based Dependency Injection

In case of Field-Based DI, we can inject the dependencies by marking them with an @Autowired annotation:

public class Store { @Autowired private Item item; }

While constructing the Store object, if there's no constructor or setter method to inject the Item bean, the container will use reflection to inject Item into Store.

We can also achieve this using XML configuration.

This approach might look simpler and cleaner but is not recommended to use because it has a few drawbacks such as:

  • This method uses reflection to inject the dependencies, which is costlier than constructor-based or setter-based injection
  • It's really easy to keep adding multiple dependencies using this approach. If you were using constructor injection having multiple arguments would have made us think that the class does more than one thing which can violate the Single Responsibility Principle.

More information on @Autowired annotation can be found in Wiring In Spring article.

8. Autowiring Dependencies

Wiring allows the Spring container to automatically resolve dependencies between collaborating beans by inspecting the beans that have been defined.

There are four modes of autowiring a bean using an XML configuration:

  • no: the default value – this means no autowiring is used for the bean and we have to explicitly name the dependencies
  • byName: autowiring is done based on the name of the property, therefore Spring will look for a bean with the same name as the property that needs to be set
  • byType: similar to the byName autowiring, only based on the type of the property. This means Spring will look for a bean with the same type of the property to set. If there's more than one bean of that type, the framework throws an exception.
  • constructor: autowiring is done based on constructor arguments, meaning Spring will look for beans with the same type as the constructor arguments

For example, let's autowire the item1 bean defined above by type into the store bean:

@Bean(autowire = Autowire.BY_TYPE) public class Store { private Item item; public setItem(Item item){ this.item = item; } }

We can also inject beans using the @Autowired annotation for autowiring by type:

public class Store { @Autowired private Item item; }

If there's more than one bean of the same type, we can use the @Qualifier annotation to reference a bean by name:

public class Store { @Autowired @Qualifier("item1") private Item item; }

Now, let's autowire beans by type through XML configuration:

Next, let's inject a bean named item into the item property of store bean by name through XML:

We can also override the autowiring by defining dependencies explicitly through constructor arguments or setters.

9. Lazy Initialized Beans

By default, the container creates and configures all singleton beans during initialization. To avoid this, you can use the lazy-init attribute with value true on the bean configuration:

As a consequence, the item1 bean will be initialized only when it's first requested, and not at startup. The advantage of this is faster initialization time, but the trade-off is that configuration errors may be discovered only after the bean is requested, which could be several hours or even days after the application has already been running.

10. Conclusion

In this article, we've presented the concepts of inversion of control and dependency injection and exemplified them in the Spring framework.

You can read more about these concepts in Martin Fowler's articles:

  • Inversión de contenedores de control y patrón de inyección de dependencia.
  • Inversión de control

Y puede obtener más información sobre las implementaciones de Spring de IoC y DI en la Documentación de referencia de Spring Framework.