Curso Jakarta EE 9 (15). CDI (4): Productores

logo Jakarta EE

En ocasiones necesitamos intervenir en la creación de los objetos de CDI ejecutando nuestro propio código. O, tal vez, la estrategia consistente en definir CDI beans anotando las clases con calificadores y ámbitos es insuficiente. Ningún problema, los productores acuden al rescate.

>>>> ÍNDICE <<<<

Ciclo de vida

El contenedor de CDI es el responsable de gestionar el ciclo de vida de sus objetos, incluyendo su creación y destrucción según los ámbitos. Dos anotaciones nos permiten participar en ese ciclo.

@PostConstruct

Con esta anotación solicitamos la invocación de un método justo después de que el contenedor haya creado el objeto y resuelto sus posibles dependencias, pero antes de que sea inyectado. Este método puede tener cualquier visibilidad, aunque a veces nos convendrá que sea accesible por las pruebas, y no devuelve ni recibe nada.

@PostConstruct
public void setup() {
    logger.info("PostConstruct method invoked ");
}
@PreDestroy

Su funcionamiento es análogo a @PostConstruct. Ahora el método anotado se ejecuta cuando el contenedor ya no va a usar más el objeto y antes de ser liberado para que el recolector de basura pueda destruirlo. Por ejemplo, si la clase es de ámbito @ApplicationScoped, esta invocación se efectuará cuando la aplicación se detenga, o si es @RequestScoped, cuando finalice la petición HTTP a la que el objeto se encuentra asociado y el cliente haya recibido la respuesta. Por cierto, ambas anotaciones -@PostConstruct y @PreDestroy- cuentan con sus equivalentes en Spring.

@PreDestroy
void tearDown() {
    logger.info("PreDestroy method invoked ");
}

Productores

Hemos visto que @PostConstruct nos brinda la posibilidad de configurar los objetos a medida que van siendo creados. No obstante, la especificación ofrece una funcionalidad mucho más potente: escribir el código que el contenedor empleará para construir sus objetos. Esta técnica permite, entre otros, lo siguiente.

  • Crear objetos cuya implementación o configuración puede cambiar en tiempo de ejecución o que no puedan ser creados hasta ese momento porque, por ejemplo, necesitamos obtener cierta información desde una base de datos (es un caso real con el que me he encontrado en varias ocasiones).
  • Construir a partir de una misma clase objetos con ámbitos distintos.
  • Convertir cualquier clase en un CDI bean aunque no pertenezca a nuestro código. Con esto, me refiero a clases de librerías de terceros o de la propia SDK de Java.

Examinemos los dos tipos de productores existentes. En ambos casos, hay que crearlos en un CDI bean

Métodos

La práctica habitual para codificar la instanciación de un objeto por parte del contenedor de CDI consiste en implementar un método “factoría”, anotado con @Produces, que lo construya y devuelva. Este método, que de ahora en adelante llamaré “productor”, puede tener cualquier visibilidad e incluso ser estático, pero nunca abstracto. Su comportamiento es similar a los métodos anotados con @Bean en las clases @Configuration de Spring.

Veamos un primer ejemplo muy sencillo.

package com.danielme.jakartaee.cdi.producers;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;

import java.util.List;

public class ListsProducer {

    @Produces
    @ApplicationScoped
    List<String> produceColors() {
        return List.of("RED", "BLUE");
    }

}

Nuestro productor crea una lista de cadenas con el ámbito aplicación (recordemos que si no se indica, será dependent). Con lo que habíamos aprendido en el curso, para hacer lo mismo tendríamos que crear una clase que contenga la lista.

Nota. El método List#of de Java 9 devuelve una lista inmutable.

Inyectamos la lista en una clase con pruebas.

package com.danielme.jakartaee.cdi.producers;

import com.danielme.jakartaee.cdi.Deployments;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(ArquillianExtension.class)
class ProducersArquillianTest {

    @Inject
    private List<String> colors;

    @Deployment
    public static WebArchive createDeployment() {
        return Deployments.producers();
    }

