Curso Jakarta EE 9 (14). CDI (3): Calificadores (Qualifiers)

logo Jakarta EE

Tras abordar la inyección de dependencias que ofrece CDI y sus ámbitos, continuamos profundizando en el uso de esta técnica. Estudiaremos funcionalidades más avanzadas que permitirán abarcar un amplio abanico de situaciones que encontraremos en nuestro día a día desarrollando aplicaciones con Jakarta EE.

>>>> ÍNDICE <<<<

El problema con las interfaces

Uno de los grandes beneficios de la inyección de dependencias es el elevado nivel de abstracción y modularidad que obtenemos en nuestras aplicaciones al utilizar interfaces, siguiendo el principio de código limpio “depender de abstracciones, no de implementaciones”. No obstante, nos hemos limitado a inyectar directamente clases para simplificar el aprendizaje inicial. En esta sección ya estamos en condiciones de dar un pasito más y realizar la inyección de interfaces, solventando la problemática que esto puede acarrear si tenemos múltiples implementaciones.

Vamos a crear una interfaz que defina un conjunto de servicios relacionados con el almacenamiento de ficheros. De momento, no necesitamos ninguna operación.

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

public interface FileStorage {
}

Tendremos una implementación para trabajar con los archivos de forma local. Es una clase inmutable de la que solo necesitamos una instancia, así que la anotamos con @ApplicationScoped.

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

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class FileStorageLocal implements FileStorage {
}

Si inyectamos la dependencia a través de la interfaz, recibimos la instancia de FileStorageLocal.

@Inject
FileStorage fileStorage;

Pero también queremos utilizar un almacenamiento remoto.

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

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class FileStorageRemote implements FileStorage {
}

¿Cómo sabe el contenedor qué FileStorage debe inyectar? No tiene la información suficiente para decidirlo, y al desplegar la aplicación en WildFly veremos este error.

20:07:36,978 ERROR [org.jboss.as.server] (management-handler-thread - 1) WFLYSRV0021: Deploy of deployment "cdi.war" was rolled back with the following failure message: 
{"WFLYCTL0080: Failed services" => {"jboss.deployment.unit.\"cdi.war\".WeldStartService" => "Failed to start service
    Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001409: Ambiguous dependencies for type FileStorage with qualifiers @Default
  at injection point [BackedAnnotatedField] @Inject com.danielme.jakartaee.cdi.file.FileService.fileStorage
  at com.danielme.jakartaee.cdi.file.FileService.fileStorage(FileService.java:0)
  Possible dependencies: 
  - Managed Bean [class com.danielme.jakartaee.cdi.file.FileStorageRemote] with qualifiers [@Any @Default],
  - Managed Bean [class com.danielme.jakartaee.cdi.file.FileStorageLocal] with qualifiers [@Any @Default]

IntelliJ también nos indica esa ambigüedad.

El mensaje es bastante descriptivo e informa de las clases candidatas para resolver la inyección. Tenemos que configurar las clases y puntos de inyección para que CDI proporcione la implementación que necesitemos recurriendo al uso de calificadores (qualifiers). Se trata de anotaciones que permiten identificar las clases en el contenedor de CDI para indicar las que queremos recibir en los puntos de inyección cuando existan varias opciones. Son la solución al interrogante del siguiente diagrama

cdi ambiguos

Primero veamos cómo crear calificadores a medida. Luego examinaremos algunos de los ofrecidos por la especificación.

Calificadores personalizados

Crear nuestro propio calificador es tan sencillo como definir una anotación de tipo @Qualifier. Necesitamos uno para cada FileStorage.

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

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 FileStorageLocalQualifier {
}
package com.danielme.jakartaee.cdi.injection.file;

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 FileStorageRemoteQualifier {
}

Aunque el lector nunca haya creado una anotación, puede comprobar que es sencillo. Se realiza empleando, valga la redundancia, ciertas anotaciones. Con @Retention estamos indicando hasta qué momento son accesibles los datos: solo en el código (SOURCE), al compilar (CLASS, opción predeterminada), o siempre disponible (RUNTIME). Elegimos el último valor con el fin de que el contenedor de CDI pueda acceder a la anotación durante la ejecución de la aplicación.

Con @Target se definen los elementos anotables. Queremos permitir el uso de FileStorageRemoteQualifier en cualquier punto de inyección, así que tenemos field (atributo), method (método) y parameter (parámetro de un método).

Aplicamos estos calificadores a las clases correspondientes.

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

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
@FileStorageLocalQualifier
public class FileStorageLocal implements FileStorage {

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

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
@FileStorageRemoteQualifier
public class FileStorageRemote implements FileStorage {

}

Nuestras implementaciones ahora son identificables de forma unívoca con los calificadores que hemos creado.

cdi custom qualifier

Para indicar cuál de ellas queremos inyectar, volvemos a usar los calificadores.

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

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 FileStorageInjectionArquillianTest {

    @Inject
    @FileStorageRemoteQualifier
    private FileStorage fileStorageRemote;

    @Inject
    @FileStorageLocalQualifier
    private FileStorage fileStorageLocal;

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

    @Test
    void testFileStorageIsLocal() {
        assertThat(fileStorageLocal).isInstanceOf(FileStorageLocal.class);
    }

    @Test
    void testFileStorageIsRemote() {
        assertThat(fileStorageRemote).isInstanceOf(FileStorageRemote.class);
    }

}

Nota. La creación de los artefactos a desplegar por Arquillian se ha encapsulado en la clase Deployments.

A continuación se muestra el uso del calificador en los otros puntos de inyección posibles, acompañando siempre a @Inject.

  • En un constructor.
 private FileStorage fileStorageLocal; 

@Inject 
public FileService(@FileStorageLocalQualifier FileStorage fileStorageLocal) { 
    this.fileStorageLocal = fileStorageLocal; 
}
  • En un método iniciador o un setter.
FileStorage fileStorageLocal; 

@Inject 
public void setFileStorage(@FileStorageLocalQualifier FileStorage fileStorageLocal) { 
    this.fileStorageLocal = fileStorageLocal; 
}

@Named

La forma más cómoda de identificar una clase consiste en asignarle un nombre único con la anotación @Named de la API Jakarta Dependency Injection. Si no se indica este nombre, se usará el de la clase.

Vamos a comprobarlo con una nueva interfaz que ofrece, por ejemplo, acceso a servicios de consulta de información sobre películas.

package com.danielme.jakartaee.cdi.movie;

public interface MovieProvider {
}

Creamos dos implementaciones con el calificador @Named.

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

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

@ApplicationScoped
@Named
public class MovieImdbProvider implements MovieProvider {
}
package com.danielme.jakartaee.cdi.injection.movie;

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

@ApplicationScoped
@Named
public class MovieTMDbProvider implements MovieProvider {
}

Repetimos la praxis de la sección anterior y usamos el calificador en el punto de inyección. Si se trata de un atributo y no indicamos el nombre de la implementación, se usará el del propio atributo. Comprobémoslo con una prueba.

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

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 static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(ArquillianExtension.class)
class MovieInjectionArquillianTest {

    @Inject
    @Named
    private MovieProvider movieImdbProvider;

    @Inject
    @Named
    private MovieProvider movieTMDbProvider;

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

    @Test
    void testProviderIsImdb() {
        assertThat(movieImdbProvider).isInstanceOf(MovieImdbProvider.class);
    }

    @Test
    void testProviderIsTMDb() {
        assertThat(movieTMDbProvider).isInstanceOf(MovieTMDbProvider.class);
    }

}

Esta convención solo funciona si hacemos la inyección en un atributo.

 WELD-001427: Only field injection points can use the @Named qualifier with no value

En un método, es obligatorio proveer el nombre del siguiente modo.

private MovieProvider movieImdbProvider;

@Inject
public MovieService(@Named("movieImdbProvider") MovieProvider movieImdbProvider) {
    this.movieImdbProvider= movieImdbProvider;
}

En cualquier caso, lo más fiable es establecer siempre un nombre tanto al definir la clase como la inyección. De este modo, no encontraremos problemas si se modifica el nombre de la clase o el atributo, y/o la persona que lee nuestro código no es consciente de esta convención. Con este cambio, el ejemplo queda así.

cdi @Named
@ApplicationScoped
@Named("MovieImdbProvider")
public class MovieImdbProvider implements MovieProvider {
}
@ApplicationScoped
@Named("MovieTMDbProvider")
public class MovieTMDbProvider implements MovieProvider {
}
@ExtendWith(ArquillianExtension.class)
class MovieInjectionArquillianTest {

    @Inject
    @Named("MovieImdbProvider")
    private MovieProvider movieImdbProvider;

    @Inject
    @Named("MovieTMDbProvider")
    private MovieProvider movieTMDbProvider;

Gracias a @Named nos ahorramos definir los calificadores personalizados, aunque es una tarea trivial. Pero es una solución menos elegante porque pasamos a trabajar con cadenas. Más adelante, veremos que la creación de calificadores tiene la ventaja adicional frente al uso de @Named de permitirnos configurar con sencillez una implementación predeterminada.

@Any

Todas las clases de CDI poseen este calificador, a excepción de las marcadas con @New, anotación que no veremos pues se consideró obsoleta (deprecated) en CDI 1.1 en favor de @Dependent. Lo usaremos para inyectar todas las implementaciones de una interfaz en un objeto Iterable de tipo Instance. Sirva la siguiente prueba como ejemplo.

@Any
@Inject
Instance<FileStorage> storageImplementations;

@Test
void testAnyInjectAll() {
    assertThatCode(() -> storageImplementations.select(FileStorageLocal.class).get())
                        .doesNotThrowAnyException();
    assertThatCode(() -> storageImplementations.select(FileStorageRemote.class).get())
                        .doesNotThrowAnyException();
}

@Default y @Alternative

@Default es un calificador asignado automáticamente a las clases que no tengan uno distinto de @Named. Debido a su nombre, cabe esperar que si hay varias implementaciones, por omisión se inyectará la anotada con @Default. Pero si no recurrimos a los calificadores personalizados e identificamos las clases con @Named, o no las identificamos en absoluto, esto no nos va a servir porque tendremos más de una implementación de tipo @Default.

Es un poco enrevesado, pero con un ejemplo queda más claro. Creemos una interfaz recurriendo al típico y socorrido ejemplo de formas geométricas.

public interface Shape {
}
public class Circle implements Shape {
}
public class Rectangle implements Shape {
}

En la siguiente clase queremos probar que Circle se inyecta por omisión, es decir, sin indicar de forma explícita un calificador.

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

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 ShapeDefaultInjectionArquillianTest {

    @Inject
    Shape shape;

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

    @Test
    void testShapeIsCirle() {
        assertThat(shape).isInstanceOf(Circle.class);
    }

}

Hemos vuelto al problema inicial, y sin ejecutar la prueba ya deberíamos prever que la inyección de Shape es ambigua porque el contenedor de CDI no sabe si usar Circle o Rectangle. Pero ahora conocemos que en realidad las clases están anotadas de esta manera.

@Default
@Any
public class Rectangle implements Shape {
}
@Default
@Any
public class Circle implements Shape {
}

 Y si usamos @Named en una de ellas, tendremos esto.

@Named
@Default
@Any
public class Rectangle implements Shape {
}
@Default
@Any
public class Circle implements Shape {
}

Así pues, en ambos escenarios hay dos clases @Default. Si queremos definir una implementación predeterminada cuando tengamos más de una de tipo @Default, debemos marcar con @Alternative todas las demás. En nuestro caso, es la clase Rectangle.

@Alternative
public class Rectangle implements Shape {
}
cdi @Alternative

Ahora en Shape recibimos Circle y la prueba es válida. ¿Y qué pasa con Rectangle? Para CDI, no existe y no podemos inyectarla, ni siquiera usando @Any. La única forma de utilizarla es definirla como la implementación por omisión en beans.xml en detrimento de Circle, la cual pasará a ser “invisible”.

<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.injection.shapes.Rectangle</class>
</alternatives>

</beans>

Con el fichero anterior (beans-alternatives.xml), incluido en el artefacto construido por el método Deployments.shapesAlternative(), esta prueba es exitosa.

@ExtendWith(ArquillianExtension.class)
class ShapeIAlternativeInjectionArquillianTest {

    @Inject
    Shape shape;

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

    @Test
    void testShapeIsRectangle() {
        assertThat(shape).isInstanceOf(Rectangle.class); 
    }

}

Este comportamiento de las implementaciones alternativas no parece útil a primera vista porque solo podemos utilizar una de ellas y las demás permanecen “inactivas”. Sin embargo, nos permitirá crear implementaciones que usaremos en los tests de integración si necesitamos sustituir las clases “reales” de la aplicación por un objeto de tipo mock. La prueba anterior es un buen ejemplo de ello.

Otro caso en el que las alternativas resultan prácticas lo encontramos cuando las implementaciones dependen del entorno de ejecución, y en función del mismo queremos usar unas clases u otras. Por ejemplo, supongamos que nuestra aplicación tendrá una instalación propia para cada cliente. En la mayoría de ellas, el FileStorage predeterminado será el local, pero en unas pocas debe ser el remoto. Podemos personalizar la instalación con el fichero de configuración beans.xml sin tener que contemplar esta casuística en el código (*) de tal modo que cada instalación use la implementación deseada.

(*) Si esto último no supone un problema, una alternativa a esta estrategia consiste en decidir qué implementación utilizar con un método “productor”, técnica que presentaré en el próximo capítulo.

@Default sí funciona de forma intuitiva cuando trabajamos con calificadores personalizados. Basta con dejar la implementación que queramos por omisión sin ningún calificador para que sea la única que recibe @Default. Veámoslo creando una nueva clase de tipo FileStorage con el calificador @Default, el cual no ponemos porque está implícito. El resto de implementaciones, recordemos, están anotadas con @FileStorageLocalQualifier y @FileStorageRemoteQualifier.

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

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class FileStorageTemp implements FileStorage {
}

Ahora tenemos tres candidatas a ser inyectadas donde solicitemos un objeto FileStorage.

cdi @Default

En FileStorageInjectionArquillianTest comprobamos con un nuevo test que, efectivamente, FileStorageTemp se inyecta cuando no se emplea ningún calificador en el punto de inyección porque es la implementación predeterminada.

@Inject
private FileStorage fileStorageDefault;

@Test
void testFileStorageDefault() {
    assertThat(fileStorageDefault).isInstanceOf(FileStorageTemp.class);
}

Hemos conseguido tener una implementación por omisión sin perder la posibilidad de inyectar cualquiera de las otras, las cuales se siguen utilizando en las demás pruebas de FileStorageInjectionArquillianTest. Las cuatro inyecciones posibles de FileStorage en esa clase quedan así.

@Inject
@FileStorageRemoteQualifier
private FileStorage fileStorageRemote;

@Inject
@FileStorageLocalQualifier
private FileStorage fileStorageLocal;

@Inject
private FileStorage fileStorageDefault;

@Any
@Inject
Instance<FileStorage> storageImplementations;

@Alternative no solo es aplicable a clases sin calificadores, sino que puede combinarse tanto con los calificadores @Named como con los personalizados. Nos permite tener varias clases con el mismo calificador personalizado o nombre dentro de CDI si todas ellas están anotadas con @Alternative excepto, claro está, la que queremos dejar como “activa” para que se inyecte.

Entre nuestras clases de ejemplo tenemos esta.

@ApplicationScoped
@Named("MovieTMDbProvider")
public class MovieTMDbProvider implements MovieProvider {
}

Hagamos otro MovieProvider, con el mismo nombre pero marcado como alternativa.

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

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Named;

@ApplicationScoped
@Named("MovieTMDbProvider")
@Alternative
public class MovieTMDbV2Provider implements MovieProvider {
}

El test correspondiente se sigue ejecutando con éxito a pesar de que existen dos MovieProvider con la denominación MovieTMDbProvider: Se inyecta el único que no es alternativo y que es el que espera recibir la prueba.

@Inject
@Named("MovieTMDbProvider")
private MovieProvider movieTMDbProvider;

@Test
void testProviderIsTMDb() {
    assertThat(movieTMDbProvider).isInstanceOf(MovieTMDbProvider.class);
}

Este diagrama ilustra el nuevo escenario. Hay tres clases y dos opciones de inyección para MovieProvider.

cdi @Alternative and @Named

Para inyectar la implementación alternativa (MovieTMDbV2Provider) ya sabemos lo que hay que hacer: definirla en el fichero beans.xml a usar en la prueba (src/test/resources/beans-movie-alternatives.xml) para que reemplace a la implementación predeterminada.

<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.movie.MovieTMDbV2Provider</class>
    </alternatives>

</beans>

Aplicando el fichero anterior en una nueva clase de pruebas, el siguiente test es válido.

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

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 static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(ArquillianExtension.class)
class MovieAlternativeInjectionArquillianTest {

    @Inject
    @Named("MovieTMDbProvider")
    private MovieProvider movieTMDbProvider;

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

    @Test
    void testProviderTMDbIsAlternative() {
        assertThat(movieTMDbProvider)
              .isInstanceOf(MovieTMDbV2Provider.class);
    }

}

Reutilizando calificadores personalizados

Escribir un calificador personalizado es pan comido, pero en sistemas grandes corremos el riesgo de crear una miríada de anotaciones. CDI permite usar un mismo calificador personalizado para identificar a clases distintas mediante la parametrización de la anotación. De hecho, este mecanismo es el que permite la existencia de @Named. Revisemos su declaración.

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {

    /** The name.
    * @return the name.
    */
    String value() default "";
}

No es la primera vez en el curso que creamos una anotación, pero este código presenta algo novedoso: tenemos un miembro de tipo cadena llamado value. Los campos de una anotación, de tipos primitivos o arrays, se definen como si fueran métodos -su valor se obtendrá invocando a los mismos- y darles valor es opcional si asignamos valores predeterminados -no pueden ser null- con default. El nombre value es especial: no hace falta indicarlo dentro de la anotación siempre y cuando sea el único parámetro al que le damos un valor. Por eso, las siguientes declaraciones son equivalentes.

@Named(value = "MovieImdbProvider")
@Named("MovieImdbProvider")

Las clases calificadas con @Named comparten, obviamente, el mismo calificador, pero se pueden distinguir unas de otras gracias al atributo value. La anotación es tan genérica porque value es de tipo cadena y puede ser cualquier cosa.

Probemos esta característica de CDI con un nuevo ejemplo basado en la interfaz “línea de comandos”.

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

public interface CommandLine {
}

En esta ocasión, contaremos con un único calificador para todas las implementaciones.

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

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 CommandLineQualifier {
    CommandLineSupported value();
}

En cierto modo, hemos escrito nuestro propio @Named, pero con una diferencia capital: cada clase se identifica por un valor del enumerado CommandLineSupported, lo que hace a nuestro código menos flexible pero más seguro. Vamos a contemplar un par de tipos de líneas de comandos.

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

public enum CommandLineSupported {
    POWERSHELL, BASH
}

Por tanto, y sin tener en cuenta el uso de alternativas, solo podemos identificar dos implementaciones de CommandLine con @CommandLineQualifier.

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

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
@CommandLineQualifier(CommandLineSupported.POWERSHELL)
public class PowershellCommandLine implements CommandLine {
}
package com.danielme.jakartaee.cdi.injection.commandline;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
@CommandLineQualifier(CommandLineSupported.BASH)
public class BashCommandLine implements CommandLine {
}

De forma esquemática, este diagrama muestra las posibilidades de inyección para CommandLine.

Una vez más, escribamos pruebas, equivalentes a las que vimos para FileStorage. No tienen ningún misterio; lo más relevante es la forma de inyectar CommandLine.

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

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 CommandLineInjectionArquillianTest {

    @Inject
    @CommandLineQualifier(CommandLineSupported.BASH)
    private CommandLine bash;

    @Inject
    @CommandLineQualifier(CommandLineSupported.POWERSHELL)
    private CommandLine powershell;

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

    @Test
    void testCommandLineIsBash() {
        assertThat(bash).isInstanceOf(BashCommandLine.class);
    }

    @Test
    void testCommandLineIsPowershell() {
        assertThat(powershell).isInstanceOf(PowershellCommandLine.class);
    }

}

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 .