Thématiques principales

mercredi 15 août 2018

JEE : CDI partie 1 : l'injection

Apres un mois de juillet avec de l'IA, nous revoilà avec du Java, en particulier JEE. Cet article intervient dans la ligné de l'article [1] ou nous avions présenté les principes de l'injection de dépendance mais étions passé au dessus des implémentations possibles. Nous traiterons donc en toute logique d'une des implémentations majeur existante : CDI (l'autre étant Spring mais nous le traiterons dans un article dédié).

Introduction

CDI, ou Context and Dependency Injection, est donc un framework d'injection de dépendances évoluant dans l'environnement JEE. Comme d'habitude avec les technologie JEE, CDI a été élaboré après que le framework concurrent ici Spring qui en a posé les bases. Ainsi il s'appuie sur la formalisation apportée par les  JSR 299 et 346 [2], [3] a partir des années 2009 et fut décliné selon differentes versions telles que:
  • v1 en 2009 avec JEE 6 
  • v1.1 en 2013
  • v1.2 en 2014
  • v2 en 2017 dans JEE 8 [10]

Standalone mode

La plupart des concepts de base de CDI ont été élaboré dans ses premières versions et en fait sa dernière évolution n'apporte surtout que la possibilité de faire évoluer le framework en dehors d'un conteneur d'application en permettant d'instancier le framework directement dans un classique main comme suit:

1
2
3
 public static void main(String[] args) {
  CDI<Object> container = new Weld().initialize();
 }

en utilisant la version 3 de Weld [4] et en le declarant dans le pom du projet:

1
2
3
4
5
<dependency>
 <groupId>org.jboss.weld.se</groupId>
 <artifactId>weld-se-core</artifactId>
 <version>3.0.5.Final</version>
</dependency>

Pourtant cette utilisation en standalone n'est pas forcement préconisé et est seulement à utiliser que lorsque l'on souhaite construire son propre conteneur d'application Java (nous reviendrons sur cette possibilité quand nous aborderons OSGI, bientot)

Standard mode

En l’état, le mieux que l'on puisse faire, c'est se référer à la spécification JEE en utilisant la dépendance suivante:

1
2
3
4
5
6
<dependency>
 <groupId>javax</groupId>
 <artifactId>javaee-api</artifactId>
 <version>8.0</version>
 <scope>provided</scope>
</dependency>

Ainsi l'application que l'on produira (sous la forme d'un War ou d'un EAR si l'on utilise des EJB) s'appuiera sur les implémentations java fournies par le container d'applications.

Pour traiter de CDI, n'importe quel container supportant une version 1.2 de CDI fera l'affaire (idéalement appuyez vous sur Glassfish [5] qui reste l’implémentation JEE de référence)

L'injection

Retournons a CDI et ce que le framework permet de faire. CDI comme son nom l’évoque traite de l'injection de dépendances. Globalement ceci passe par l'utilisation de beans et de l'annotation @Inject.

1
2
@Inject
private ServletConf servletConf;

On ne peut pas plus simple pour un framework! ou du moins pour son utilisation en première approche. Cependant ce n'est pas tout, car CDI fourni de nombreuses autres fonctionnalités techniques pour la conception logicielle en s'appuyant sur différents Design Pattern tel que:
  • Le Decorateur [6]
  • L'Observateur [7]
  • L'InvocationHandler [8]
  • La Factory [9]
Ou entre autre la possibilité de spécifier au moteur d'injection d'utiliser des alternatives (avec l'annotation du même nom @Alternative) des spécialisations ou de faire de la résolution dynamique à l'aide de la classe Instance<T>

Nous nous limiterons aux patterns, les annotations cités ci-dessus étant assez immédiates a utiliser.

La factory [11]

Apres cet exemple simple, considérons par exemple que nous ne souhaitons plus simplement injecter un bean quelconque mais injecter un bean de notre propre composition. Comment faire? Et bien la va intervenir une annotation @Produces que l'on va associer a une ou plusieurs méthodes qui seront désignées comme les constructeurs des beans à injecter.

L'interet de l'approche est double, d'une part elle permet d'associer la création d'un bean a injecter avec un comportement et d'autre part de permettre a des objets qui ne sont pas des beans [12] a être quand même injectable :

1
2
3
4
5
@Produces 
public CDIModel createCDIModel()
{
 return new CDIModel();
}

Alors est ce que ça marche? Et bien non!! Arf pourquoi ?? C’était si simple, ça aurait du! Et bien simplement que le moteur CDI va avoir un conflit d'injection, il se base sur les signature de type pour trouver quelque chose a injecter et la, il en trouve deux : la classe CDIModel elle même et la méthode createCDIModel qui retourne un CDIModel.

Pour s'en sortir, il nous faut un moyen de discrimination. Pour cela, on va utiliser l'annotation @Qualifier qui va nous permettre de construire des annotations utilisables a la fois sur les éléments sources et sur les éléments cibles, la ou l'on souhaite réaliser l'injection.

On va donc créer une annotation maison pour discriminer les beans construis avec la factory.

1
2
3
4
5
6
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
public @interface FromFactory {

}

On l'ajoute sur la signature de la méthode et sur l’élément ou l'injecter et la oh miracle ça fonctionne (ici par exemple on injecte avec les deux approches dans une jsp)

1
2
@Produces @FromFactory
public CDIModel createCDIModel()

Sans oublier d'ajouter l'annotation @Default sur notre classe CDIModel de façon a ce que par défaut le moteur d'injection n'utilise pas la factory. Il ne le fera que si on lui dit explicitement.

Enfin, on utilise les deux modes d'injection:

1
2
3
4
5
6
@Inject
private CDIModel model;

@Inject
@FromFactory
private CDIModel model2;

donnant

1
2
3
Model CDI: les données du model

Model 2 CDI: les données du model From Factory

Named

Pour faire plus simple, et ne pas forcement avoir a construire des annotations, CDI fourni une annotation dédiée à la discrimination des beans: @Named qui permet de donner un nom a l’élément producteur du bean et de faciliter le binding sur l’élément où l'injecter. L’intérêt de cette annotation est qu elle permet de construire des beans spécifiques utilisables directement dans les JSP (même si l'on a pas encore parler de cette techno, cela va permettre d'utiliser des beans dans des éléments de pseudo code déclarés dans le code html, nous y reviendrons)

Ainsi par exemple, on va pourvoir ajouter deux @Named, l'un sur la classe:

1
2
3
4
@Named("model")
public class CDIModel {
...  
}

et l'autre sur la factory

1
2
3
4
5
6
7
@Produces @Named("model2")
public CDIModel createCDIModel()
{
 CDIModel m=new CDIModel();
 m.setDataModel(m.getDataModel()+" From Factory");
 return m;
}

les deux permettant de ne pas avoir a écrire le code suivant dans notre servlet ni même de spécifier qu'il faut les injecter:

1
2
3
4
5
6
7
8
if(model!=null)
 request.setAttribute("model", model);

if(model2!=null)
{
 model2.setDataModel(model2.getDataModel()+" From Factory");
 request.setAttribute("model2", model2);
})

Les scopes

Puisque nous parlons de JSP, profitons en pour nous intéresser au cycle de vie des beans. C'est quoi le lien? Bien entendu il s'agit de la durée de vie du bean selon le type de requête qu'un utilisateur est amener a faire sur les differentes pages de notre application (on parle d'application JEE quoi...). Entre autre on doit se demander si notre bean doit vivre juste le temps d'une requete (on utilisera alors l'annotation @RequestScoped), le temps de plusieurs requêtes (@SessionScoped) ou carrément le temps de toute la durée de vie de notre application (@ApplicationScoped).

Ainsi selon le scope choisi, un bean ne vivra pas le même temps... cela signifie aussi que celui ci sera recréé autant de fois que nécessaire, selon les demandes. Donc resumons:

  • Avec un scope application, le bean ne sera donc créer qu'une fois et ne sera a priori pas détruit (sauf lors de l’arrêt de l'applicatif). 
  • Le scope requete provoque a l'inverse la création du bean a chaque requête http et détruite a l'issu de la réponse. 
  • Le scope session est un intermédiaire où le bean est créé a la connexion de l'utilisateur et a l'aide d'information de session permet de maintenir le bean tant que cet utilisateur utilise l'application.

Pour tester ces scopes nous allons aller un peu plus loin dans la gestion du cycle de vie du bean. En effet, on a bien compris qu'un bean a une durée de vie plus ou moins longue mais on a pas parler de ce qui se passe lors de sa naissance ou de sa mort. Bien sur nous avons évoqué l'utilisation de l'annotation @Produces permettant de réaliser une pseudo initialisation du bean mais en fait l'utilisation de @Produces a surtout pour vocation d’initialiser un contexte de construction du bean et non le bean lui-même.

Ainsi donc, pour ce qui concerne la naissance et la mort d'un bean, il existe deux annotations spécifiques @PostConstruct et @PreDestroy permettant de preciser deux méthodes à exécuter à l'instar d'un constructeur et d'un destructeur.

Pouvoir exécuter un destructeur, on comprend vite que ça permet d’éventuellement désallouer des ressources. Par contre, on peut discuter de la pertinence d'un pseudo constructeur par rapport au vrai constructeur de la classe. En effet celui ci a pour objet justement d'initialiser l'objet! Alors pourquoi un @PostConstrut? Simplement pour permettre au bean d'exister complètement avant de réaliser une initialisation quelconque ceci permettant donc la prise en compte de la présence d’éventuelles autres injections de bean dans le bean que nous sommes en train de construire! Sans cela, point de salut!

Donc voila un petit exemple pour illustrer tout cela et voir comment fonctionne nos @ResquestScope et @ApplicationScope (on ne traitera pas @SessionScope, celui ci étant plus long a mettre en oeuvre et n'amenant qu'assez peu d'infos supplémentaires sur le fonctionnement de CDI)

Dans un premier temps, on va tout d'abord créer deux nouveaux bean pour lesquels nous définirons les méthodes @PostConstruct et @PreDestroy.

A ces deux classes on va ajouter a l'un @ApplicationScope et a la seconde @RequestScope comme suit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@ApplicationScoped
public class CDIModel {

 private String dataModel="les données du model";
 
 @PostConstruct
 public void build()
 {
  System.out.println("Construction de CDIModel");
 }
 
 @PreDestroy
 public void destroy()
 {
  System.out.println("Destruction de CDIModel");
 }
...
}
et


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Named
@RequestScoped
public class CDIModelShortScope {

 @PostConstruct
 public void build()
 {
  System.out.println("Construction de CDIModelShortScope");
 }
 
 @PreDestroy
 public void destroy()
 {
  System.out.println("Destruction de CDIModelShortScope");
 }
 
}

On réalise finalement les actions suivantes
  • On build et on construit notre EAR.
  • On charge celui ci dans notre container d'application preferé
  • On charge la page et on fait plusieurs raffraichissement.
  • On fini par decharger l'EAR
On jette un œil aux logs :

 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
29
30
31
32
33
34
35
36
37
38
[2018-08-13T23:42:14.969+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=31 _ThreadName=Thread-8] [timeMillis: 1534196534969] [levelValue: 800] [[
  Construction de CDIModel]]

[2018-08-13T23:42:14.986+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=31 _ThreadName=Thread-8] [timeMillis: 1534196534986] [levelValue: 800] [[
  Construction de CDIModelShortScope]]

[2018-08-13T23:42:15.002+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=31 _ThreadName=Thread-8] [timeMillis: 1534196535002] [levelValue: 800] [[
  Destruction de CDIModelShortScope]]

[2018-08-13T23:43:31.005+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=32 _ThreadName=Thread-8] [timeMillis: 1534196611005] [levelValue: 800] [[
  org.tc.jee.essai.prod.web.cdi.bean.ServletConf@3dc05bc]]

[2018-08-13T23:43:31.013+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=32 _ThreadName=Thread-8] [timeMillis: 1534196611013] [levelValue: 800] [[
  Construction de CDIModelShortScope]]

[2018-08-13T23:43:31.014+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=32 _ThreadName=Thread-8] [timeMillis: 1534196611014] [levelValue: 800] [[
  Destruction de CDIModelShortScope]]

[2018-08-13T23:43:32.079+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=34 _ThreadName=Thread-8] [timeMillis: 1534196612079] [levelValue: 800] [[
  org.tc.jee.essai.prod.web.cdi.bean.ServletConf@3dc05bc]]

[2018-08-13T23:43:32.081+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=34 _ThreadName=Thread-8] [timeMillis: 1534196612081] [levelValue: 800] [[
  Construction de CDIModelShortScope]]

[2018-08-13T23:43:32.081+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=34 _ThreadName=Thread-8] [timeMillis: 1534196612081] [levelValue: 800] [[
  Destruction de CDIModelShortScope]]

[2018-08-13T23:43:33.420+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=30 _ThreadName=Thread-8] [timeMillis: 1534196613420] [levelValue: 800] [[
  org.tc.jee.essai.prod.web.cdi.bean.ServletConf@3dc05bc]]

[2018-08-13T23:43:33.422+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=30 _ThreadName=Thread-8] [timeMillis: 1534196613422] [levelValue: 800] [[
  Construction de CDIModelShortScope]]

[2018-08-13T23:43:33.423+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=30 _ThreadName=Thread-8] [timeMillis: 1534196613423] [levelValue: 800] [[
  Destruction de CDIModelShortScope]]

[2018-08-13T23:43:42.998+0200] [glassfish 5.0] [INFO] [] [] [tid: _ThreadID=48 _ThreadName=Thread-8] [timeMillis: 1534196622998] [levelValue: 800] [[
  Destruction de CDIModel]]
Effectivement la on se rend compte que le bean ayant le scope de applicatif n'est créer qu'au premier chargement de la page tandis que le scope associé a la requête et créer et détruit quasiment a la volé lors de chaque rafraîchissement.

Conclusion

Bien sur nous n'avons pas tout vu il reste les patterns avec CDI! mais cet article est deja assez long et vous propose de revenir d'ici quelques jours pour terminer ce sujet.

Références

[1] https://un-est-tout-et-tout-est-un.blogspot.com/2018/05/iod-linjection-de-dependances.html
[2] https://docs.oracle.com/javaee/6/tutorial/doc/giwhl.html
[3] http://openwebbeans.apache.org/
[4] http://weld.cdi-spec.org/
[5] https://javaee.github.io/glassfish/
[6] https://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-decorateur.html
[7] https://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-observateur.html
[8] https://un-est-tout-et-tout-est-un.blogspot.com/2018/04/invocationhandler.html
[9] https://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-factory.html
[10] http://www.cdi-spec.org/news/2017/05/15/CDI_2_is_released/
[11] https://readlearncode.com/java-ee/introduction-to-cdi-producer-methods/
[12] https://docs.oracle.com/javaee/6/tutorial/doc/gjfzi.html
[13] https://rmannibucau.developpez.com/tutoriels/cdi/introduction-cdi

Aucun commentaire:

Enregistrer un commentaire