Method Injection

Inyección de métodos

En la mayoría de los escenarios de aplicación, la mayoría de los beans del contenedor son singletons. Cuando un bean singleton necesita colaborar con otro bean singleton, o cuando un bean no singleton necesita colaborar con otro bean no singleton, normalmente se gestiona la dependencia definiendo un bean como propiedad del otro.

El problema aparece cuando los ciclos de vida de los beans son diferentes. Supongamos que el bean singleton A necesita usar el bean no singleton (prototype) B, quizá en cada invocación de un método de A.

El contenedor crea el bean singleton A una sola vez y, por tanto, solo tiene una oportunidad para establecer sus propiedades. No puede proporcionar a A una nueva instancia de B cada vez que la necesite.

Una solución consiste en renunciar a parte de la inversión de control. Puedes hacer que el bean A conozca el contenedor implementando la interfaz ApplicationContextAware y llamar a getBean("B") para solicitar al contenedor una instancia —normalmente nueva— de B cada vez que A la necesite. El siguiente ejemplo muestra este enfoque:

package fiona.apple;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * Clase que utiliza una clase de estilo Command con estado
 * para realizar algún procesamiento.
 */
public class CommandManager implements ApplicationContextAware {

	private ApplicationContext applicationContext;

	public Object process(Map commandState) {
		// Obtiene una nueva instancia del Command adecuado.
		Command command = createCommand();

		// Establece el estado en la instancia, que idealmente es nueva.
		command.setState(commandState);
		return command.execute();
	}

	protected Command createCommand() {
		// Observa la dependencia de la API de Spring.
		return this.applicationContext.getBean("command", Command.class);
	}

	public void setApplicationContext(
			ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}
}

Este enfoque no es deseable, porque el código de negocio conoce y queda acoplado a Spring Framework. La inyección de métodos, una funcionalidad algo avanzada del contenedor IoC de Spring, permite resolver este caso de uso de forma más limpia.

Inyección de métodos de búsqueda

La inyección de métodos de búsqueda permite al contenedor sobrescribir métodos de beans gestionados por el contenedor y devolver el resultado de buscar otro bean con nombre dentro del contenedor. Normalmente, la búsqueda implica un bean prototype, como en el escenario anterior.

Spring Framework implementa esta inyección mediante generación de bytecode con la biblioteca CGLIB, creando dinámicamente una subclase que sobrescribe el método.

  • Para que esta creación dinámica de subclases funcione, ni la clase que el contenedor de Spring va a extender ni el método que se va a sobrescribir pueden ser final.
  • Para probar unitariamente una clase que tiene un método abstract, debes crear tú mismo una subclase y proporcionar una implementación simulada del método abstract.
  • Los métodos de búsqueda no funcionan con métodos de fábrica, en particular con métodos @Bean de clases de configuración. En ese caso, el contenedor no controla la creación de la instancia y no puede generar una subclase en tiempo de ejecución.

En el caso de la clase CommandManager del ejemplo anterior, el contenedor de Spring sobrescribe dinámicamente la implementación del método createCommand(). La clase CommandManager deja de tener dependencias de Spring, como muestra el siguiente ejemplo:

package fiona.apple;

// ¡Ya no hay importaciones de Spring!

public abstract class CommandManager {

	public Object process(Object commandState) {
		// Obtiene una nueva instancia de la interfaz Command adecuada.
		Command command = createCommand();

		// Establece el estado en la instancia, que idealmente es nueva.
		command.setState(commandState);
		return command.execute();
	}

	// De acuerdo..., ¿pero dónde está la implementación de este método?
	protected abstract Command createCommand();
}

En la clase cliente que contiene el método que se inyectará —CommandManager en este caso—, el método debe tener una firma con la siguiente forma:

<public|protected> [abstract] <tipo-de-retorno> nombreDelMetodo(sin-argumentos);

Si el método es abstract, la subclase generada dinámicamente lo implementa. En caso contrario, la subclase generada dinámicamente sobrescribe el método concreto definido en la clase original. Considera el siguiente ejemplo:

<!-- Bean con estado desplegado como prototype (no singleton). -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
	<!-- Inyecta aquí las dependencias necesarias. -->
</bean>

<!-- commandManager usa el bean prototype myCommand. -->
<bean id="commandManager" class="fiona.apple.CommandManager">
	<lookup-method name="createCommand" bean="myCommand"/>
</bean>

El bean identificado como commandManager llama a su propio método createCommand() cada vez que necesita una nueva instancia del bean myCommand. Debes asegurarte de desplegar myCommand con ámbito prototype si eso es lo que realmente necesitas. Si es un singleton, se devolverá la misma instancia de myCommand cada vez.

Como alternativa, dentro del modelo de componentes basado en anotaciones, puedes declarar un método de búsqueda mediante la anotación @Lookup:

public abstract class CommandManager {

	public Object process(Object commandState) {
		Command command = createCommand();
		command.setState(commandState);
		return command.execute();
	}

	@Lookup("myCommand")
	protected abstract Command createCommand();
}

De forma más idiomática, puedes permitir que el bean de destino se resuelva a partir del tipo de retorno declarado para el método de búsqueda:

public abstract class CommandManager {

	public Object process(Object commandState) {
		Command command = createCommand();
		command.setState(commandState);
		return command.execute();
	}

	@Lookup
	protected abstract Command createCommand();
}

Otra forma de acceder a beans de distinto ámbito es mediante un punto de inyección ObjectFactory o Provider.

También puede resultarte útil ServiceLocatorFactoryBean, ubicado en el paquete org.springframework.beans.factory.config.

Sustitución arbitraria de métodos

Una forma menos útil de inyección de métodos que la inyección de métodos de búsqueda es la posibilidad de sustituir métodos arbitrarios de un bean gestionado por otra implementación de método. Puedes omitir el resto de esta sección hasta que realmente necesites esta funcionalidad.

Con metadatos de configuración basados en XML, puedes utilizar el elemento replaced-method para sustituir la implementación de un método existente por otra en un bean desplegado. Considera la siguiente clase, que tiene un método llamado computeValue que queremos sobrescribir:

public class MyValueCalculator {

	public String computeValue(String input) {
		// Algún código real...
	}

	// Otros métodos...
}

Una clase que implemente la interfaz org.springframework.beans.factory.support.MethodReplacer proporciona la nueva definición del método:

/**
 * Diseñada para sustituir la implementación existente
 * de computeValue(String) en MyValueCalculator.
 */
public class ReplacementComputeValue implements MethodReplacer {

	public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
		// Obtiene el valor de entrada, trabaja con él y devuelve un resultado calculado.
		String input = (String) args[0];
		// ...
		return ...;
	}
}

La definición de bean para desplegar la clase original y especificar la sustitución del método se parecería al siguiente ejemplo:

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
	<!-- Sustitución arbitraria de método. -->
	<replaced-method name="computeValue" replacer="replacementComputeValue">
		<arg-type>String</arg-type>
	</replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

Puedes usar uno o más elementos <arg-type/> dentro de <replaced-method/> para indicar la firma del método que se sobrescribe. La firma de los argumentos solo es necesaria si el método está sobrecargado y existen varias variantes en la clase.

Para mayor comodidad, la cadena de tipo de un argumento puede ser una subcadena del nombre de tipo totalmente cualificado. Por ejemplo, todas las siguientes cadenas coinciden con java.lang.String:

java.lang.String
String
Str

Como a menudo el número de argumentos es suficiente para distinguir entre las posibles opciones, este atajo puede ahorrar bastante escritura, al permitir utilizar únicamente la cadena más corta que coincida con un tipo de argumento.