JavaRush /Blog Java /Random-FR /Une courte excursion dans l'injection de dépendances ou "...
Viacheslav
Niveau 3

Une courte excursion dans l'injection de dépendances ou "Qu'est-ce que le CDI ?"

Publié dans le groupe Random-FR
La base sur laquelle reposent désormais les frameworks les plus populaires est l’injection de dépendances. Je suggère de regarder ce que dit la spécification CDI à ce sujet, quelles sont les capacités de base dont nous disposons et comment nous pouvons les utiliser.
Une brève excursion dans l’injection de dépendance ou

Introduction

Je voudrais consacrer cette brève revue à une chose telle que le CDI. Qu'est-ce que c'est? CDI signifie Contextes et injection de dépendances. Il s'agit d'une spécification Java EE qui décrit l'injection de dépendances et les contextes. Pour plus d'informations, vous pouvez consulter le site http://cdi-spec.org . Puisque CDI est une spécification (une description de son fonctionnement, un ensemble d'interfaces), nous aurons également besoin d'une implémentation pour l'utiliser. L'une de ces implémentations est Weld - http://weld.cdi-spec.org/ Pour gérer les dépendances et créer un projet, nous utiliserons Maven - https://maven.apache.org Donc, nous avons Maven installé, maintenant nous Je le comprendrai en pratique, pour ne pas comprendre le résumé. Pour ce faire, nous allons créer un projet utilisant Maven. Ouvrons la ligne de commande (sous Windows, vous pouvez utiliser Win+R pour ouvrir la fenêtre "Exécuter" et exécuter cmd) et demandons à Maven de tout faire pour nous. Pour cela, Maven dispose d'un concept appelé archétype : Maven Archetype .
Une brève excursion dans l’injection de dépendance ou
Après cela, aux questions « Choisissez un numéro ou appliquez un filtre » et « Choisissez org.apache.maven.archetypes:maven-archetype-quickstart version », appuyez simplement sur Entrée. Ensuite, saisissez les identifiants du projet, appelés GAV (voir Naming Convention Guide ).
Une brève excursion dans l’injection de dépendance ou
Après la création réussie du projet, nous verrons l'inscription « BUILD SUCCESS ». Nous pouvons maintenant ouvrir notre projet dans notre IDE préféré.

Ajouter du CDI à un projet

En introduction, nous avons vu que le CDI dispose d'un site Internet intéressant - http://www.cdi-spec.org/ . Il existe une section de téléchargement, qui contient un tableau contenant les données dont nous avons besoin :
Une brève excursion dans l’injection de dépendance ou
Ici, nous pouvons voir comment Maven décrit le fait que nous utilisons l'API CDI dans le projet. L'API est une interface de programmation d'application, c'est-à-dire une interface de programmation. Nous travaillons avec l'interface sans nous soucier de quoi et comment cela fonctionne derrière cette interface. L'API est une archive jar que nous allons commencer à utiliser dans notre projet, c'est-à-dire que notre projet commence à dépendre de ce jar. Par conséquent, l'API CDI de notre projet est une dépendance. Dans Maven, un projet est décrit dans des fichiers POM.xml ( POM - Project Object Model ). Les dépendances sont décrites dans le bloc dépendances, auquel nous devons ajouter une nouvelle entrée :
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
Comme vous l'avez peut-être remarqué, nous ne spécifions pas la portée avec la valeur fournie. Pourquoi y a-t-il une telle différence ? Cette portée signifie que quelqu'un nous fournira la dépendance. Lorsqu'une application s'exécute sur un serveur Java EE, cela signifie que le serveur fournira à l'application toutes les technologies JEE nécessaires. Par souci de simplicité de cette revue, nous travaillerons dans un environnement Java SE, donc personne ne nous fournira cette dépendance. Vous pouvez en savoir plus sur Dependency Scope ici : " Dependency Scope ". Bon, nous avons désormais la possibilité de travailler avec des interfaces. Mais nous avons également besoin de mise en œuvre. Comme nous nous en souvenons, nous utiliserons Weld. Il est intéressant de noter que différentes dépendances sont données partout. Mais nous suivrons la documentation. Par conséquent, lisons " 18.4.5. Définition du chemin de classe " et faisons ce qui est dit :
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
Il est important que les versions de troisième ligne de Weld prennent en charge CDI 2.0. On peut donc compter sur l’API de cette version. Nous sommes maintenant prêts à écrire du code.
Une brève excursion dans l’injection de dépendance ou

Initialisation d'un conteneur CDI

Le CDI est un mécanisme. Quelqu'un doit contrôler ce mécanisme. Comme nous l'avons déjà lu ci-dessus, un tel gestionnaire est un conteneur. Par conséquent, nous devons le créer, il n'apparaîtra pas lui-même dans l'environnement SE. Ajoutons ce qui suit à notre méthode principale :
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
Nous avons créé le conteneur CDI manuellement car... Nous travaillons dans un environnement SE. Dans les projets de combat typiques, le code s'exécute sur un serveur, qui fournit diverses technologies au code. Par conséquent, si le serveur fournit du CDI, cela signifie que le serveur dispose déjà d'un conteneur CDI et que nous n'aurons rien à ajouter. Mais pour les besoins de ce didacticiel, nous prendrons l'environnement SE. De plus, le conteneur est là, de manière claire et compréhensible. Pourquoi avons-nous besoin d’un conteneur ? Le récipient à l'intérieur contient des haricots (haricots CDI).
Une brève excursion dans l’injection de dépendance ou

Haricots CDI

Donc des haricots. Qu'est-ce qu'un bac CDI ? Il s'agit d'une classe Java qui suit certaines règles. Ces règles sont décrites dans la spécification, au chapitre « 2.2. Quels types de classes sont les beans ? ». Ajoutons un bean CDI au même package que la classe App :
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
Nous pouvons maintenant appeler ce bean à partir de notre mainméthode :
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
Comme vous pouvez le constater, nous n'avons pas créé le bean à l'aide du mot-clé new. Nous avons demandé au conteneur CDI : "Conteneur CDI. J'ai vraiment besoin d'une instance de la classe Logger, donnez-la-moi s'il vous plaît." Cette méthode est appelée « Recherche de dépendances », c'est-à-dire recherche de dépendances. Créons maintenant une nouvelle classe :
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
Une classe primitive qui renvoie une représentation textuelle d'une date. Ajoutons maintenant la sortie date au message :
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
Une annotation @Inject intéressante est apparue. Comme indiqué dans le chapitre " 4.1. Points d'injection " de la documentation cdi soudure, à l'aide de cette annotation nous définissons le point d'injection. En russe, cela peut être lu comme « points de mise en œuvre ». Ils sont utilisés par le conteneur CDI pour injecter des dépendances lors de l'instanciation des beans. Comme vous pouvez le voir, nous n'attribuons aucune valeur au champ dateSource. La raison en est que le conteneur CDI permet à l'intérieur des beans CDI (uniquement les beans qu'il a lui-même instanciés, c'est-à-dire qu'il gère) d'utiliser « Dependency Injection ». Il s'agit d'une autre manière d' inversion de contrôle , une approche dans laquelle la dépendance est contrôlée par quelqu'un d'autre plutôt que par la création explicite des objets. L'injection de dépendances peut être effectuée via une méthode, un constructeur ou un champ. Pour plus de détails, voir le chapitre de spécification CDI " 5.5. Injection de dépendances ". La procédure permettant de déterminer ce qui doit être implémenté est appelée résolution typesafe, c'est ce dont nous devons parler.
Une brève excursion dans l’injection de dépendance ou

Résolution de nom ou résolution Typesafe

Généralement, une interface est utilisée comme type d'objet à implémenter, et le conteneur CDI lui-même détermine quelle implémentation choisir. Ceci est utile pour de nombreuses raisons, dont nous discuterons. Nous avons donc une interface de logger :
public interface Logger {
    void print(String message);
}
Il dit que si nous avons un enregistreur, nous pouvons lui envoyer un message et il terminera sa tâche : se connecter. Comment et où ne seront pas intéressants dans ce cas. Créons maintenant une implémentation pour le logger :
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
Comme vous pouvez le voir, il s'agit d'un enregistreur qui écrit sur System.out. Merveilleux. Désormais, notre méthode principale fonctionnera comme avant. Logger logger = container.select(Logger.class).get(); Cette ligne sera toujours reçue par l'enregistreur. Et la beauté est que nous avons seulement besoin de connaître l'interface, et le conteneur CDI pense déjà à l'implémentation pour nous. Disons que nous avons une deuxième implémentation qui devrait envoyer le journal quelque part vers un stockage distant :
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
Si nous exécutons maintenant notre code sans modifications, nous obtiendrons une erreur, car Le conteneur CDI voit deux implémentations de l'interface et ne peut pas choisir entre elles : org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger Que faire ? Il existe plusieurs variantes disponibles. La plus simple est l' annotation @Vetoed pour un bean CDI afin que le conteneur CDI ne perçoive pas cette classe comme un bean CDI. Mais il existe une approche bien plus intéressante. Un bean CDI peut être marqué comme "alternative" en utilisant l'annotation @Alternativedécrite dans le chapitre " 4.7. Alternatives " de la documentation Weld CDI. Qu'est-ce que ça veut dire? Cela signifie qu'à moins que nous disons explicitement de l'utiliser, il ne sera pas sélectionné. Il s'agit d'une version alternative du bean. Marquons le bean NetworkLogger comme @Alternative et nous pouvons voir que le code est à nouveau exécuté et utilisé par SystemOutLogger. Pour activer l'alternative, nous devons avoir un fichier beans.xml . La question peut se poser : « beans.xml, où dois-je vous mettre ? » Par conséquent, plaçons le fichier correctement :
Une brève excursion dans l’injection de dépendance ou
Dès que nous aurons ce fichier, l'artefact avec notre code sera appelé « Archive de bean explicite ». Nous avons maintenant 2 configurations distinctes : logiciel et XML. Le problème est qu'ils chargeront les mêmes données. Par exemple, la définition du bean DataSource sera chargée 2 fois et notre programme plantera lors de son exécution, car Le conteneur CDI les considérera comme 2 beans distincts (bien qu'il s'agisse en fait de la même classe, ce que le conteneur CDI a appris deux fois). Pour éviter cela, il existe 2 options :
  • supprimez la ligne initializer.addPackages(App.class.getPackage())et ajoutez une indication de l'alternative au fichier 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>
  • ajoutez un attribut bean-discovery-modeavec la valeur " none " à l'élément racine du beans et spécifiez une alternative par programme :
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
Ainsi, en utilisant l'alternative CDI, le conteneur peut déterminer quel bean sélectionner. Fait intéressant, si le conteneur CDI connaît plusieurs alternatives pour la même interface, alors nous pouvons le savoir en indiquant la priorité à l'aide d'une annotation @Priority(depuis CDI 1.1).
Une brève excursion dans l’injection de dépendance ou

Qualifications

Séparément, il convient de discuter de choses telles que les qualificatifs. Le qualificatif est indiqué par une annotation au-dessus du bean et affine la recherche du bean. Et maintenant plus de détails. Fait intéressant, tout bean CDI a dans tous les cas au moins un qualificatif - @Any. Si nous ne spécifions AUCUN qualificatif au-dessus du bean, mais que le conteneur CDI lui-même ajoute @Anyun autre qualificatif au qualificatif - @Default. Si nous spécifions quelque chose (par exemple, spécifions explicitement @Any), alors le qualificatif @Default ne sera pas automatiquement ajouté. Mais la beauté des qualificatifs est que vous pouvez créer vos propres qualificatifs. Le qualificatif n'est presque pas différent des annotations, car en substance, il s'agit simplement d'une annotation écrite d'une manière spéciale. Par exemple, vous pouvez saisir Enum pour le type de protocole :
public enum ProtocolType {
    HTTP, HTTPS
}
Ensuite nous pouvons faire un qualificatif qui prendra en compte ce type :
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
Il convient de noter que les champs marqués comme @Nonbindingn'affectent pas la détermination du qualificatif. Vous devez maintenant spécifier le qualificatif. Il est indiqué au dessus du type du bean (pour que CDI sache le définir) et au dessus du Point d'Injection (avec l'annotation @Inject pour que vous compreniez quel bean chercher pour injection à cet endroit). Par exemple, nous pouvons ajouter une classe avec un qualificatif. Pour plus de simplicité, pour cet article, nous les ferons dans le 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");
	}
}
Et puis, lorsque nous effectuerons Inject, nous spécifierons un qualificatif qui influencera la classe qui sera utilisée :
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
Génial, n'est-ce pas ?) Cela semble beau, mais on ne sait pas pourquoi. Imaginez maintenant ce qui suit :
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 cette façon, nous pouvons remplacer l'obtention de la valeur afin qu'elle puisse être calculée dynamiquement. Par exemple, cela peut être extrait de certains paramètres. Ensuite, nous pouvons modifier l'implémentation même à la volée, sans recompiler ni redémarrer le programme/serveur. Cela devient beaucoup plus intéressant, n'est-ce pas ? )
Une brève excursion dans l’injection de dépendance ou

Producteurs

Une autre fonctionnalité utile du CDI concerne les producteurs. Ce sont des méthodes spéciales (elles sont marquées d'une annotation spéciale) qui sont appelées lorsqu'un bean a demandé une injection de dépendances. Plus de détails sont décrits dans la documentation, dans la section " 2.2.3. Méthodes du producteur ". L'exemple le plus simple :
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
Désormais, lors de l'injection dans des champs de type Integer, cette méthode sera appelée et une valeur en sera obtenue. Ici, nous devons immédiatement comprendre que lorsque nous voyons le mot-clé new, nous devons immédiatement comprendre qu'il ne s'agit PAS d'un bean CDI. Autrement dit, une instance de la classe Random ne deviendra pas un bean CDI simplement parce qu'elle est dérivée de quelque chose qui contrôle le conteneur CDI (dans ce cas, le producteur).
Une brève excursion dans l’injection de dépendance ou

Intercepteurs

Les intercepteurs sont des intercepteurs qui « interfèrent » dans le travail. En CDI, cela se fait assez clairement. Voyons comment effectuer la journalisation à l'aide d'interpréteurs (ou d'intercepteurs). Tout d’abord, nous devons décrire la liaison à l’intercepteur. Comme beaucoup de choses, cela se fait à l'aide d'annotations :
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
L'essentiel ici est qu'il s'agit d'une liaison pour l'intercepteur ( @InterceptorBinding), qui sera héritée par extends ( @InterceptorBinding). Écrivons maintenant l'intercepteur lui-même :
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
Vous pouvez en savoir plus sur la façon dont les intercepteurs sont écrits dans l'exemple de la spécification : " 1.3.6. Exemple d'intercepteur ". Eh bien, tout ce que nous avons à faire c'est d'allumer l'inercepteur. Pour ce faire, spécifiez l'annotation de liaison au-dessus de la méthode en cours d'exécution :
@ConsoleLog
public void print(String message) {
Et maintenant un autre détail très important. Les intercepteurs sont désactivés par défaut et doivent être activés de la même manière que les alternatives. Par exemple, dans le fichier beans.xml :
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
Comme vous pouvez le constater, c'est assez simple.
Une brève excursion dans l’injection de dépendance ou

Événement et observateurs

CDI fournit également un modèle d'événements et d'observateurs. Ici, tout n'est pas aussi évident qu'avec les intercepteurs. Ainsi, l'événement dans ce cas peut être absolument n'importe quelle classe ; rien de spécial n'est nécessaire pour la description. Par exemple:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
Maintenant, quelqu'un devrait attendre l'événement :
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
L'essentiel ici est de spécifier l'annotation @Observes, qui indique qu'il ne s'agit pas simplement d'une méthode, mais d'une méthode qui doit être appelée à la suite de l'observation d'événements de type LogEvent. Eh bien, maintenant nous avons besoin de quelqu'un qui surveillera :
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
Nous disposons d'une méthode unique qui indiquera au conteneur qu'un événement Event s'est produit pour le type d'événement LogEvent. Il ne reste plus qu'à utiliser l'observateur. Par exemple, dans NetworkLogger nous pouvons ajouter une injection de notre observateur :
@Inject
private LogObserver observer;
Et dans la méthode print, nous pouvons informer l'observateur que nous avons un nouvel événement :
public void print(String message) {
	observer.observe(new LogEvent());
Il est important de savoir que les événements peuvent être traités dans un seul thread ou dans plusieurs. Pour le traitement asynchrone, utilisez une méthode .fireAsync(au lieu de .fire) et une annotation @ObservesAsync(au lieu de @Observes). Par exemple, si tous les événements sont exécutés dans des threads différents, alors si un thread lève une exception, alors les autres pourront faire leur travail pour d'autres événements. Vous pouvez en savoir plus sur les événements en CDI, comme d'habitude, dans la spécification, au chapitre " 10. Événements ".
Une brève excursion dans l’injection de dépendance ou

Décorateurs

Comme nous l'avons vu ci-dessus, divers modèles de conception sont rassemblés sous l'aile CDI. Et en voici un autre – un décorateur. C'est une chose très intéressante. Jetons un coup d'œil à cette classe :
@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);
    }
}
En le déclarant décorateur, nous disons que lorsqu'une implémentation de Logger est utilisée, ce « module complémentaire » sera utilisé, qui connaît l'implémentation réelle, qui est stockée dans le champ délégué (puisqu'il est marqué de l'annotation @Delegate). Les décorateurs ne peuvent être associés qu'à un bean CDI, qui lui-même n'est ni un intercepteur ni un décorateur. Un exemple peut également être vu dans le cahier des charges : " 1.3.7. Exemple de décorateur ". Le décorateur, comme l'intercepteur, doit être allumé. Par exemple, dans beans.xml :
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
Pour plus de détails, voir référence soudure : « Chapitre 10. Décorateurs ».

Cycle de vie

Les haricots ont leur propre cycle de vie. Cela ressemble à ceci :
Une brève excursion dans l’injection de dépendance ou
Comme vous pouvez le voir sur l'image, nous avons ce que l'on appelle des rappels de cycle de vie. Ce sont des annotations qui indiqueront au conteneur CDI d'appeler certaines méthodes à un certain stade du cycle de vie du bean. Par exemple:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
Cette méthode sera appelée lorsqu'un bean CDI est instancié par un conteneur. La même chose se produira avec @PreDestroy lorsque le bean sera détruit alors qu'il n'est plus nécessaire. Ce n’est pas pour rien que l’acronyme CDI contient la lettre C – Contexte. Les beans en CDI sont contextuels, ce qui signifie que leur cycle de vie dépend du contexte dans lequel ils existent au sein du conteneur CDI. Pour mieux comprendre cela, vous devriez lire la section de spécification « 7. Cycle de vie des instances contextuelles ». Il convient également de savoir que le conteneur lui-même a un cycle de vie, que vous pouvez lire dans « Événements du cycle de vie du conteneur ».
Une brève excursion dans l’injection de dépendance ou

Total

Ci-dessus, nous avons examiné la pointe de l’iceberg appelée CDI. CDI fait partie de la spécification JEE et est utilisé dans l'environnement JavaEE. Ceux qui utilisent Spring n'utilisent pas CDI, mais DI, c'est-à-dire que ce sont des spécifications légèrement différentes. Mais connaissant et comprenant ce qui précède, vous pouvez facilement changer d’avis. Considérant que Spring prend en charge les annotations du monde CDI (le même Inject). Matériaux additionnels: #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION