Une introduction à CDI (Injection de contextes et de dépendances) en Java

1. Vue d’ensemble

CDI (Contexts and Dependency Injection) est un cadre standard dependency injection inclus dans Java EE 6 et versions ultérieures.

  • Il nous permet de gérer le cycle de vie des composants avec état via des contextes de cycle de vie spécifiques à un domaine et d’injecter des composants (services) dans des objets client de manière sécurisée.

Dans ce didacticiel, nous allons examiner en détail les fonctionnalités les plus pertinentes de CDI et mettre en œuvre différentes approches pour l’injection de dépendances dans des classes client.

2. DYDI (injection de dépendance à faire soi-même)

En résumé, il est possible de mettre en œuvre l’ID sans avoir recours à aucun cadre.

Cette approche est communément appelée DYDI (injection de dépendance à faire soi-même).

Avec DYDI, nous gardons le code d’application isolé de la création d’objet en transmettant les dépendances requises aux classes clientes par le biais d’anciennes usines/générateurs.

Voici à quoi une implémentation DYDI de base pourrait ressembler:

public interface TextService {
    String doSomethingWithText(String text);
    String doSomethingElseWithText(String text);
}
public class SpecializedTextService implements TextService { ... }
public class TextClass {
    private TextService textService;

   //constructor
}
public class TextClassFactory {

    public TextClass getTextClass() {
        return new TextClass(new SpecializedTextService();
    }
}

Bien entendu, DYDI convient à certains cas d’utilisation relativement simples.

Si notre exemple d’application devenait de plus en plus complexe et de plus en plus complexe en mettant en place un réseau d’objets interconnectés, nous finirions par le polluer avec des tonnes d’usines de graphes d’objets.

Cela nécessiterait beaucoup de code standard pour la création de graphiques d’objets. Ce n’est pas une solution entièrement évolutive.

Pouvons-nous faire plus de DI? Bien sûr on peut. C’est exactement là que CDI entre en scène.

3. Un exemple simple

  • CDI transforme DI en un processus simple, consistant simplement à décorer les classes de services avec quelques annotations simples et à définir les points d’injection correspondants dans les classes de clients. **

Pour montrer comment CDI implémente DI au niveau le plus élémentaire, supposons que nous voulions développer une simple application d’édition de fichier image. Capable d’ouvrir, éditer, écrire, sauvegarder un fichier image et ainsi de suite.

3.1. Le «beans.xml» Fichier

Premièrement, nous devons placer un fichier “beans.xml” dans le dossier “src/main/resources/META-INF/” . Même si ce fichier ne contient aucune directive DI spécifique, il est nécessaire pour que CDI soit opérationnel et opérationnel :

<beans xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
  http://java.sun.com/xml/ns/javaee/beans__1__0.xsd">
</beans>

3.2. Les classes de service

Créons ensuite les classes de service qui effectuent les opérations mentionnées ci-dessus sur les fichiers GIF, JPG et PNG:

public interface ImageFileEditor {
    String openFile(String fileName);
    String editFile(String fileName);
    String writeFile(String fileName);
    String saveFile(String fileName);
}
public class GifFileEditor implements ImageFileEditor {

    @Override
    public String openFile(String fileName) {
        return "Opening GIF file " + fileName;
    }

    @Override
    public String editFile(String fileName) {
      return "Editing GIF file " + fileName;
    }

    @Override
    public String writeFile(String fileName) {
        return "Writing GIF file " + fileName;
    }

    @Override
    public String saveFile(String fileName) {
        return "Saving GIF file " + fileName;
    }
}
public class JpgFileEditor implements ImageFileEditor {
   //JPG-specific implementations for openFile()/editFile()/writeFile()/saveFile()
    ...
}
public class PngFileEditor implements ImageFileEditor {
   //PNG-specific implementations for openFile()/editFile()/writeFile()/saveFile()
    ...
}

3.3. La classe de clients

Enfin, implémentons une classe client qui prend une implémentation ImageFileEditor dans le constructeur et définissons un point d’injection avec l’annotation @ Inject :

public class ImageFileProcessor {

    private ImageFileEditor imageFileEditor;

    @Inject
    public ImageFileProcessor(ImageFileEditor imageFileEditor) {
        this.imageFileEditor = imageFileEditor;
    }
}

En termes simples, l’annotation @ Inject est le véritable bourreau de travail de CDI. Cela nous permet de définir des points d’injection dans les classes de clients.

Dans ce cas, @ Inject demande à CDI d’injecter une implémentation ImageFileEditor dans le constructeur.

De plus, il est également possible d’injecter un service en utilisant l’annotation @ Inject dans les champs (injection de champ) et les traceurs (injection de setter). Nous examinerons ces options plus tard.

3.4. Construction du graphe d’objet ImageFileProcessor avec soudure

Bien entendu, nous devons nous assurer que CDI injectera la bonne implémentation ImageFileEditor dans le constructeur de la classe ImageFileProcessor .

Pour ce faire, nous devrions d’abord obtenir une instance de la classe.

  • Comme nous n’utilisons pas de serveur d’application Java EE pour utiliser CDI, nous le ferons avec Weld , l’implémentation de référence CDI dans Java SE ** :

public static void main(String[]args) {
    Weld weld = new Weld();
    WeldContainer container = weld.initialize();
    ImageFileProcessor imageFileProcessor = container.select(ImageFileProcessor.class).get();

    System.out.println(imageFileProcessor.openFile("file1.png"));

    container.shutdown();
}

Nous créons ici un objet WeldContainer , puis obtenons un objet ImageFileProcessor et appelons enfin sa méthode openFile () .

Comme on pouvait s’y attendre, si nous exécutons l’application, CDI se plaindre très fort en générant une exception DeploymentException:

Unsatisfied dependencies for type ImageFileEditor with qualifiers @Default at injection point...
  • Nous obtenons cette exception car CDI ne sait pas quelle implémentation de ImageFileEditor à injecter dans le constructeur ImageFileProcessor . **

Dans la terminologie de CDI , il s’agit d’une exception d’injection ambiguë .

3.5. Les annotations @ Default et @ Alternative

Il est facile de résoudre cette ambiguïté. CDI, par défaut, annote toutes les implémentations d’une interface avec l’annotation @ Default .

Nous devrions donc explicitement indiquer quelle implémentation doit être injectée dans la classe client:

@Alternative
public class GifFileEditor implements ImageFileEditor { ... }
@Alternative
public class JpgFileEditor implements ImageFileEditor { ... }
public class PngFileEditor implements ImageFileEditor { ... }

Dans ce cas, nous avons annoté GifFileEditor et JpgFileEditor avec l’annotation @ Alternative , de sorte que CDI sait maintenant que PngFileEditor (annoté par défaut avec l’annotation @ Default ) est l’implémentation que nous voulons injecter.

Si nous réexécutons l’application, cette fois-ci elle sera exécutée comme prévu:

Opening PNG file file1.png

En outre, l’annotation de PngFileEditor avec l’annotation @ Default et le maintien des autres implémentations en tant qu’alternatives produiront le même résultat ci-dessus.

Ceci montre, en quelques mots, comment nous pouvons très facilement échanger l’injection d’exécution d’implémentations en commutant simplement les annotations @ Alternative dans les classes de service .

4. Injection de champ

CDI prend en charge l’injection de terrain et de setter.

Voici comment procéder à l’injection de champ ( les règles de qualification des services avec les annotations @ Default et @ Alternative restent les mêmes ):

@Inject
private final ImageFileEditor imageFileEditor;

5. Injection de poseur

De même, voici comment procéder à l’injection de setter:

@Inject
public void setImageFileEditor(ImageFileEditor imageFileEditor) { ... }

6. L’annotation @ Named

Jusqu’à présent, nous avons appris à définir les points d’injection dans les classes client et à injecter des services avec les annotations @ Inject , @ Default et @ Alternative , qui couvrent la plupart des cas d’utilisation.

Néanmoins, CDI nous permet également d’effectuer une injection de service avec l’annotation @ Named .

  • Cette méthode fournit un moyen plus sémantique d’injecter des services, en liant un nom significatif à une implémentation:

@Named("GifFileEditor")
public class GifFileEditor implements ImageFileEditor { ... }

@Named("JpgFileEditor")
public class JpgFileEditor implements ImageFileEditor { ... }

@Named("PngFileEditor")
public class PngFileEditor implements ImageFileEditor { ... }

Maintenant, nous devrions refactoriser le point d’injection dans la classe ImageFileProcessor pour qu’il corresponde à une implémentation nommée:

@Inject
public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

Il est également possible d’effectuer une injection de champ et de setter avec des implémentations nommées, ce qui ressemble beaucoup à l’utilisation des annotations @ Default et @ Alternative :

@Inject
private final @Named("PngFileEditor") ImageFileEditor imageFileEditor;

@Inject
public void setImageFileEditor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

7. L’annotation @ Produces

Parfois, un service nécessite une configuration complète avant d’être injecté pour gérer des dépendances supplémentaires.

CDI fournit une assistance dans ces situations via l’annotation @ Produces .

  • @ Produces nous permet d’implémenter des classes fabriques dont la responsabilité est la création de services entièrement initialisés.

Pour comprendre le fonctionnement de l’annotation @ Produces , modifions la classe ImageFileProcessor afin qu’elle puisse utiliser un service supplémentaire TimeLogger dans le constructeur.

Le service sera utilisé pour enregistrer l’heure à laquelle une certaine opération de fichier image est effectuée:

@Inject
public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

public String openFile(String fileName) {
    return imageFileEditor.openFile(fileName) + " at: " + timeLogger.getTime();
}
    //additional image file methods

Dans ce cas, la classe TimeLogger prend deux services supplémentaires, SimpleDateFormat et Calendar :

public class TimeLogger {

    private SimpleDateFormat dateFormat;
    private Calendar calendar;

   //constructors

    public String getTime() {
        return dateFormat.format(calendar.getTime());
    }
}

Comment pouvons-nous dire à CDI où chercher pour obtenir un objet TimeLogger complètement initialisé?

Nous venons de créer une classe de fabrique TimeLogger et d’annoter sa méthode de fabrique avec l’annotation @ Produces :

public class TimeLoggerFactory {

    @Produces
    public TimeLogger getTimeLogger() {
        return new TimeLogger(new SimpleDateFormat("HH:mm"), Calendar.getInstance());
    }
}

Chaque fois que nous obtenons une instance ImageFileProcessor , CDI analyse la classe TimeLoggerFactory , puis appelle la méthode getTimeLogger () (comme elle est annotée avec l’annotation @ Produces ), puis injecte le service Time Logger .

Si nous exécutons l’exemple d’application refactorisé avec Weld , les résultats suivants seront générés:

Opening PNG file file1.png at: 17:46

8. Qualificateurs personnalisés

CDI prend en charge l’utilisation de qualificateurs personnalisés pour qualifier les dépendances et résoudre les points d’injection ambigus.

Les qualificatifs personnalisés sont une fonctionnalité très puissante. Non seulement elles lient un nom sémantique à un service, mais elles lient également des métadonnées d’injection. Des métadonnées telles que https://docs.oracle.com/javase/7/docs/api/java/lang/annotation/RetentionPolicy . html[RetentionPolicy]et les cibles d’annotation légales ( ElementType ).

Voyons comment utiliser les qualificateurs personnalisés dans notre application:

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

Lions maintenant les qualificatifs personnalisés aux implémentations ImageFileEditor :

@GifFileEditorQualifier
public class GifFileEditor implements ImageFileEditor { ... }
@JpgFileEditorQualifier
public class JpgFileEditor implements ImageFileEditor { ... }
@PngFileEditorQualifier
public class PngFileEditor implements ImageFileEditor { ... }

Enfin, refactorisons le point d’injection dans la classe ImageFileProcessor _: _

@Inject
public ImageFileProcessor(@PngFileEditorQualifier ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

Si nous relançons notre application, elle devrait générer le même résultat que ci-dessus.

Les qualificateurs personnalisés fournissent une approche sémantique soignée pour les noms de liaison et les métadonnées d’annotation aux implémentations.

De plus, les qualificateurs personnalisés nous permettent de définir des points d’injection de type sûr plus restrictifs (dépassant les fonctionnalités des annotations @Default et @Alternative) ** .

  • Si seul un sous-type est qualifié dans une hiérarchie de types, CDI n’injectera que le sous-type, pas le type de base. **

9. Conclusion

Incontestablement, le CDI simplifie l’injection de dépendance , le coût des annotations supplémentaires nécessite très peu d’effort pour le gain d’une injection de dépendance organisée.

Il arrive que DYDI ait toujours sa place sur CDI. Comme lors du développement d’applications assez simples qui ne contiennent que des graphiques d’objets simples.

Comme toujours, tous les exemples de code présentés dans cet article sont disponibles à l’adresse over sur GitHub .