Vinculación de Beans en Spring: Estrategias y Configuración

La creación de relaciones de colaboración entre objetos de una aplicación se conoce como vinculación (wiring), y es la esencia de la inyección de dependencias (DI). Spring ofrece una gran flexibilidad en esta tarea, proporcionando tres mecanismos principales de vinculación:

  • Configuración explícita en XML
  • Configuración explícita en Java (JavaConfig)
  • Descubrimiento implícito de beans y autoinyección

Los dos primeros constituyen configuraciones explícitas, mientras que el último es la llamada autoinyección. No hay una solución absoluta; la elección depende de las preferencias o necesidades del proyecto. Los estilos de configuración de Spring pueden combinarse, por lo que es posible utilizar XML para algunos beans, JavaConfig para otros y dejar que Spring descubra el resto de forma automática.

Se recomienda utilizar el mecanismo de autoinyección tanto como sea posible. Cuanta menos configuración explícita, mejor. Cuando sea inevitable (por ejemplo, para clases de terceros o JARs que no pueden modificarse), se prefiere JavaConfig por su seguridad tipográfica y potencia frente a XML. Solo se debería recurrir a XML cuando se necesite la comodidad de los espacios de nombres XML y no haya una implementación equivalente en JavaConfig.

Autoinyección de beans

Spring implementa la autoinyección desde dos perspectivas:

  • Escaneo de componentes (component scanning): Spring descubre automáticamente los beans que debe crear en el contexto.
  • Autoinyección (autowiring): Spring satisface automáticamente las dependencias entre beans.

Combinando el escaneo de componentes y la autoinyección se minimiza la configuración explícita.

Creación de un bean

Se usa la anotación @Component sobre una clase para indicar que es un componente y que Spring debe crear un bean para ella.

Asignación de nombre al bean

En el contexto de aplicación de Spring, todos los beans tienen un ID. Si no se especifica, Spring genera uno basado en el nombre de la clase, con la primera letra en minúscula. Para dar un nombre personalizado, se pasa el ID como valor a @Component:

@Component("reproductor")
public class ReproductorCD implements MediaPlayer {
    // ...
}

Alternativamente, se puede usar la anotación @Named del estándar JSR-330:

import javax.inject.Named;

@Named("reproductor")
public class ReproductorCD implements MediaPlayer {
    // ...
}

Spring soporta @Named como sustituto de @Component; en la mayoría de los casos son intercambiables.

Activación del escaneo de componentes

El escaneo de componentes no está habilitado por defecto. Es necesario configurarlo explícitamente, ya sea mediante JavaConfig o XML.

Con JavaConfig se utiliza la anotación @Configuration en la clase de configuración y @ComponentScan para activar el escaneo:

@Configuration
@ComponentScan("com.ejemplo")
public class ConfigApp {
}

Especificación del paquete base

Por defecto, @ComponentScan escanea el paquete donde se encuentra la clase de configuración. Para cambiar el paquete base se usa el atributo basePackages:

@Configuration
@ComponentScan(basePackages = {"com.ejemplo", "com.ejemplo2"})
public class ConfigApp {
}

Para evitar el uso de cadenas (no seguras en refactorizaciones), se puede usar basePackageClasses con referencias a clases o interfaces del paquete base:

@Configuration
@ComponentScan(basePackageClasses = {ReproductorCD.class, ReproductorDVD.class})
public class ConfigApp {
}

Se recomienda crear una interfaz marcadora vacía en el paquete base para mantener la compatibilidad con la refactorización sin referenciar código de aplicación real.

En XML se utiliza el elemento <context:component-scan>:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.ejemplo"/>

</beans>

Autoinyección con @Autowired

La anotación @Autowired de Spring se puede aplicar en constructores, métodos setter o cualquier método para satisfacer dependencias:

@Component
public class ReproductorCD implements MediaPlayer {
    private DiscoCompacto disco;

    @Autowired
    public ReproductorCD(DiscoCompacto disco) {
        this.disco = disco;
    }
}

También se puede usar en un método setter:

@Autowired
public void setDisco(DiscoCompacto disco) {
    this.disco = disco;
}

Si no existe un bean candidato, se lanza una excepción. Para evitarlo, se puede marcar la dependencia como opcional:

@Autowired(required = false)
public ReproductorCD(DiscoCompacto disco) {
    this.disco = disco;
}

En caso de que existan varios beans candidatos, Spring también lanza una excepción por ambigüedad.

Como alternativa, se puede usar @Inject del estándar JSR-330:

import javax.inject.Inject;
import javax.inject.Named;

@Named("reproductor")
public class ReproductorCD implements MediaPlayer {
    @Inject
    public ReproductorCD(DiscoCompacto disco) {
        this.disco = disco;
    }
}

Prueba de la autoinyección

Ejemplo de test con Spring y JUnit:

package com.ejemplo;

import static org.junit.Assert.*;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.SystemOutRule;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ConfigApp.class)
public class TestReproductor {

    @Rule
    public final SystemOutRule log = new SystemOutRule().enableLog();

    @Autowired
    private DiscoCompacto disco;

    @Autowired
    private MediaPlayer reproductor;

    @Test
    public void discoNoDebeSerNulo() {
        assertNotNull(disco);
    }

    @Test
    public void reproductorDebeContenerDisco() {
        assertTrue(disco == reproductor.obtenerDisco());
    }

    @Test
    public void testReproducir() {
        reproductor.reproducir();
        assertEquals("Reproduciendo Álbum Sargento Pimienta de Los Beatles\r\n", log.getLog());
    }
}

Se utiliza SpringJUnit4ClassRunner para crear automáticamente el contexto de Spring. @ContextConfiguration indica la clase de configuración. El SystemOutRule captura la salida estándar para verificar el resultado.

Configuración explícita con JavaConfig

Cuando la autoinyección no es posible (por ejemplo, con clases de bibliotecas de terceros), se recurre a la configuración explícita. JavaConfig proporciona seguridad tipográfica y flexibilidad.

Una clase de configuración se anota con @Configuration y contiene métodos anotados con @Bean que devuelven instancias de los beans.

Declaración de un bean simple

@Configuration
public class ConfigDiscos {
    @Bean
    public DiscoCompacto albumSargento() {
        return new AlbumSargentoPimienta();
    }
}

El ID del bean es el nombre del método. Se puede personalizar con el atributo name:

@Bean(name = "discoFavorito")
public DiscoCompacto albumSargento() {
    return new AlbumSargentoPimienta();
}

Gracias a Java, se puede añadir lógica en la creación del bean:

@Bean
public DiscoCompacto albumAleatorio() {
    int opcion = (int) (Math.random() * 4);
    switch (opcion) {
        case 0: return new AlbumSargentoPimienta();
        case 1: return new AlbumBlanco();
        case 2: return new DiaNoche();
        default: return new Revolver();
    }
}

Inyección de dependencias en JavaConfig

Se puede invocar directamente otro método @Bean para obtener una referencia:

@Bean
public MediaPlayer reproductorCD() {
    return new ReproductorCD(albumSargento());
}

Aunque parezca que cada llamada crea una nueva instancia, Spring intercepta la llamada y devuelve el mismo bean singleton ya creado.

Una forma más clara es inyectar el bean como parámetro del método:

@Bean
public MediaPlayer reproductorCD(DiscoCompacto disco) {
    return new ReproductorCD(disco);
}

De esta manera, el bean disco puede provenir de cualquier otro método @Bean de la misma clase de configuración o incluso de XML o escaneo.

También se puede usar un setter:

@Bean
public MediaPlayer reproductorCD(DiscoCompacto disco) {
    ReproductorCD reproductor = new ReproductorCD();
    reproductor.setDisco(disco);
    return reproductor;
}

Configuración con XML

XML fue la forma principal de configuración en versiones antiguas de Spring. Aunque hoy se prefiere la autoinyección o JavaConfig, es importante comprender la sintaxis.

El archivo XML debe tener como raíz el elemento <beans> con las declaraciones de espacios de nombres correspondientes.

Declaración de un bean simple

<bean id="disco" class="com.ejemplo.AlbumSargentoPimienta" />

Si no se proporciona un id, Spring asigna uno automático como com.ejemplo.AlbumSargentoPimienta#0.

Inyección por constructor

Usando el elemento <constructor-arg>:

<bean id="reproductor" class="com.ejemplo.ReproductorCD">
    <constructor-arg ref="disco" />
</bean>

O con el espacio de nombres c (definido en Spring 3.0):

xmlns:c="http://www.springframework.org/schema/c"
...
<bean id="reproductor" class="com.ejemplo.ReproductorCD" c:disco-ref="disco" />

Si hay varios parámetros, se puede usar el índice (con el prefijo _):

<bean id="reproductor" class="com.ejemplo.ReproductorCD" c:_0-ref="disco" />

Para un solo parámetro, se puede usar c:_-ref="...".

Inyección de literales

<bean id="disco" class="com.ejemplo.Album">
    <constructor-arg value="123" />
    <constructor-arg value="Artista Famoso" />
</bean>

Con el espacio de nombres c:

<bean id="disco" class="com.ejemplo.Album" c:codigo="123" c:artista="Artista Famoso" />

Inyección de colecciones

Supongamos que el constructor acepta una lista List<String>:

<bean id="disco" class="com.ejemplo.Album">
    <constructor-arg>
        <list>
            <value>pista1</value>
            <value>pista2</value>
        </list>
    </constructor-arg>
</bean>

También se puede usar <set> para conjuntos.

Inyección por setter

Usando <property>:

<bean id="reproductor" class="com.ejemplo.ReproductorCD">
    <property name="disco" ref="disco" />
</bean>

Con el espacio de nombres p:

xmlns:p="http://www.springframework.org/schema/p"
...
<bean id="reproductor" class="com.ejemplo.ReproductorCD" p:disco-ref="disco" />

Para colecciones, primero se define un bean de lista con <util:list> (necesita el namespace util):

xmlns:util="http://www.springframework.org/schema/util"
...
<util:list id="listaPistas">
    <value>pista1</value>
    <value>pista2</value>
</util:list>

Luego se inyecta como referencia:

<bean id="disco" class="com.ejemplo.Album" p:pistas-ref="listaPistas" />

Configuración mixta

Los tres estilos (autoinyección, JavaConfig, XML) no son mutuamente excluyentes. Se pueden combinar para aprovechar lo mejor de cada uno.

Incluir XML desde JavaConfig

Se usa la anotación @ImportResource:

@Configuration
@Import({ConfigDiscos.class})
@ImportResource("classpath:config-dvd.xml")
public class ConfigSistemaSonido {
}

Incluir JavaConfig desde XML

Se declara un bean de la clase de configuración:

<bean class="com.ejemplo.ConfigDiscos" />

También se puede usar un archivo XML principal que agrupe otros:

<beans ...>
    <import resource="config-discos.xml" />
    <bean class="com.ejemplo.ConfigReproductor" />
    <context:component-scan base-package="com.ejemplo" />
</beans>

Se recomienda crear un archivo de configuración de alto nivel que combine todos los módulos, ya sea con @Configuration o con un XML raíz, manteniendo el código limpio y modular.

Etiquetas: Spring inyeccion-de-dependencias java-config configuración-xml autowired

Publicado el 6-26 01:56