Tutoriel sur une introduction à CDI (Context and Dependency Injection)

L'injection de dépendances est souvent la base de tout programme moderne. L'idée en résumé est de déporter la responsabilité de la liaison des composants du programme dans un framework afin de pouvoir facilement changer ces composants ou leur comportement. Parmi les leaders du marché Java, il y a Spring IoC, Guice, Dagger… ou encore le standard « Java EE » CDI qui existe depuis Java EE 6. Ce dernier s'est inspiré de plusieurs standards de facto, pour finalement devenir aujourd'hui la base de la plateforme Java EE moderne. Ce tutoriel vous propose un tour d'horizon des fonctionnalités de base de CDI.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum 5 commentaires Donner une note à l'article (4.5).

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Qu'est-ce que CDI

CDI (Context and Dependency Injection) est une spécification destinée à standardiser l'injection de dépendances et de contextes, au sein de la plateforme Java et plus particulièrement Java EE.

Intégrée à la spécification Java EE 6, sa version 1.0 est sortie en décembre 2009 et a été suivie des versions 1.1 (mai 2013) et 1.2 (avril 2014). Son élaboration a été le fruit des JSR 299 et 346.

C'est donc la version 1.2 qui est l'objet de ce tutoriel.

Le but de cette spécification est de définir les API et les comportements associés en ce qui concerne ce qu'on appelle communément l'injection de dépendances (inversion de contrôle).

Très rapidement, le but de cette dernière est d'avoir un couplage lâche entre nos classes. En d'autres termes, définir un contrat entre les beans, mais faciliter le remplacement d'un maillon de la chaîne très facilement sans avoir à faire les liens et l'initialisation soi-même.

Couplage lâche, mais typage fort. CDI utilise autant que possible la compilation Java pour les validations de base à la compilation.

II. Un exemple pour débuter

Prenons l'exemple d'une voiture. Pour simplifier, on dira qu'une voiture a un moteur et une couleur. Une voiture peut aussi rouler et freiner.

Notre classe ressemble donc par exemple à :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public class Voiture {
    private Moteur moteur;
    
    private Couleur couleur;

    // Modificateurs pour moteur/couleur.

    private float vitesseActuelle; // km/h

    public void rouler(final float vitesse) {
        vitesseActuelle = vitesse;
    }

    public void freiner() {
        freiner = vitesse * 0.8;
    }
}

Un moteur est dans notre représentation simpliste une puissance et une marque :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
public class Moteur {
    private String marque;
    
    private int puissance; // chevaux

    // Modificateurs

    public float max(float vitesse) { // si trop rapide on peut limiter la vitesse ici
        return vitesse;
    }
}

Enfin une couleur est une nuance et une marque de peinture. C'est en gros la même classe que Moteur, mais avec nuance au lieu de puissance.

Si on souhaite faire un programme pour faire rouler une voiture à 90 km/h avec un moteur de marque « X », de puissance « 100 », de couleur de marque « Y » et de nuance « 50 », alors on écrira en Java classique quelque chose comme :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
public static void main(String[] args) {
    Moteur moteur = new Moteur();
    moteur.setMarque("X");
    moteur.setPuissance(100);

    Couleur couleur = new Couleur();
    couleur.setMarque("Y");
    couleur.setNuance(50);

    Voiture voiture = new Voiture();
    voiture.setMoteur(moteur);
    voiture.setCouleur(couleur);
    voiture.rouler(90);
}

On remarque tout de suite que la majorité du code est là seulement pour lier les beans entre eux et que modifier le moteur va nécessiter de modifier notre programme.

En CDI on aurait :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
public class Voiture {
    @Inject
    private Moteur moteur;

    @Inject
    private Couleur couleur;

    // PAS de modificateur

    // Les actions ne changent pas, c'est notre « métier »
    private float vitesseActuelle; // km/h

    public void rouler(final float vitesse) {
        vitesseActuelle = moteur.max(vitesse);
    }

    public void freiner() {
        freiner = vitesse * 0.8;
    }
}

Ce qui a changé :

  • les accesseurs ne sont plus là ;
  • la voiture ne sait pas quel moteur, ni quelle couleur elle a.

C'est ça l'injection de dépendances. Chaque bean reçoit ce dont il a besoin, sans forcément connaître l'implémentation qui correspond, mais comme il a ce qu'il attend – le contrat est respecté – tout se passe bien.

L'utilisation de notre Voiture CDI est alors facile :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
public class VoitureUser {
    @Inject
    private Voiture voiture;

    public void main(String[] argv) {
        voiture.rouler(90);
    }
}

Cependant, ne vous arrêtez pas à cette apparence simpliste, CDI est très puissant et permet de faire beaucoup de choses avancées comme nous allons le voir.

III. Principales implémentations

CDI a trois implémentations principales :

  • Weld : implémentation de référence de CDI. Utilisé dans GlassFish, JBoss (AS et WildFly) ;
  • OpenWebBeans : implémentation Apache. Utilisé dans TomEE, OpenEJB, Geronimo, WebSphere ;
  • CanDi : implémentation Caucho. Utilisé dans Resin.

Weld et OpenWebBeans sont utilisables en « standalone » (c'est-à-dire sans conteneur particulier ou encore à partir d'un simple main(String[]) classique) assez facilement, comme on le verra à la fin.

IV. Un mot sur la découverte des beans

Dans l'exemple précédent, il n'a pas été question de déclaration de beans dans un fichier XML ou autre. CDI (version 1.x) n'a pas de descripteur contrairement aux autres spécifications EE. Pour découvrir les beans, il utilise ce qu'on appelle le scanning. Pour faire simple, il prend l'application (classpath en standalone, classloader de la webapp et ses parents pour un war) et regarde toutes les classes pour découvrir les beans.

Ce genre d'opération peut potentiellement s'avérer assez long, alors il y a une astuce : ne sont scannés que les jar avec un fichier beans.xml ou des classes décorées.

Le fichier beans.xml peut être vide (oui 0 caractère !) et doit se trouver dans le dossier META-INF pour les jar et WEB-INF pour les war (même si pour ces derniers, META-INF est toléré).

Il est préférable d'utiliser cette solution. Toutefois, depuis CDI 1.1 il est désormais possible d'utiliser uniquement des annotations, donc sans nécessiter la présence de ce fichier XML. Dans ce cas, les beans des jar sans beans.xml seront ceux :

  • qui sont annotés avec un scope ;
  • qui sont annotés avec un stéréotype.

L'inconvénient de cette dernière solution est qu'elle peut ralentir un peu le démarrage et que plusieurs fonctionnalités de CDI ne seront pas disponibles. Mais pour des applications simples, cela convient très bien.

V. @Inject : par là où tout commence

CDI est surtout basé sur l'injection et le marqueur d'injection. Cela se traduit par l'annotation javax.inject.Inject. Elle peut être apposée :

  • sur un attribut
 
Sélectionnez
1.
2.
3.
4.
public class Voiture {
    @Inject
    private Moteur moteur;
}
  • sur un constructeur
 
Sélectionnez
1.
2.
3.
4.
public class Voiture {
    @Inject
    public Voiture(Moteur moteur) {...}
}
  • sur un modificateur
 
Sélectionnez
1.
2.
3.
4.
public class Voiture {
    @Inject
    public setMoteur(Moteur moteur) {...}
}

VI. Qualificateurs : pourquoi et quand

Il arrive que le type ne soit pas suffisant pour différencier deux beans. On peut avoir deux moteurs, mais un de marque X1 et un autre de marque X2. Cependant les deux sont des « Moteur ».

Pour garder le typage fort, CDI permet d'enrichir avec une information supplémentaire le type du bean : ce sont les qualificateurs.

En pratique un qualificateur est une annotation décorée avec @javax.inject.Qualifier.

Définir un qualificateur est aussi simple que définir une annotation décorée de @Qualifier.

Pour différencier les moteurs, on peut faire un qualificateur par marque par exemple :

 
Sélectionnez
1.
2.
3.
4.
@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD, PARAMETER })
public @interface MarqueX1 {}

Ensuite, pour avoir un Moteur de marque X1, il suffit d'ajouter le qualificateur à côté de @Inject au point d'injection :

 
Sélectionnez
1.
2.
3.
4.
5.
public class Voiture {
    @Inject
    @MarqueX1
    private Moteur moteurDeMarqueX1;
}

Pour que le moteur corresponde à ce point d'injection, il doit, lui aussi, avoir ce qualificateur :

 
Sélectionnez
1.
2.
3.
@MarqueX1
public class MoteurDeMarqueX1Implementation extends Moteur {
}

Si on veut différencier à l'injection les moteurs par marque et puissance, on peut faire un qualificateur MoteurQualifier générique par exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD, PARAMETER })
public @interface MoteurQualifier {
    String marque() default "";
    float puissance() default 0.f;
}

Et le moteur sera alors :

 
Sélectionnez
1.
2.
3.
@MoteurQualifier(marque = "X1", puissance = 120.f)
public class MoteurDeMarqueX1Implementation extends Moteur {
}

Si un qualificateur a comme celui-ci des paramètres, ceux-ci sont utilisés pour la résolution. Cela signifie que cette voiture utilisera le MoteurDeMarqueX1Implementation :

 
Sélectionnez
1.
2.
3.
4.
5.
public class Voiture {
    @Inject
    @MoteurQualifier(marque = "X1", puissance = 120.f)
    private Moteur moteurDeMarqueX1;
}

Mais que :

 
Sélectionnez
1.
2.
3.
4.
5.
public class Voiture {
    @Inject
    @MoteurQualifier
    private Moteur moteurDeMarqueX1;
}

ne fonctionnera pas.

Dans notre cas, on veut par exemple que la marque soit utilisée pour la résolution, mais on ne veut pas utiliser la puissance. On veut donc que la suite fonctionne :

 
Sélectionnez
1.
2.
3.
4.
5.
public class Voiture {
    @Inject
    @MoteurQualifier(marque = "X1")
    private Moteur moteurDeMarqueX1;
}

Pour cela, il suffit de dire qu'on veut ignorer la méthode puissance de notre qualificateur. Ainsi, il suffit de la décorer avec @javax.enterprise.util.NonBinding :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD, PARAMETER })
public @interface MoteurQualifier {
    String marque() default "";

    @NonBinding
    float puissance() default 0.f;
}

On vient de voir qu'en ajoutant un qualificateur à un bean, on « enrichit » son type d'une donnée supplémentaire permettant à CDI de lever des ambiguïtés si elles existent.

Dans ce contexte il y a deux qualificateurs particuliers : @javax.enterprise.inject.Default et @javax.enterprise.inject.Any. Le premier est celui utilisé si aucun qualificateur n'est spécifié sur le bean. Le second est un qualificateur que possèdent implicitement tous les beans.

@Any est utilisé particulièrement pour dire que l'on veut une injection en ignorant les qualificateurs (on veut une voiture, peu importe sa marque et sa puissance).

Pour finir avec les qualificateurs, il est bon de savoir que l'API propose également l'annotation @javax.enterprise.util.AnnotationLiteral, laquelle permet de définir des qualificateurs programmatiquement. Cet article ne va pas à ce niveau de détail, mais quand vous vous serez familiarisé avec CDI et commencerez à écrire vos propres extensions, vous verrez que c'est très utile et évite de jouer avec la réflexion Java outre mesure.

Pour notre MoteurQualifier on aurait :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
public class MoteurQualifierLiteral extends AnnotationLiteral<MoteurQualifier> implements MoteurQualifier {
    private String marque;

    private float puissance;

    public  MoteurQualifierLiteral(String m, float p) {
        marque = m;
        puissance = p;
    }

    @Override
    public String marque() { return marque; }

    @Override
    public float puissance() { return puissance; }
}

Sans rentrer dans le détail non plus, CDI fournit plusieurs méthodes pour résoudre un bean programmatiquement, auxquelles il faut préciser des annotations correspondant aux qualificateurs du bean. Pour résoudre un moteur de marque X1, il suffira de fournir l'annotation: new MoteurQualifierLiteral("X1", 0.f);.

Note : comme puissance est déclarée comme @NonBinding, dans notre cas on pourrait renvoyer une valeur constante dans puissance().

Il est à noter que quelques qualificateurs sont fournis par défaut en plus de @Default et @Any :

  • @Named : la valeur est utilisée dans le binding
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
@Named("biTurbi")
public class BiTurbo extends Moteur {}

// Dans la classe cliente
@Inject
@Named("biTurbo")
private Moteur moteur;

Ce qualificateur est intéressant, car par défaut (i.e. sans valeur spécifiée) il utilise le nom de la classe décapitalisé. Dans notre exemple, le code suivant faisait exactement la même chose :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
@Named
public class BiTurbo extends Moteur {}

// dans la classe cliente
@Inject
@Named("biTurbo")
private Moteur moteur;
  • @Model : un « alias » pour @Named (sans paramètre), il implique aussi le scope « request » ;
  • @New : déprécié depuis CDI 1.1, il permet d'avoir une « nouvelle » instance (ne respecte pas les scopes).

VII. Scopes : gestion de cycle de vie automatique

Les scopes permettent de définir le cycle de vie des beans. Il y en a plusieurs fournis par défaut, mais vous pouvez écrire les vôtres comme on va le voir.

Un bean CDI est un « singleton dans son scope ».

Par défaut CDI fournit les scopes suivants :

  • @RequestScoped : lié à la « requête ». Typiquement la requête HTTP, mais peut aussi être considéré comme lié à un ThreadLocal ;
  • @SessionScoped : lié à la session HTTP ;
  • @ApplicationScoped : lié à l'application (créé à la première invocation et détruit quand l'application est détruite) ;
  • @ConversationScoped : lié à la conversation courante (sorte de sous-session démarrée/stoppée manuellement) ;
  • @Dependent : crée une nouvelle instance pour l'injection en cours.

Il y a deux types de scopes :

  • les normal scopes (@ApplicationScoped, @RequestScoped, @SessionScoped) : deux injections dans le même contexte (même requête pour @RequestScoped) utilisent la même référence ;
  • les pseudoscopes (@Dependent) : il n'y a pas d'instance courante, donc le postulat précédent n'est pas garanti.

Pour notre voiture, on peut imaginer écrire un scope « Trajet ». Cela se fait assez facilement en deux étapes :

  • créer une annotation représentant son scope ;
  • implémenter son scope : cela signifie implémenter la résolution des instances en fonction d'un contexte lié au scope ;
  • enregistrer notre scope.

Déclarer un scope signifie qu'on déclare une annotation décorée avec @NormalScope ou @Scope pour les pseudoscopes.

Ici, on a par exemple :

 
Sélectionnez
1.
2.
3.
4.
@NormalScope // on peut préciser si nos beans doivent être passivables ou pas, avec l'attribut passivating
@Rentention(RUNTIME)
@Target({ TYPE, FIELD, METHOD })
public @interface TrajetScoped {}

Puis, pour définir notre résolution, on implémente javax.enterprise.context.spi.Context  :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
public class TrajetContext implements Context {
    private Repository repository = new Repository();

    @Override
    public Class<? Extends Annotation> getScope() {
      return TrajecScoped.class;
    }

    @Override
    public boolean isActive() {
        return true;
    }

    @Override // find only
    public <T> T get(Contextual<T> contextual) {
      return (T) repository.lookup(contextual);
    }

    @Override // findOrCreate behaviore
    public <T> T get(Contextual<T> contextual, CreationalContext<T> creationalContext) {
      Object instance = repository.lookup(contextual);
      if (instance == null) {
          instance = (T) contextual.create(creationalContext);
        repository.created(contextual, instance); // Mettre en cache l'instance pour l'état "courant".
      }
      return instance;
    }
}

Bien entendu, l'implémentation a été rendue abstraite par le « repository », mais ce TrajetContext montre qu'un contexte personnalisé est simple une fois qu'on a bien défini son cycle de vie associé. Le scope « application » par exemple (@ApplicationScoped) utilise un map qui est activé le temps de vie de l'application. Le scope «requête » (@RequestScoped) dure le temps d'une requête, c'est-à-dire le temps qu'un thread a un contexte particulier – le plus souvent une requête HTTP.

Dans notre cas, on peut penser avoir un « pool » de voitures dans notre (grand) garage et qu'on en prend une quand on sort. Dans ce cas, remplacer « Repository » par une implémentation de pool suffit. La seule difficulté sera de prendre en compte l'évènement « fin de trajet » pour récupérer la voiture et la remettre dans le pool. Mais si on suppose que nos voitures sont utilisées par un loueur, on aura l'information quand le client ramène la voiture et on pourra alors récupérer notre contexte et rendre la voiture au pool.

Finalement pour enregistrer notre contexte, on a simplement besoin d'une extension CDI très simple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public class TrajetExtension implements javax.enterprise.inject.spi.Extension {
    // addScope est optionnel, utile uniquement si le jar contenant @TrajetScoped n'est pas scanné
    // commun lorsque l'on crée un scope à partir d'un existant, dépourvu d'annotation
    void addScope(@javax.enterprise.event.Observes BeforeBeanDiscovery bbd) {
      bbd.addScope(TrajetScoped.class, true, false);
    }

    // ajoute le contexte d'implémentation dans CDI
    void addContext(@Observes AfterBeanDiscovery abd) {
      abd.addContext(new TrajetContext());
    }
}

Pour que cette extension soit prise en compte, il suffit de la déclarer en mettant son nom qualifié dans META-INF/services/javax.enterprise.inject.spi.Extension.

Il est plutôt rare d'avoir besoin de créer ses propres scopes, mais une fois qu'on sait définir l'état courant d'un scope, cela reste simple à mettre en œuvre.

VIII. @PostContruct, @PreDestroy : réagir au cycle de vie de ses beans

Les instances de beans ont souvent besoin d'initialiser des ressources et de les nettoyer quand elles n'en ont plus besoin. Pour cela, il faut que le bean sache qu'il est créé et qu'il est détruit. Pour le premier, on pourrait utiliser le constructeur, mais si on a besoin des injections alors cela ne marche plus.

C'est dans ce but que @PostConstruct et @PreDestroy sont utilisés. À supposer qu'on ait un Moteur 2.0 intelligent, il pourrait démarrer et s'arrêter automatiquement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
pubic class Moteur {
    @PostConstruct
    private void demarrer() { ... }

    @PreDestroy
    private void eteindre() { ... }
}

Dans ce cas, la voiture recevrait un moteur allumé et ne devrait pas s'occuper de l'éteindre.

En pratique, on se sert de ces hooks pour initialiser/détruire un client à une ressource externe telle une base de données (au sens large) par exemple. Un autre cas d'utilisation est la conversion vers des objets runtime de configuration :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
pubic class Moteur {
    @Inject
    private Configuration config;

    private float vitesseMax;

    @PostConstruct
    public void init() {
        if ("X1".equals(config.getMarque())) {
            vitesseMax = ...;
        }
    }
}

IX. Instance ou résolution dynamique

On a parlé plus haut des qualificateurs et des @AnnotationLiteral, mais maintenant on va voir un cas concret d'utilisation.

Pour cela, on va supposer que la voiture peut décider quel moteur elle veut, selon un paramètre « marque » :

 
Sélectionnez
1.
2.
3.
public class Voiture {
    public void initMoteur(String marque) { ... }
}

On réutilise ici notre qualificateur @MoteurQualifier et son Literal associé.

Comment résoudre au moment de l'exécution l'instance de moteur désirée ? Pour cela, CDI fournit l'API Instance<X> :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
public class Voiture {
    @Inject
    @Any
    private Instance<Moteur> moteurResolver;

    private Moteur moteur;

    public void initMoteur(String marque) {
      moteur =  moteurResolver.select(new MoteurQualifierLiteral("X1", 100.f)).get();
    }
}

Et voilà, on a utilisé un qualificateur programmatiquement pour récupérer une instance.

Quelques notes sur cette utilisation :

  • pour des questions de performances, il est conseillé de mettre en cache le résultat (affecté à une instance comme dans l'exemple) afin d'éviter sa résolution systématique ;
  • Instance permet de chaîner les qualificateurs si votre instance a besoin de plusieurs qualificateurs, par exemple si on a un qualificateur pour la marque et un pour la puissance, on pourrait faire :
 
Sélectionnez
1.
2.
3.
public void initMoteur(String marque) {
  moteur = moteurResolver.select(new MarqueLiteral("X1")).select(new PuissanceLiteral(100.f)).get();
}
  • Instance fournit quelques méthodes utilitaires pour savoir si la résolution va fonctionner ou pas (isSatisfied() et isAmbiguous()).

X. Les intercepteurs

Les intercepteurs permettent d'effectuer des actions avant et après les appels de méthodes et depuis CDI 1.1, après les appels de constructeurs.

Pour faire cela, il y a trois étapes :

  • définir un @InterceptorBinding ;
  • définir son intercepteur ;
  • activer l'intercepteur dans le beans.xml.

Note : les intercepteurs style EJB (@Interceptors) sont supportés, mais privilégiez les @InterceptorBinding qui permettent un faible couplage entre le bean et l'intercepteur.

On va définir un intercepteur qui audite notre voiture.

Pour définir un @InterceptorBinding, il suffit de créer une annotation décorée avec @javax.interceptor.InterceptorBinding :

 
Sélectionnez
1.
2.
3.
4.
@InterceptorBinding
@Target({TYPE, METHOD}) 
@Retention(RUNTIME) 
public @interface Audited {}

Définir un intercepteur comporte deux étapes :

  • créer une classe décorée avec @javax.interceptor.Interceptor et son @InterceptorBinding ;
  • définir une méthode public Object around(InvocationContext ctx) throws Exception décorée avec @AroundInvoke.

Note : pour les constructeurs, il faut utiliser @AroundConstruct.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@Interceptor
@Audited
public class Auditor { // implements Serializable pour intercepter les beans passivables
    private static Logger LOGGER = Logger.getLogger(Auditor.class.getName());

    @AroundInvoke
    public Object audit(InvocationContext context) throws Exception {
      LOGGER.info("Calling" + context.getMethod().getName());
      return context.proceed();
    }
}

Maintenant, pour activer notre intercepteur, on le déclare dans notre beans.xml :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
<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">
    <interceptors>
      <class>com.company.Auditor<class>
    </interceptors>
</beans>

Note : depuis CDI 1.1, ajouter l'annotation @Priority sur l'intercepteur suffit à l'activer, mais en CDI 1.0 le beans.xml est le moyen le plus simple.

Maintenant que notre intercepteur est activé, il faut l'utiliser. Pour cela, on ajoute juste le binding de l'intercepteur sur le bean à intercepter :

 
Sélectionnez
1.
2.
3.
@Audited
public class Voiture {
}

XI. Les décorateurs

Les décorateurs implémentent le pattern Façade. Ils permettent de modifier une implémentation ou d'auditer de façon plus métier, un service particulier.

La définition d'un décorateur se fait en quatre temps :

  • implémentation de l'interface désirée ;
  • décoration du décorateur avec @Decorator ;
  • injection du bean décoré avec @Delegate si besoin ;
  • activation du décorateur dans le beans.xml (ou avec @Priority pour CDI 1.1 comme les intercepteurs).

Chose importante : les décorateurs peuvent être abstraits ! Cela permet de se rapprocher des traits d'autres langages et évite d'implémenter toutes les méthodes de l'interface décorée.

Supposons que notre voiture soit cette fois-ci une interface et qu'une implémentation de celle-ci ait comme caractéristique d'être limitée à 50 km/h, on peut alors définir ce décorateur :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
@Decorator
public class PasTropViteDecorator implements Voiture {
    @Inject
    @Delegate
    private Voiture delegate;

    @Override
    public void rouler(float vitesse) {
      delegate.rouler(Math.max(50, vitesse));
    }
}

Enfin le beans.xml ressemble à :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
<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">
  <decorators>
    <class>com.company.PasTropViteDecorator</class>
  </decorators>
</beans>

XII. Les alternatives

Parfois on veut pouvoir changer d'implémentation sans tout recoder. Les alternatives permettent cela en les activant dans le beans.xml.

On suppose que Moteur est une interface et qu'on a un bean DefaultMoteur qui est utilisé par défaut.

Si on veut pouvoir changer de moteur par configuration pour une version plus rapide « MoteurRapide » on peut définir la classe :

 
Sélectionnez
1.
2.
3.
4.
@Alternative
public class MoteurRapide implements Moteur {
    // impl
}

et l'activer dans le beans.xml

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
<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">
  <alternatives>
    <class>com.company.MoteurRapide</class>
  </alternatives>
</beans>

Dans ce cas, le moteur injecté dans notre voiture sera MoteurRapide.

Les alternatives fonctionnent aussi avec de l'héritage, mais il est important de noter que les qualificateurs sont pris en compte. Donc si la classe parent déclare deux qualificateurs, l'alternative doit le faire aussi.

XIII. Les spécialisations : une alternative, pas exactement ?

Les spécialisations sont une solution pour remplacer un bean par un autre. Elles ressemblent beaucoup aux alternatives, mais désactivent vraiment le bean spécialisé.

Ici pas besoin de modifier le beans.xml, il suffit d'hériter du bean spécialisé et de décorer la sous-classe avec @Specializes :

 
Sélectionnez
1.
2.
3.
4.
@Specializes
public class VoitureLente extends Voiture {
    // Surcharge ce qui est nécessaire
}

XIV. Que d'annotations : les stéréotypes à la rescousse

Assez vite, on a des annotations partout :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
@Named
@Audited
@Secured
@ApplicationScoped
public class Voiture {
}

Et ça peut être bien pire ;). Quand les intercepteurs sont toujours les mêmes (@Transactional, @Secured, @Audited…) et/ou que les scopes et qualificateurs sont également les mêmes, on peut faire une annotation « raccourci ». C'est ce qu'on appelle un stéréotype :

 
Sélectionnez
1.
2.
3.
@NamedAndSecuredAndAuditedApplicationScopedBean
public class Voiture {
}

Bien sûr, on peut mettre un nom plus explicite :

 
Sélectionnez
1.
2.
3.
@Service
public class Voiture {
}

Conseil : le mieux est d'avoir un nom « explicite » si possible. La spécification par exemple utilise « RequestScoped » au lieu d'un très technique « ThreadLocalScoped ».

 
Sélectionnez
1.
2.
3.
@Trajet
public class Voiture {
}

Pour définir un stéréotype, il suffit de créer une annotation décorée avec @Stereotype et complétée de toutes les annotations que l'on souhaite substituer (scope, interceptor binding, @Alternative, qualificateur).

Note : pour les alternatives, il faut activer le stéréotype dans le beans.xml :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
<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">
  <alternatives>
    <stereotype>com.company.MoteurRapide</stereotype>
  </alternatives>
</beans>

Dans notre cas, on pourrait avoir un stéréotype pour forcer nos beans à avoir un nom et être @TrajetScoped

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
@TrajetScoped
@Named
@Stereotype
@Retention(RUNTIME)
@Target(TYPE)
public @interface Trajet {
}

Dans ce cas, on aura une instance de voiture par trajet et on pourra récupérer cette voiture avec le nom « voiture ».

XV. Comment intégrer du code non CDI : @Produces

Tout cela est bien, mais comment injecter des beans non « CDI-isables ». Prenons le cas d'un mapper JSON que l'on appellera par exemple ObjectMapper. La bibliothèque utilisée n'étant pas CDI, comment peut-on injecter l'ObjectMapper ?

Pour cela, CDI permet de déclarer des fabriques sous forme de méthode ou d'attribut.

Cela se fait simplement en décorant l'attribut ou la méthode avec @Produces :

 
Sélectionnez
1.
2.
3.
4.
@Produces
public ObjectMapper creerMapper() {
    return new ObjectMapper();
}

ou

 
Sélectionnez
1.
2.
@Produces
private ObjectMapper mapper = new ObjectMapper();

ou pour notre cas :

 
Sélectionnez
1.
2.
3.
4.
@Produces
public static ObjectMapper creerMapper() {
    return new ObjectMapper();
}

ou

 
Sélectionnez
1.
2.
@Produces
private static ObjectMapper mapper = new ObjectMapper();

Si le producteur est static, la valeur de retour (pour la méthode) ou la valeur de la variable (pour un attribut) est utilisée directement. Si ce n'est pas le cas, la valeur est lue à partir d'une instance (CDI) de la classe dans laquelle est défini le producteur. À savoir que le bean peut recevoir un scope, des qualificateurs… c'est donc un vrai bean CDI.

Un producteur peut avoir un scope :

 
Sélectionnez
1.
2.
3.
4.
5.
@Produces
@ApplicationScoped
public static ObjectMapper creerMapper() {
    return new ObjectMapper();
}

mais dans ce cas, le bean produit doit pouvoir être « scopé » (proxiable).

