JavaRush /Blog Java /Random-ES /Una breve incursión en la inyección de dependencia o "¿Qu...
Viacheslav
Nivel 3

Una breve incursión en la inyección de dependencia o "¿Qué más es CDI?"

Publicado en el grupo Random-ES
La base sobre la que se construyen ahora los marcos más populares es la inyección de dependencia. Sugiero mirar lo que dice la especificación CDI al respecto, qué capacidades básicas tenemos y cómo podemos usarlas.
Una breve incursión en la inyección de dependencia o

Introducción

Me gustaría dedicar esta breve reseña a algo como el CDI. ¿Qué es esto? CDI significa Contextos e Inyección de Dependencia. Esta es una especificación Java EE que describe la inyección de dependencia y los contextos. Para obtener información, puede consultar el sitio web http://cdi-spec.org . Dado que CDI es una especificación (una descripción de cómo debería funcionar, un conjunto de interfaces), también necesitaremos una implementación para usarlo. Una de esas implementaciones es Weld: http://weld.cdi-spec.org/ Para administrar dependencias y crear un proyecto, usaremos Maven: https://maven.apache.org Entonces, tenemos Maven instalado, ahora Lo entenderé en la práctica, para no entender lo abstracto. Para hacer esto, crearemos un proyecto usando Maven. Abramos la línea de comando (en Windows, puede usar Win+R para abrir la ventana "Ejecutar" y ejecutar cmd) y pedirle a Maven que haga todo por nosotros. Para ello, Maven tiene un concepto llamado arquetipo: Maven Archetype .
Una breve incursión en la inyección de dependencia o
Después de eso, en las preguntas " Elija un número o aplique filtro " y " Elija org.apache.maven.archetypes:maven-archetype-quickstart version ", simplemente presione Enter. A continuación, introduzca los identificadores del proyecto, el llamado GAV (consulte la Guía de convenciones de nomenclatura ).
Una breve incursión en la inyección de dependencia o
Después de la creación exitosa del proyecto, veremos la inscripción "BUILD SUCCESS". Ahora podemos abrir nuestro proyecto en nuestro IDE favorito.

Agregar CDI a un proyecto

En la introducción, vimos que CDI tiene un sitio web interesante: http://www.cdi-spec.org/ . Hay una sección de descarga, que contiene una tabla que contiene los datos que necesitamos:
Una breve incursión en la inyección de dependencia o
Aquí podemos ver cómo Maven describe el hecho de que usamos la API CDI en el proyecto. API es una interfaz de programación de aplicaciones, es decir, alguna interfaz de programación. Trabajamos con la interfaz sin preocuparnos de qué y cómo funciona detrás de esta interfaz. La API es un archivo jar que comenzaremos a utilizar en nuestro proyecto, es decir, nuestro proyecto empieza a depender de este jar. Por lo tanto, la API CDI para nuestro proyecto es una dependencia. En Maven, un proyecto se describe en archivos POM.xml ( POM - Modelo de objetos del proyecto ). Las dependencias se describen en el bloque de dependencias, al que debemos agregar una nueva entrada:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
Como habrás notado, no especificamos el alcance con el valor proporcionado. ¿Por qué existe tal diferencia? Este alcance significa que alguien nos proporcionará la dependencia. Cuando una aplicación se ejecuta en un servidor Java EE, significa que el servidor proporcionará a la aplicación todas las tecnologías JEE necesarias. Para simplificar esta revisión, trabajaremos en un entorno Java SE, por lo que nadie nos proporcionará esta dependencia. Puede leer más sobre el alcance de la dependencia aquí: " Alcance de la dependencia ". Bien, ahora tenemos la capacidad de trabajar con interfaces. Pero también necesitamos implementación. Como recordamos, usaremos Weld. Es interesante que en todas partes se den diferentes dependencias. Pero seguiremos la documentación. Por lo tanto, leamos " 18.4.5. Configuración del Classpath " y hagamos lo que dice:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
Es importante que las versiones de tercera línea de Weld admitan CDI 2.0. Por tanto, podemos contar con la API de esta versión. Ahora estamos listos para escribir código.
Una breve incursión en la inyección de dependencia o

Inicializando un contenedor CDI

CDI es un mecanismo. Alguien debe controlar este mecanismo. Como ya hemos leído anteriormente, dicho administrador es un contenedor. Por lo tanto, necesitamos crearlo; él mismo no aparecerá en el entorno SE. Agreguemos lo siguiente a nuestro método principal:
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
Creamos el contenedor CDI manualmente porque... Trabajamos en un entorno SE. En proyectos de combate típicos, el código se ejecuta en un servidor, que proporciona varias tecnologías al código. En consecuencia, si el servidor proporciona CDI, esto significa que el servidor ya tiene un contenedor CDI y no necesitaremos agregar nada. Pero para los propósitos de este tutorial, tomaremos el entorno SE. Además, el contenedor está aquí, de forma clara y comprensible. ¿Por qué necesitamos un contenedor? El contenedor interior contiene frijoles (frijoles CDI).
Una breve incursión en la inyección de dependencia o

Frijoles CDI

Entonces, frijoles. ¿Qué es un contenedor CDI? Esta es una clase de Java que sigue algunas reglas. Estas reglas se describen en la especificación, en el capítulo " 2.2. ¿Qué tipos de clases son beans? ". Agreguemos un bean CDI al mismo paquete que la clase App:
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
Ahora podemos llamar a este bean desde nuestro mainmétodo:
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
Como puede ver, no creamos el bean usando la nueva palabra clave. Le preguntamos al contenedor CDI: "Contenedor CDI. Realmente necesito una instancia de la clase Logger, dámela, por favor". Este método se llama " Búsqueda de dependencias ", es decir, buscar dependencias. Ahora creemos una nueva clase:
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
Una clase primitiva que devuelve una representación de texto de una fecha. Ahora agreguemos la salida de la fecha al mensaje:
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
Ha aparecido una interesante anotación @Inject. Como se indica en el capítulo " 4.1. Puntos de inyección " de la documentación de soldadura cdi, utilizando esta anotación definimos el Punto de Inyección. En ruso, esto puede leerse como “puntos de implementación”. El contenedor CDI los utiliza para inyectar dependencias al crear instancias de beans. Como puede ver, no asignamos ningún valor al campo dateSource. La razón de esto es el hecho de que el contenedor CDI permite que los beans CDI internos (sólo aquellos beans que él mismo creó una instancia, es decir, que administra) utilicen " Inyección de dependencia ". Esta es otra forma de Inversión de Control , un enfoque en el que la dependencia es controlada por otra persona en lugar de que nosotros creemos explícitamente los objetos. La inyección de dependencia se puede realizar mediante un método, constructor o campo. Para obtener más detalles, consulte el capítulo de especificación CDI " 5.5. Inyección de dependencia ". El procedimiento para determinar qué se debe implementar se llama resolución de tipo seguro, que es de lo que debemos hablar.
Una breve incursión en la inyección de dependencia o

Resolución de nombres o resolución Typesafe

Normalmente, se utiliza una interfaz como tipo de objeto a implementar y el propio contenedor CDI determina qué implementación elegir. Esto es útil por muchas razones, que discutiremos. Entonces tenemos una interfaz de registrador:
public interface Logger {
    void print(String message);
}
Dice que si tenemos algún registrador, podemos enviarle un mensaje y completará su tarea: registrar. Cómo y dónde no será de interés en este caso. Ahora creemos una implementación para el registrador:
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
Como puede ver, este es un registrador que escribe en System.out. Maravilloso. Ahora, nuestro método principal funcionará como antes. Logger logger = container.select(Logger.class).get(); El registrador seguirá recibiendo esta línea. Y lo bueno es que solo necesitamos conocer la interfaz, y el contenedor CDI ya piensa en la implementación por nosotros. Digamos que tenemos una segunda implementación que debería enviar el registro a algún lugar de almacenamiento remoto:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
Si ahora ejecutamos nuestro código sin cambios, obtendremos un error, porque El contenedor CDI ve dos implementaciones de la interfaz y no puede elegir entre ellas: ¿ org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger Qué hacer? Hay varias variaciones disponibles. La más simple es la anotación @Vetoed para un bean CDI para que el contenedor CDI no perciba esta clase como un bean CDI. Pero hay un enfoque mucho más interesante. Un bean CDI se puede marcar como "alternativa" utilizando la anotación @Alternativedescrita en el capítulo " 4.7. Alternativas " de la documentación de Weld CDI. ¿Qué significa? Esto significa que, a menos que digamos explícitamente que lo usemos, no será seleccionado. Esta es una versión alternativa del frijol. Marquemos el bean NetworkLogger como @Alternative y podremos ver que el código se ejecuta nuevamente y es utilizado por SystemOutLogger. Para habilitar la alternativa debemos tener un archivo beans.xml . Puede surgir la pregunta: " beans.xml, ¿dónde te pongo? " Por tanto, coloquemos el archivo correctamente:
Una breve incursión en la inyección de dependencia o
Tan pronto como tengamos este archivo, el artefacto con nuestro código se llamará “ Archivo de bean explícito ”. Ahora tenemos 2 configuraciones separadas: software y xml. El problema es que cargarán los mismos datos. Por ejemplo, la definición del bean DataSource se cargará 2 veces y nuestro programa fallará cuando se ejecute, porque El contenedor CDI los considerará como 2 beans separados (aunque en realidad son la misma clase, de la cual el contenedor CDI aprendió dos veces). Para evitar esto hay 2 opciones:
  • elimine la línea initializer.addPackages(App.class.getPackage())y agregue una indicación de la alternativa al archivo xml:
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • agregue un atributo bean-discovery-modecon el valor " none " al elemento raíz del beans y especifique una alternativa mediante programación:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
Por lo tanto, utilizando la alternativa CDI, el contenedor puede determinar qué bean seleccionar. Curiosamente, si el contenedor CDI conoce varias alternativas para la misma interfaz, podemos saberlo indicando la prioridad mediante una anotación @Priority(desde CDI 1.1).
Una breve incursión en la inyección de dependencia o

Clasificatorios

Por separado, vale la pena discutir el tema de las eliminatorias. El calificador se indica mediante una anotación encima del bean y refina la búsqueda del bean. Y ahora más detalles. Curiosamente, cualquier frijol CDI en cualquier caso tiene al menos un calificador: @Any. Si no especificamos NINGÚN calificador encima del bean, pero luego el contenedor CDI agrega @Anyotro calificador al calificador: @Default. Si especificamos algo (por ejemplo, especificamos explícitamente @Any), entonces el calificador @Default no se agregará automáticamente. Pero lo bueno de los clasificatorios es que puedes crear tus propios clasificatorios. El calificador casi no se diferencia de las anotaciones, porque En esencia, esto es solo una anotación escrita de una manera especial. Por ejemplo, puede ingresar Enum para el tipo de protocolo:
public enum ProtocolType {
    HTTP, HTTPS
}
A continuación podemos hacer un calificador que tendrá en cuenta este tipo:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
Vale la pena señalar que los campos marcados como @Nonbindingno afectan la determinación del calificador. Ahora necesitas especificar el calificador. Está indicado encima del tipo de bean (para que CDI sepa definirlo) y encima del Punto de Inyección (con la anotación @Inject para que entiendas qué bean buscar para la inyección en este lugar). Por ejemplo, podemos agregar alguna clase con un calificador. Para simplificar, para este artículo los haremos dentro del NetworkLogger:
public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
Y luego, cuando realicemos Inject, especificaremos un calificador que influirá en qué clase se utilizará:
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
Genial, ¿no?) Parece hermoso, pero no está claro por qué. Ahora imagina lo siguiente:
Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
De esta manera podemos anular la obtención del valor para que pueda calcularse dinámicamente. Por ejemplo, se puede tomar de algunas configuraciones. Luego podemos cambiar la implementación incluso sobre la marcha, sin volver a compilar ni reiniciar el programa/servidor. Se vuelve mucho más interesante, ¿no? )
Una breve incursión en la inyección de dependencia o

productores

Otra característica útil del CDI son los productores. Estos son métodos especiales (están marcados con una anotación especial) que se llaman cuando algún bean ha solicitado una inyección de dependencia. Se describen más detalles en la documentación, en la sección " 2.2.3. Métodos de productor ". El ejemplo más simple:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
Ahora, al inyectar en campos de tipo Integer, se llamará a este método y se obtendrá un valor de él. Aquí debemos entender inmediatamente que cuando vemos la palabra clave nueva, debemos entender inmediatamente que NO es un bean CDI. Es decir, una instancia de la clase Random no se convertirá en un bean CDI sólo porque deriva de algo que controla el contenedor CDI (en este caso, el productor).
Una breve incursión en la inyección de dependencia o

Interceptores

Los interceptores son interceptores que "interfieren" en el trabajo. En CDI esto se hace con bastante claridad. Veamos cómo podemos registrar utilizando intérpretes (o interceptores). Primero, necesitamos describir el enlace al interceptor. Como muchas cosas, esto se hace mediante anotaciones:
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
Lo principal aquí es que se trata de un enlace para el interceptor ( @InterceptorBinding), que será heredado por extends ( @InterceptorBinding). Ahora escribamos el interceptor en sí:
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
Puede leer más sobre cómo se escriben los interceptores en el ejemplo de la especificación: " 1.3.6. Ejemplo de interceptor ". Bueno, todo lo que tenemos que hacer es encender el inerceptor. Para hacer esto, especifique la anotación vinculante encima del método que se está ejecutando:
@ConsoleLog
public void print(String message) {
Y ahora otro detalle muy importante. Los interceptores están deshabilitados de forma predeterminada y deben habilitarse de la misma manera que las alternativas. Por ejemplo, en el archivo beans.xml :
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
Como puedes ver, es bastante simple.
Una breve incursión en la inyección de dependencia o

Eventos y observadores

CDI también proporciona un modelo de eventos y observadores. Aquí no todo es tan obvio como con los interceptores. Entonces, el Evento en este caso puede ser absolutamente de cualquier clase, no se necesita nada especial para la descripción. Por ejemplo:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
Ahora alguien debería esperar el evento:
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
Lo principal aquí es especificar la anotación @Observes, que indica que este no es solo un método, sino un método que debe llamarse como resultado de observar eventos del tipo LogEvent. Bueno, ahora necesitamos a alguien que mire:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
Tenemos un método único que le indicará al contenedor que se ha producido un evento para el tipo de evento LogEvent. Ahora sólo queda utilizar el observador. Por ejemplo, en NetworkLogger podemos agregar una inyección de nuestro observador:
@Inject
private LogObserver observer;
Y en el método print podemos notificar al observador que tenemos un nuevo evento:
public void print(String message) {
	observer.observe(new LogEvent());
Es importante saber que los eventos se pueden procesar en un hilo o en varios. Para el procesamiento asincrónico, utilice un método .fireAsync(en lugar de .fire) y una anotación @ObservesAsync(en lugar de @Observes). Por ejemplo, si todos los eventos se ejecutan en subprocesos diferentes, entonces si un subproceso genera una excepción, los demás podrán hacer su trabajo para otros eventos. Puedes leer más sobre eventos en CDI, como es habitual, en la especificación, en el capítulo " 10. Eventos ".
Una breve incursión en la inyección de dependencia o

Decoradores

Como vimos anteriormente, bajo el ala CDI se recogen varios patrones de diseño. Y aquí hay otro: un decorador. Esto es algo muy interesante. Echemos un vistazo a esta clase:
@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
Al declararlo decorador decimos que cuando se utilice cualquier implementación de Logger se utilizará este “complemento” que conoce la implementación real, la cual se almacena en el campo delegado (ya que está marcado con la anotación @Delegate). Los decoradores sólo pueden asociarse con un bean CDI, que en sí mismo no es ni un interceptor ni un decorador. También se puede ver un ejemplo en la especificación: " 1.3.7. Ejemplo de decorador ". El decorador, al igual que el interceptor, debe estar activado. Por ejemplo, en beans.xml :
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
Para más detalles ver referencia de soldadura: " Capítulo 10. Decoradores ".

Ciclo vital

Los frijoles tienen su propio ciclo de vida. Se parece a esto:
Una breve incursión en la inyección de dependencia o
Como puede ver en la imagen, tenemos las llamadas devoluciones de llamada del ciclo de vida. Estas son anotaciones que le indicarán al contenedor CDI que llame a ciertos métodos en una determinada etapa del ciclo de vida del bean. Por ejemplo:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
Este método se llamará cuando un contenedor cree una instancia de un bean CDI. Lo mismo sucederá con @PreDestroy cuando el bean se destruya cuando ya no sea necesario. No en vano el acrónimo CDI contiene la letra C - Contexto. Los beans en CDI son contextuales, lo que significa que su ciclo de vida depende del contexto en el que existen dentro del contenedor CDI. Para comprender esto mejor, debe leer la sección de especificaciones " 7. Ciclo de vida de instancias contextuales ". También vale la pena saber que el contenedor en sí tiene un ciclo de vida, sobre el cual puede leer en " Eventos del ciclo de vida del contenedor ".
Una breve incursión en la inyección de dependencia o

Total

Arriba miramos la punta del iceberg llamado CDI. CDI es parte de la especificación JEE y se utiliza en el entorno JavaEE. Quienes usan Spring no usan CDI, sino DI, es decir, son especificaciones ligeramente diferentes. Pero sabiendo y entendiendo lo anterior, puedes cambiar de opinión fácilmente. Teniendo en cuenta que Spring admite anotaciones del mundo CDI (el mismo Inject). Materiales adicionales: #viacheslav
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION