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 à :
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 :
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 :
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 :
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 :
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
2.
3.
4.
public
class
Voiture {
@Inject
private
Moteur moteur;
}
- sur un constructeur
2.
3.
4.
public
class
Voiture {
@Inject
public
Voiture
(
Moteur moteur) {
...}
}
- sur un modificateur
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 :
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 :
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 :
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 :
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 :
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 :
2.
3.
4.
5.
public
class
Voiture {
@Inject
@MoteurQualifier
(
marque =
"X1"
, puissance =
120.
f)
private
Moteur moteurDeMarqueX1;
}
Mais que :
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 :
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 :
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 :
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
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 :
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 :
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 :
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 :
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 :
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 :
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 » :
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> :
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 :
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 :
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.
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 :
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 :
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 :
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 à :
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 :
2.
3.
4.
@Alternative
public
class
MoteurRapide implements
Moteur {
// impl
}
et l'activer dans le beans.xml
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 :
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 :
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 :
2.
3.
@NamedAndSecuredAndAuditedApplicationScopedBean
public
class
Voiture {
}
Bien sûr, on peut mettre un nom plus explicite :
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 ».
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 :
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
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 :
2.
3.
4.
@Produces
public
ObjectMapper creerMapper
(
) {
return
new
ObjectMapper
(
);
}
ou
2.
@Produces
private
ObjectMapper mapper =
new
ObjectMapper
(
);
ou pour notre cas :
2.
3.
4.
@Produces
public
static
ObjectMapper creerMapper
(
) {
return
new
ObjectMapper
(
);
}
ou
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 :
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 :
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 :
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) :
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 :
2.
3.
4.
@Produces
Logger createLogger
(
InjectionPoint injectionPoint) {
return
Logger.getLogger
(
injectionPoint.getMember
(
).getDeclaringClass
(
).getName
(
) );
}
qui s'utilisera :
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 :
2.
3.
4.
5.
@Produces
@Config
// qualificateur
String config
(
InjectionPoint injectionPoint) {
return
System.getProperty
(
injectionPoint.getMember
(
).getDeclaringClass
(
).getName
(
) +
"#"
+
injectionPoint.getMember
(
).getName
(
));
}
permet d'utiliser :
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 :
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 :
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 :
2.
3.
4.
public
class
Voiture {
@Inject
private
Event<
Demarrage>
demarrageEvent;
}
Enfin on déclenche notre évènement :
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) :
2.
3.
public
void
logDemarrage
(
@Observes
Demarrage demarrage) {
logger.info
(
"Demarrage à "
+
demarrage.getDate
(
));
}
De plus, les observateurs peuvent recevoir des injections en paramètre :
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 />;
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 :
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 :
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 :
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.