    @Test
    void testColorsList() {
        assertThat(colors).containsExactlyInAnyOrder("RED", "BLUE");
    }

}

Si queremos crear otro listado de igual tipo, tenemos el mismo problema de ambigüedad que cuando intentábamos inyectar una interfaz con más de una implementación en el capítulo anterior. Lo solventamos del mismo modo: usando calificadores. Usaré @Named por simplicidad, aunque también se pueden emplear calificadores personalizados.

@Produces
@Named
@ApplicationScoped
List<String> produceColors() {
    return List.of("RED", "BLUE");
}

@Produces
@Named
@ApplicationScoped
List<String> produceNames() {
   return List.of("John", "Elaine");
}

El nombre por omisión de cada lista es el del método y podemos inyectarlas en un atributo de nombre coincidente.

@Inject
@Named
private List<String> produceColors;

@Inject
@Named
private List<String> produceNames;

@Test
void testColorsList() {
    assertThat(produceColors).containsExactlyInAnyOrder("RED", "BLUE");
}

@Test
void testNamesList() {
    assertThat(produceNames).containsExactlyInAnyOrder("John", "Elaine");
}

Reitero la recomendación de asignar un nombre a cada instancia, y así es como he dejado el código final en el repositorio.

@Produces
@Named("colors")
@ApplicationScoped
List<String> produceColors() {
    return Arrays.asList("RED", "BLUE");
}

@Produces
@Named("names")
@ApplicationScoped
List<String> produceNames() {
    return Arrays.asList("John", "Elaine");
}
@Inject
@Named("colors")
private List<String>  colors;

@Inject
@Named("names")
private List<String> names;

Si fuera necesario, el método productor puede recibir dependencias. Hagamos que la clase FileStorageLocal devuelva la siguiente lista con un nuevo método definido en la interfaz FileStorage.

package com.danielme.jakartaee.cdi.injection.file;

import jakarta.enterprise.context.ApplicationScoped;

import java.util.List;

@ApplicationScoped
@FileStorageLocalQualifier
public class FileStorageLocal implements FileStorage {

    @Override
    public List<String> availableFiles() {
        return List.of("JakartaEE.pdf");
    }

}

Esta lista es inmutable y la vamos a usar en varias clases -esto no es más que un ejemplo teórico-, así que la convertimos en una dependencia inyectable. En el método productor necesitamos recibir FileStorageLocal.

@Produces
@Named("filesAvailable")
@ApplicationScoped
List<String> fetchAvailableFiles(@FileStorageLocalQualifier FileStorage fileStorage) {
    return fileStorage.availableFiles();
}

Aquí aplica la misma restricción que vimos para la inyección en constructores: todos los parámetros del método son dependencias.

Comprobemos que el funcionamiento es el esperado.

@Inject
@Named("filesAvailable")
private List<String> files;

@Test
void testFilesList() {
    assertThat(files).containsExactly("JakartaEE.pdf");
}
Atributos

Un atributo también puede actuar de productor. Esto parece extraño, pero tiene, al menos, dos aplicaciones prácticas.

  • Inyectar solo un atributo de un objeto. Nos ahorramos inyectar la clase simplemente para invocar el getter correspondiente, el cual, quizás, ahora resulte prescindible.
  • Definir “recursos” como objetos inyectables. Los recursos son una especie de cajón de sastre que engloba varios objetos disponibles en una aplicación Jakarta EE en función de su tipo o configuración: el contexto de persistencia de Jakarta Persistence, las fuentes de datos configuradas en el servidor, colas de Jakarta Messaging (JMS), EJB remotos… Usaremos algunos de ellos a lo largo del curso.

Un buen ejemplo de esto último consiste en la conversión en un objeto inyectable por CDI de un objeto recuperable por su nombre a través de la API JNDI con la anotación @Resource. Echemos un vistazo al siguiente fragmento de código.

@Resource(lookup = "java:/jdbc/personalBudgetDS")
DataSource dataSource;

Inyecta una fuente de datos definida en el servidor de aplicaciones. Podemos repetir este código en donde necesitemos el DataSource, pero también contamos con esta opción.

@Produces
@Resource(lookup = "java:/jdbc/personalBudgetDS")
DataSource dataSource;

Gracias al productor, ahora se puede inyectar la fuente de datos como una dependencia más y de forma transparente, ocultando el hecho de que el objeto es un recurso del servidor del cual debemos conocer su nombre. Y si tuviéramos más de un DataSource en la aplicación, podemos distinguirlos con calificadores.

@Produces
@Resource(lookup = "java:/jdbc/personalBudgetDS")
@PersonalBudgetDatasource
DataSource dataSource;
@Inject
@PersonalBudgetDatasource
private DataSource dataSource;
@Disposes

Para los objetos creados por los productores tenemos la funcionalidad equivalente a @PreDestroy con la anotación @Disposes. Con ella indicamos en un método el parámetro en el que queremos recibir el objeto creado por el productor.

void close(@Disposes @PersonalBudgetDatasource DataSource datasource) {

}

La interfaz InjectionPoint

Cuando un productor construye objetos de ámbito @Dependent, puede recibir como parámetro una instancia de la clase InjectionPoint con los detalles del punto de inyección para el que está construyendo el nuevo objeto. Esta información incluye, entre otros, las anotaciones del punto, lo que resulta interesante porque nos permite “parametrizar” la creación del objeto proporcionando datos en la declaración de la inyección.

Supongamos que nuestra aplicación tiene un conjunto de parámetros de configuración (pares nombre\valor) bastante extenso, y queremos inyectarlos de forma individual. No es una situación nada descabellada, de hecho es rara la aplicación en la que no se requiera esta funcionalidad.

Para no complicarnos, vayamos a lo fácil -y típico-, y definamos estos parámetros en un fichero de propiedades estándar de Java situado en /src/main/resources/config.properties. Sólo tendrá un par de valores, suficiente para hacer las pruebas pertinentes.

key1 = value1
key2 = value2

Es posible utilizar productores con los conocimientos que ya tenemos, pero no hay que pensar mucho para darse cuenta de que esta estrategia es poco práctica si las propiedades abundan: necesitaríamos crear un productor para cada una de ellas.

@Produces
@Named
String propertyKey1() {
    return getProperty("key1");
}

@Produces
@Named
String propertyKey2() {
   return getProperty("key2");
}

Además, sería ideal hacer la inyección tal que así.

@Inject
@ConfigProperty("key1")
private String key1;

Práctico, ¿verdad? La anotación @ConfigProperty es un calificador personalizado, esto es, un @Qualifier que recibe, obligatoriamente, el nombre de la propiedad cuyo valor solicitamos.

package com.danielme.jakartaee.cdi.producers;

import jakarta.enterprise.util.Nonbinding;
import jakarta.inject.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface ConfigProperty {

    @Nonbinding String value();

}

No hay mucho que comentar sobre este código, pues ya vimos el uso de parámetros en los calificadores cuando desarrollamos en el capítulo anterior @CommandLineQualifier. Tan solo hay un detalle destacable: la anotación @Nonbinding. Con ella, estamos indicando que value no se usará para identificar a un CDI bean.

Este es el productor.

package com.danielme.jakartaee.cdi.producers;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.spi.InjectionPoint;

import java.io.IOException;
import java.util.Properties;

@ApplicationScoped
public class PropertiesProducer {

    private static final String CONFIG_PROPERTIES = "config.properties";
    private Properties properties;

    @PostConstruct
    public void loadProperties() throws IOException {
        properties = new Properties();
        properties.load(PropertiesProducer.class.getClassLoader().getResourceAsStream(CONFIG_PROPERTIES));
    }

    @Produces
    @ConfigProperty("")
    String getProperty(InjectionPoint injectionPoint) {
        String key = injectionPoint.getAnnotated().getAnnotation(ConfigProperty.class).value();
        return properties.getProperty(key);
    }

}

PropertiesProducer usa @PostConstruct para cargar en un objeto Properties el fichero config.properties, asumiendo que siempre va a existir, porque solo hará falta hacerlo una vez. Tenemos un único método productor que devuelve la cadena de tipo @ConfigProperty. Por tanto, solo hay un CDI bean para ese calificador, pero su contenido es variable y lo va a determinar el solicitante de la inyección. Esto es debido a que el ámbito de la cadena creada es @Dependent y cada vez que se pida un @ConfigProperty se devolverá una nueva.

Ahora ya estamos en condiciones de escribir las pruebas correspondientes para demostrar el funcionamiento de la inyección.

package com.danielme.jakartaee.cdi.producers;

import com.danielme.jakartaee.cdi.Deployments;
import jakarta.inject.Inject;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(ArquillianExtension.class)
class InjectionPointArquillianTest {

    @Inject
    @ConfigProperty("key1")
    private String key1;

    @Inject
    @ConfigProperty("key2")
    private String key2;

    @Inject
    @ConfigProperty("undefined")
    private String undefined;

    @Deployment
    public static WebArchive createDeployment() {
        return Deployments.injectionPoint();
    }

    @Test
    void testGetByAnnotationValue() {
        assertThat(key1).isEqualTo("value1");
    }

    @Test
    void testGetByMemberName() {
        assertThat(key2).isEqualTo("value2");
    }

    @Test
    void testUndefinedProperty() {
        assertThat(undefined).isNull();
    }

}

En el artefacto de la prueba debe incluirse el fichero de configuración.

public static WebArchive injectionPoint() {
    return ShrinkWrap.create(WebArchive.class, WAR_NAME)
            .addPackage(PropertiesProducer.class.getPackage())
            .addAsResource(new File("src/main/resources/"), "")
            .addAsLibraries(getAssertjFiles());
}

Antes de finalizar, es preciso hacer una aclaración. Si bien este ejemplo es funcional y con las adaptaciones necesarias podemos usarlo en nuestras aplicaciones Jakarta EE, hay una opción mejor para trabajar con ficheros de configuración: la especificación Configuration. Aunque pertenece a MicroProfile, está en WildFly lista para ser utilizada, así que no reinventemos la rueda.

Alternativas

Podemos usar el sistema de implementaciones alternativas cuando creamos los objetos con productores.

    @Produces
    @Named("names")
    @ApplicationScoped
    List<String> produceNames() {
        return List.of("John", "Elaine");
    }

    @Produces
    @Named("names")
    @Alternative
    @ApplicationScoped
    List<String> produceNamesAlternative() {
        return List.of("Michael");
    }

El fragmento de código anterior se comporta de forma esperada y CDI inyecta la lista names que no es alternativa (podemos verificarlo ejecutando ProducersArquillianTest). Sin embargo, a priori es imposible usar la alternativa porque hay que “activarla” en el fichero beans.xml indicando su clase.

<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">

    <alternatives>
        <class>?</class>
    </alternatives>

</beans>

¿Qué clase ponemos? La solución consiste en anotar con @Alternative toda la clase con los productores y usar esa. Eso sí, @Alternative se aplica a todos los objetos que se construyan.

@Alternative
public class ListAlternativesProducer {

    @Produces
    @Named("names")
    @ApplicationScoped
    List<String> produceNamesAlternative() {
        return List.of("Michael");
    }
}

Comprobamos que esto es así con una prueba…

@ExtendWith(ArquillianExtension.class)
class ProducersAlternativeArquillianTest {

    @Inject
    @Named("names")
    private List<String> names;

    @Deployment
    public static WebArchive createDeployment() {
        return Deployments.producersAlternatives();
    }

    @Test
    void testNamesList() {
        assertThat(names).containsExactly("Michael");
    }

}

…que use este fichero.

<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">

    <alternatives>
        <class>com.danielme.jakartaee.cdi.producers.ListAlternativesProducer</class>
    </alternatives>

</beans>

Código de ejemplo

El código de ejemplo del 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 .