Enfin, un producteur peut avoir des qualificateurs comme un ManagedBean normal :

 
Sélectionnez
1.
2.
3.
4.
5.
@Produces
@Named("mapper") // @Named utiliserait "creerMapper"
public static ObjectMapper creerMapper() {
    return new ObjectMapper();
}

Une fois le producteur déclaré, il suffit d'injecter l'instance avec le type et les qualificateurs utilisés :

 
Sélectionnez
1.
2.
3.
4.
5.
public class TrajetReporter {
    @Inject
    @Named("mapper")
    private ObjectMapper mapper;
}

Les méthodes @Produces peuvent également recevoir des injections sous la forme de paramètres additionnels (avec qualificateurs si besoin) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
@Produces
public static ObjectMapper creerMapper(Voiture voiture) { // == @Inject Voiture;
    if (isMappable(voiture)) {
      return new ObjectMapper();
    }
    return new NoopMapper();
}

Elles peuvent aussi recevoir un paramètre de type particulier : InjectPoint. Celui-ci définit le point d'injection (attribut, modificateur…).

L'exemple classique est la création de logger :

 
Sélectionnez
1.
2.
3.
4.
@Produces 
Logger createLogger(InjectionPoint injectionPoint) { 
  return Logger.getLogger( injectionPoint.getMember().getDeclaringClass().getName() );
}

qui s'utilisera :

 
Sélectionnez
1.
2.
3.
public class Voiture {
    @Inject Logger logger; // == Logger.getLogger(Voiture.class.getName());
}

À noter que si le bean dans lequel l'injection est faite (Voiture ici) est « passivation capable » (~ Serializable) alors l'injection devra l'être également. Cela signifie qu'utiliser cela pour un logger n'est peut-être pas une bonne idée, mais l'utiliser pour de la configuration (String) fonctionne parfaitement :

 
Sélectionnez
1.
2.
3.
4.
5.
@Produces
@Config // qualificateur
String config(InjectionPoint injectionPoint) { 
    return System.getProperty( injectionPoint.getMember().getDeclaringClass().getName() +  "#" + injectionPoint.getMember().getName());
}

permet d'utiliser :

 
Sélectionnez
1.
2.
3.
4.
5.
public class Voiture {
    @Inject
    @Config
    private String marque;
}

Enfin, comme @Produces correspond à l'initialisation d'un bean, il existe @Disposes qui correspond à la destruction. Pour cela, il suffit d'utiliser le même type (+ qualificateur) en paramètre décoré de @Disposes :

 
Sélectionnez
1.
2.
3.
public void detruire(@Disposes ObjectMapper mapper) {
    mapper.close();
}

Il faut que cette méthode soit définie dans la même classe que le @Produces.

Enfin, il est à noter qu'une méthode annotée avec @Produces peut avoir des paramètres qui sont considérés comme des points d'injection.

XVI. Bus d'évènement : Event<?>, @Observes

CDI fournit également un bus d'évènement. Il se base principalement sur deux API : Event<> et @Observes. Ce bus est fortement typé (comme les beans : type + qualificateurs) et synchrone (CDI 2.0 définira une version asynchrone).

Supposons que l'on souhaite envoyer un évènement « la voiture démarre ».

La première chose à faire est de définir notre évènement. N'importe quelle classe fera l'affaire, mais pour des raisons évidentes, éviter les types courants tels que String est une bonne idée :

 
Sélectionnez
1.
2.
3.
4.
5.
public class Demarrage {
    private Date date = new Date();

    public Date getDate() { return date; }
}

Puis on injecte un Event paramétré avec le type de notre évènement dans notre voiture :

 
Sélectionnez
1.
2.
3.
4.
public class Voiture {
    @Inject
    private Event<Demarrage> demarrageEvent;
}

Enfin on déclenche notre évènement :

 
Sélectionnez
1.
2.
3.
4.
5.
public void rouler(float vitesse) {
  demarrageEvent.fire(new Demarrage()); // l'instance peut avoir un état, c'est une instance "normale", pas un bean CDI

  // même implémentation qu'avant
}

Note : l'injection de l'Event peut avoir des qualificateurs, dans ce cas l'évènement sera qualifié.

Si nécessaire, il est possible de tester la même version avec un intercepteur ou un décorateur, lequel ne fera que lancer l'Event, permettant ainsi ne pas toucher l'implémentation de Voiture.

Pour réagir à cet Event, il faut l'écouter. Cela consiste à définir une méthode avec en paramètre l'évènement (avec des qualificateurs le cas échéant) :

 
Sélectionnez
1.
2.
3.
public void logDemarrage(@Observes Demarrage demarrage) {
  logger.info("Demarrage à " + demarrage.getDate());
}

De plus, les observateurs peuvent recevoir des injections en paramètre :

 
Sélectionnez
1.
2.
3.
public void logDemarrage(@Observes Demarrage demarrage, Voiture voitureContextuelle) {
  logger.info("Demarrage à " + demarrage.getDate() + " de " + voitureContextuelle);
}

XVII. Maitriser ses beans

Avertissement : cette partie concerne seulement CDI 1.1.

Depuis CDI 1.1, le beans.xml autorise la définition d'exclusions, permettant ainsi de contrôler un minimum le scanning.

À cet effet, la balise <exclude> permet de préciser, via son attribut name, les éléments à ignorer, selon les possibilités suivantes :

  • un package à exclure : com.company.* ;
  • un package et ses sous-packages à exclure : com.company.** ;
  • une classe à exclure : com.company.Exclusion.

Les blocs <exclude> permettent également de rendre conditionnelle l'application des exclusions, à l'aide des balises suivantes :

  • si une classe est disponible ou pas : <if-class-not-available />;
  • si une variable d'exécution (e.g. -DmyProperty) est définie ou si elle possède une valeur particulière : <if-system-property />;
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee">
  <scan>
    <exclude name="com.company.**"/>

    <exclude name="com.company.**">
      <if-class-not-available name="com.google.GWT" />
    </exclude>

    <exclude name="com.company.foo*">
      <if-system-property name="myProperty1" value="true" />
    </exclude>

    <exclude name="com.company.*">
      <if-class-available name="com.company.extension.Replacer" />
    <if-system-property name="myProperty2" />
    </exclude>
  </scan>
</beans>

XVIII. Lien avec la spécification EE

Les EJB sont intégrés à CDI, cela signifie qu'ils peuvent être injectés en utilisant @Inject au lieu de @EJB. Pour les @Stateless et les @Singleton cela ne change rien, mais pour les @Stateful cela est intéressant, car ces derniers sont autorisés (contrairement aux @Stateless et @Singleton) à avoir un scope. Donc on peut lier un @Stateful à une requête ou une session ce qui est assez intéressant pour une gestion de panier par exemple.

Autre point intéressant, @Resource, @PersistenceContext, @PersistenceUnit et @WebServiceRef sont supportés dans les beans CDI lorsqu'ils sont déployés dans un conteneur EE.

Cela signifie qu'on peut par exemple les combiner avec des qualificateurs, via des producteurs :

 
Sélectionnez
1.
2.
3.
4.
@PersistenceContext(unitName = "my-unit")
@MyUnit
@Produces
private EntityManager em;

XIX. Et en standalone ?

CDI 1.x ne définit pas de mode standalone « standard », mais CDI 2.0 le fera.

En attendant, il y a deux principales solutions :

  • utiliser les API des éditeurs (OpenWebBeans ou Weld) ;
  • utiliser Apache DeltaSpike qui fournit pour cela une API « portable » : CdiCtrl.

La documentation du CdiCtrl est disponible ici : http://deltaspike.apache.org/documentation/#_start_a_cdi_container_using_java_se.

En deux mots, l'idée est de déclarer org.apache.deltaspike.cdictrl:deltaspike-cdictrl-api comme dépendance, d'ajouter l'implémentation que l'on désire : org.apache.deltaspike.cdictrl:deltaspike-cdictrl-owb et org.apache.openwebbeans:openwebbeans-impl pour OpenWebBeans par exemple.

Une fois les dépendances modifiées, il suffit d'utiliser l'API CdiCtrl :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
public static void main(String[] args) {
    CdiContainer cdiContainer = CdiContainerLoader.getCdiContainer();
    cdiContainer.boot();

    // Votre code.

    cdiContainer.shutdown();
}

Ce qui est intéressant avec DeltaSpike, c'est qu'il fournit aussi une API pour démarrer/arrêter les contextes/scopes. Cela signifie que l'on peut utiliser @RequestScoped comme un « thread scope », par exemple pour avoir une instance par thread en démarrant/arrêtant le contexte par thread.

Pour cela, il faut utiliser le ContextControl :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
ContextControl contextControl = cdiContainer.getContextControl();
contextControl.startContext(RequestScoped.class);

// Votre code.

contextControl.stopContext(RequestScoped.class);

XX. Conclusion et remerciements

Ce tutoriel vous a présenté les fonctionnalités de base de CDI. Parmi ce qui n'a pas été abordé, il y a les Extensions, lesquelles permettent par exemple d'étendre le conteneur, de créer ses propres beans ou de modifier les existants. L'intégration avec la plateforme Java EE est aussi cruciale : JBatch, JAX-RS, BeanValidation, JSF… autant de points d'intégration prêts à l'emploi qui évitent toute configuration technique et permettent de se concentrer davantage sur la partie métier.

Enfin la version 2.0 est en cours d'élaboration. Un de ses buts est de standardiser l'utilisation de CDI hors conteneur EE – en Java SE en particulier.

Nous tenons à remercier Claude Leloup et Laurent Barbareau pour la relecture orthographique et Mickaël Baron pour la mise au gabarit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2015 Mannibucau. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.