Curso Jakarta EE 9 (12). CDI (1): Inyección de dependencias.

logo Jakarta EE

La primera especificación de Jakarta EE\ Java EE que vamos a estudiar en profundidad es una de las más importantes aunque suele pasar desapercibida en comparación con Jakarta Persistence o Jakarta REST. Pero Jakarta CDI, abreviatura de “Contexts and dependency injections” (inyección de contextos y dependencias), es el pilar sobre el que construiremos nuestras aplicaciones.

>>>> ÍNDICE <<<<

Inyección de dependencias

Antes de nada, tenemos que comprender en qué consiste la inyección de dependencias, uno de los patrones de diseño basado en el principio “Inversión de control” (IoC) y el corazón de la especificación CDI. Mucha atención, pues es un concepto complicado la primera vez que nos acercamos a él y su comprensión resulta esencial.

Empecemos con un ejemplo. Supongamos que existe una clase, llamada A, que tiene que realizar ciertas operaciones con los datos que le proporciona un objeto de otra clase, por ejemplo, B. Siguiendo la estrategia “tradicional”, A tendría que crear una instancia de B llamando a su constructor o bien empleando un patrón de diseño creacional, por ejemplo, algún tipo de factoría. Así, vamos construyendo toda la aplicación mediante la interacción de sus clases.

Con inyección de dependencias, la clase A no tiene que instanciar un objeto de la clase B, que a partir de ahora vamos a considerar como una dependencia de A. Lo recibe en tiempo de ejecución de forma automática solo por haber declarado que lo necesita. Dicho de otro modo, una instancia de B es “inyectada” en A “mágicamente”. El control se invierte en lo que respecta a la creación de objetos de forma un tanto desconcertante: en lugar de tenerlo nosotros, lo asume un elemento externo (una librería, marco de trabajo, etc.), representando en la siguiente ilustración por la caja verde.

La clase A utiliza los métodos de B, pero no sabe cómo construir u obtener un objeto de B; eso ha dejado de ser una responsabilidad suya. Esta abstracción puede llevarse un paso más allá si los servicios que ofrece B se modelan con una interfaz, llamémosla iB. Ahora, en A no solicitamos la inyección de un objeto de B, sino de uno perteneciente a una clase que implemente iB. De este modo, A ni siquiera conoce la existencia de B, solo quiere utilizar las operaciones de iB.

Hemos reducido al mínimo el acoplamiento entre clases y pasamos a trabajar con abstracciones. La aplicación se va construyendo con módulos independientes e intercambiables, y el código es más fácil de leer y evolucionar. También es más sencillo probarla: las dependencias pueden tener versiones específicas para los tests. Seremos capaces de inyectar dobles de tests (mocks) cuando sea necesario y sin modificar el código de la aplicación.

La técnica anterior ocupa un lugar prominente en las prácticas de código limpio propuestas por Robert Martin y resumidas en los principios SOLID tan en boga hoy en día. La D significa “Dependency Inversion” y defiende el uso de abstracciones frente a implementaciones concretas. Combinando este principio con la inyección de dependencias tenemos una apuesta ganadora.

La especificación Jakarta CDI

Todo lo anterior está muy bien, pero ¿cómo programamos la “caja verde” de las figuras? En nuestro caso, no hará falta. Será una implementacfs muy similar alión compatible con la especificación Jakarta CDI la responsable de crear, inyectar y destruir los objetos de las clases que van a ser utilizadas como dependencias y\o receptoras de las mismas. Estos objetos son gestionados por un componente denominado “contenedor de dependencias de CDI” y “viven” dentro de él según un ciclo de vida bien definido. Además de la inyección, el contenedor nos ofrece funcionalidades adicionales y potentes para los objetos que gestiona, tales como la posibilidad de utilizar programación orientada a aspectos (interceptores) o un sistema de comunicación fundamentado en eventos (otro tipo de inversión de control). Pero todo a su debido tiempo.

La especificación CDI debutó en la versión 5 de JEE (2006). Al igual que otras tantas, nació como una estandarización de productos con gran adopción por parte del mercado, en este caso concreto el ya desaparecido JBoss Seam. Poco a poco, ha ido asumiendo la gestión de dependencias que realizaban las especificaciones JSF y EJB. En su versión actual, incluso puede utilizarse con facilidad en proyectos que no se ejecuten en un servidor de aplicaciones. De cara al futuro, se pretende seguir avanzando en su modularización y en la inclusión de funcionalidades de otras especificaciones, en especial EJB, que tienen más sentido que formen parte de CDI.

A nivel conceptual, la inyección de dependencias de Jakarta CDI es muy similar a la que encontramos en Spring. Si el lector ya tiene experiencia con este celebérrimo marco de trabajo, estos capítulos le resultarán familiares: en esencia, aprenderá a realizar tareas similares en el mundo Jakarta EE. Por eso, y sin entrar en detalles, a veces hablaré de Spring para subrayar las diferencias más significativas.

Weld es la implementación compatible con Jakarta CDI -e implementación de referencia en JEE 8- que incluye WildFly. Su desarrollo corre a cargo de Red Hat\JBoss, así que todo queda en casa. En el curso, nos vamos a ceñir al estándar y no examinaremos características exclusivas de Weld. Un producto alternativo es Apache OpenWebBeans, incluido en el servidor Apache TomEE

CDI Beans

El primer ejemplo de inyección de dependencias con CDI ya lo vimos en el proyecto del capítulo Testing en WildFly con Arquillian. Echemos un vistazo a HelloServlet.

package com.danielme.jakartaee.arquillian;

import jakarta.inject.Inject;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(HelloServlet.URL)
public class HelloServlet extends HttpServlet {

    public static final String URL = "helloServlet";

    @Inject
    private Message message;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        PrintWriter printWriter = response.getWriter();
        response.setContentType("text/plain;charset=UTF-8");
        printWriter.print(message.get());
    }

}

La anotación @Inject (*) solicita al contenedor de CDI la recepción (inyección) en el atributo message de una instancia de Message. Se inyectará justo después de que el contenedor de servlets cree HelloServlet, de tal modo que podemos utilizarla en el método doGet para devolver la cadena que retorna Message#get sin temor a un NullPointer. Esto es posible porque la especificación Jakarta Servlet exige que los servlets en un servidor de aplicaciones admitan inyecciones CDI, tal y como recoge la documentación oficial.

(*) Esta anotación en realidad no pertenece a CDI sino a la especificación Jakarta Dependency Injection, API que tan solo define seis anotaciones.

Tenemos acceso directo al contenedor con la clase abstracta CDI. En HelloServlet podríamos recuperar Message sin necesidad de inyectarla.

Message message = CDI.current().select(Message.class).get();

Esta alternativa permite usar CDI en clases en las que no sea posible la inyección. Será raro que tengamos que recurrir a ella, pero es útil conocer esta posibilidad.

Las clases gestionadas por el contendor (e inyectables) se denominan CDI beans. ¿Cuáles son? La documentación de la especificación señala que son las marcadas con alguna de las siguientes anotaciones: @Interceptor, @Decorator, @Dependent y todas aquellas de tipo @NormalScope o @Stereotype. Es el caso de Message: la anotación @ApplicationScoped es de tipo @NormalScope.

package com.danielme.jakartaee.cdi;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class Message {

    private static final String HELLO_MESSAGE = "Jakarta EE rocks!!";

    public String get() {
        return HELLO_MESSAGE;
    }

}

Este criterio es el comportamiento predeterminado. Se personaliza en el fichero de configuración /src/main/webapp/WEB-INF/beans.xml con la propiedad bean-discovery-mode.

<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_3_0.xsd"
       version="3.0"
       bean-discovery-mode="all">

</beans>

Aprovecho para recordar que los espacios de nombres de los XML definidos por Jakarta EE no son los mismos que los empleados en JEE.

Hay tres valores posibles de esta propiedad.

  • annotated (clases anotadas). La opción predeterminada que acabo de comentar. Si es lo que queremos, no es necesario crear el fichero beans.xml –salvo que lo necesitemos para hacer otras configuraciones que ya veremos. Dejó de ser obligatorio en CDI 1.1 (JEE 7), aunque todavía algunos tutoriales indiquen lo contrario.
  • none (ninguna). Desactiva CDI.
  • all. Todas las clases del módulo en el que se encuentra el fichero son CDI beans.

Por lo general, se suele utilizar la tercera opción y en la mayoría de aplicaciones encontraremos un fichero beans.xml como el anterior.

Aunque usemos annotated o all, es posible excluir clases y paquetes completos.

<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_3_0.xsd"
       version="3.0"
       bean-discovery-mode="all">

    <scan>
      <exclude name="com.danielme.jakartaee.cdi.Message" />
    </scan>

</beans>

Si usamos este fichero en la aplicación de ejemplo, WildFly es incapaz de desplegarla porque no puede realizar la inyección de Message en HelloServlet. La excepción es la siguiente.

20:49:03,530 ERROR [org.jboss.as.server] (management-handler-thread - 1) WFLYSRV0021: Deploy of deployment "arquillian.war" was rolled back with the following failure message: 
{"WFLYCTL0080: Failed services" => {"jboss.deployment.unit.\"arquillian.war\".WeldStartService" => "Failed to start service
    Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type Message with qualifiers @Default
  at injection point [BackedAnnotatedField] @Inject private com.danielme.jakartaee.cdi.HelloServlet.message
  at com.danielme.jakartaee.cdi.HelloServlet.message(HelloServlet.java:0)
"}}

El mensaje pone de relieve una característica de CDI: es imprescindible satisfacer todas las dependencias. En otras palabras, las dependencias no son opcionales.

Las exclusiones pueden registrarse de forma programática con la anotación @Vetoed. Se aplica a una clase tal y como cabría esperar, pero también permite ocultar todo un paquete a CDI del siguiente modo.

@Vetoed
package com.danielme.jakartaee.cdi;

import jakarta.enterprise.inject.Vetoed;

Este fragmento es el código completo de package-info.java. A pesar de su extensión, los ficheros package-info no definen una clase. Se utilizan para proporcionar a nivel de paquete comentarios javadoc y anotaciones que incluyan en su “target” el tipo ElementType.PACKAGE.

Si fuera necesario utilizar un fichero beans.xml en nuestras pruebas con Arquillian, lo añadimos al artefacto.

 return ShrinkWrap.create(WebArchive.class, "arquillian-test.war")
                .addClass(Message.class)
                .addClass(HelloServlet.class)
                .addAsWebInfResource(new FileAsset(new File("src/main/webapp/WEB-INF/beans.xml")), "beans.xml")
                .addAsLibraries(assertjFiles);

En el ejemplo se ha usado el fichero beans.xml de la propia aplicación. Lo común será disponer de versiones específicas para usarlas en las pruebas tal y como haremos en las próximas entregas del curso.

Puntos de inyección

Una clase puede recibir sus dependencias de tres formas distintas, utilizando siempre @Inject.

  • Directamente en sus atributos. Es lo más legible y habitual. No es necesario que el atributo tenga getters o setters, tampoco importa su visibilidad.
  • A través de uno o varios métodos llamados “iniciadores” (podrían ser setters pero no es obligatorio) que reciban como argumentos las dependencias (todos los parámetros del método han de serlo).
 @Inject 
private void initDependencies(DependencyA dependencyA, DependencyB dependencyB) {     
    this.dependencyA = dependencyA;     
    this.dependencyB = dependencyB; 
}
  • Con un constructor que reciba todas las dependencias. Podemos tener todos los constructores que queramos, pero solo uno puede estar anotado con @Inject. Nuestro servlet de ejemplo quedaría así.
private final Message message; 

@Inject 
public HelloServlet(Message message) {     
    this.message = message; 
}

Mi recomendación personal, y esto aplica también a Spring, es realizar la inyección vía constructores para disfrutar siguientes beneficios.

  • Las dependencias son inmutables y pueden declararse como tales (final) porque siempre se inician en todos los constructores de la clase. Con las otras estrategias, no. Trabajar con clases inmutables -una vez creadas, su estado no cambia- aporta grandes ventajas, como la facilidad de la reutilización de sus objetos con seguridad o de su testeo.
  • Los objetos pueden crearse llamando a sus constructores. Admito que esto parece poco útil si consideramos que la clase que recibe las dependencias va a ser creada por el contenedor de CDI. Pero nos va a permitir realizar pruebas unitarias sobre esas clases: las instanciamos con los objetos que necesitemos -por ejemplo, mocks– y las probamos sin necesidad de ejecutar el contenedor para evitar el elevado consumo de tiempo que esta acción implica. Se puede conseguir lo mismo con métodos iniciadores, pero estos han de tener la visibilidad adecuada para que puedan ser llamados desde el código de pruebas y resulta más natural crear un objeto con un constructor.
  • Los constructores son un buen indicador de un diseño pobre: si tienen muchos argumentos, resulta sospechoso y quizás la clase no esté bien cohesionada (*). Si vamos inyectando las dependencias como atributos, este hecho resulta menos evidente.

(*) Una clase poco cohesionada realiza muchas tareas poco relacionadas y viola el principio de responsabilidad única (Single responsability), fundamental en un buen diseño orientado a objetos. Las clases cohesionadas son más pequeñas y fáciles de reutilizar y mantener.

Lastimosamente, hay una limitación técnica en CDI a la hora de declarar inyecciones en constructores: las clases que se instancien como un proxy o representante (veremos esto en el próximo capítulo) requieren un constructor sin argumentos para que el contenedor pueda crear ese proxy. Si este mandato no se respeta, Weld mostrará un error como el siguiente.

WELD-001435: Normal scoped bean class com.danielme.jakartaee.cdi.scope.ConversationScopedDependency is not proxyable because it has no no-args constructor

Este requisito lo impone la especificación para facilitar el desarrollo de las implementaciones y, por tanto, Weld no tiene más remedio que cumplirlo. No obstante, en la práctica no necesita el constructor vacío y la restricción se puede desactivar dando a la propiedad org.jboss.weld.construction.relaxed el valor true en el fichero /src/main/resources/weld.properties. Si queremos respetar el estándar y desarrollar aplicaciones portables entre servidores, debemos obviar esta configuración, aunque lo cierto es que la mayoría de servidores de aplicaciones optan por Weld como proveedor de CDI.

Jakarta CDI en IntelliJ

IntelliJ Ultimate incluye una pequeña herramienta que nos ayuda a navegar por los CDI Beans declarados en nuestras aplicaciones. A través del menú View->Tool Windows->CDI se activa esta vista.

IntelliJ CDI tool

Código de ejemplo

El código de ejemplo para este capítulo se encuentra en GitHub (todos los proyectos están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .