Archives Wild Code School
Java

CDI - One container to rule them all 1/2

Quête 1 sur 43

Cette quête que tu dois accepter sous peine de te retrouver renvoyé sous le reigne de Sauron, va te permettre de découvrir une des plus belles pièces d'ingénierie de ces dernières années : CDI.

Objectifs

  • Comprendre le concepts d'injection de dépendances
  • Comprendre le cycle de vie des beans
  • Rendre ton code plus simple et modulable

Séparation des concepts

Dans la quête JSP, tu as créé la Servlet GithubersServlet qui instanciait 5 Githubers et les propagaient jusqu'à la JSP.

Dans une vraie application, les Githubers viendraient d'une base de données ou d'une API, et il n'est pas souhaitable de mettre toute cette mécanique directement dans une servlet.

Nous allons donc encapsuler les Githubers dans un DAO. Le "Data Access Object" est un "pattern" visant à encapsuler l'accès à une source de données (que ce soit pour la lecture ou l'écriture) dans une classe dédiée.

Aussi, tu vas créer un package fr.wildcodeschool.githubtracker.dao et y créer l'interface suivante:

java
package fr.wildcodeschool.githubtracker.controller;  

import fr.wildcodeschool.githubtracker.model.Githuber;  
import java.util.List;  

public interface GithuberDAO {  
    List<Githuber> getGithubers();  
}

Dans la suite de la quête, nous allons créer différentes implémentations de cette interface et l'intégrer au reste de l'application.

Tu vas commencer par créer une implementation DumbGithuberDAO dans le même package. Inutile de spécifier un constructeur. Tu déplaceras le code de GithubersServlet qui instancie les Githubers dans cette classe. Et la méthode -que tu devras nécessairement implémenter pour que cela compile- les retournera! Si ta liste de Githubers est statique tu n'oublieras pas d'utiliser Collections.unmodifiableList() pour éviter qu'un appelant ne puisse modifier les Githubers depuis l'exérieur du DAO...

Puis, dans GithubersServlet tu déclareras une référence de GithuberDAO (pas DumbGithuberDAO) et tu l'assigneras à une nouvelle instance de DumbGithuberDAO soit dans le constructeur, soit dans init();

Enfin, tu appeleras getGithubers() sur ton instance de DAO pour récupérer les githubers et les envoyer à la JSP comme avant. Vérifie que l'application fonctionne toujours.

↗
Implementing an interface
docs.oracle.com
↗
DAO @ Oracle
oracle.com
↗
DAO @ Wikipedia
en.wikipedia.org

La poupée russe des dépendances

Dans les applications de plus grande envergure, les Servlets appellent rarement les DAO directement. Le code est une sorte de mille-feuilles et traditionnellement, d'autres classes Java faisaient des traitements métiers, mais dépendaient (avaient besoin) d'autres objets pour acéder aux couches sous-jacentes de l'application (typiquement un DAO). Ainsi tu te retrouvais à passer ta dépendance dans le constructeur de ton objet métier.

Voici un exemple hypothétique (rien à coder):

java
public class GithubersService {  
    private GithubersDAO dao;  

    public GithubersService(GithubersDAO dao) {  
        this.dao = dao;  
    }  

    public List<Githuber> getGithubers(String loginPart) throws GithubersException {  
        try {  
            return dao.getGitHubers(loginPart);  
        } catch (SQLException e) {  
            throw new GithubersException("Error loading Githubers", e);  
        }  
    }

...maintenant imagine que le DAO ait besoin d'une connexion à une base de données:

java
public class GithubersDAO {  
    private Connection connection;  

    public GithubersDAO(Connection connection) {  
        this.connection = connection;  
    }  

    public List<Githuber> getGitHubers(String loginPart) throws SQLException {  
        try(PreparedStatement ps = connection.prepareStatement("select...from...where... and login like ?")) {  
            ps.setString(1, '%' + loginPart + '%');  
            ResultSet rs = ps.executeQuery();  
            while(rs.next()) {  
                // ...  
  }  
            return ...  
        }  
    }

Donc pour avoir un GithubersService, il faut faire (schématiquement) new GithubersService(dao) où dao = new GithubersDAO(connection) où connection = DriverManager.getConnection() ...ou pire...en fonction de l'architecture de ton application.

Maintenant, imagine que tu veuilles utiliser GithubersService dans une servlet (dont, rappelons-le, l'instanciation est faite par le serveur d'application, PAS par toi) :

java
@WebServlet(urlPatterns = {"/mypath"})  
public class MyServlet extends HttpServlet {  
    private GithubersService ghs;  

    @Override  
  public void init() throws ServletException {  
        ghs = new GithubersService(/*ARGHHHH KILL ME!*/);  
    }  

    @Override  
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        // do something...  
        ghs.getGithubers(....)  
        // do something...
  }  
}

ARGH! Comment donc instancier toute la grappe? Hé bien il faut aussi créer le DAO depuis la servlet...mais aussi récupérer la connection...encore depuis la servlet avec une pincée de TheadLocal si tu sais l'utiliser. C'est moche. Autre possibilité: récupérer ces resources via des méthodes statiques, puisque DriverManager.getConnection() est statique. Dur dur quand on fait de la POO! Et si la connexion à la base s'interrompait alors que tu avais gardé une référence vers un objet dépendant de cette connexion dans ta servlet? L'application ne fonctionnait plus!

Rassure-toi cet exemple était hypothétique...mais pour te donner une idée de la chose, tu vas créer un package fr.wildcodeschool.githubtracker.service et y ajouter une classe GithubersService. Tu y ajouteras:

  • Une propriété de type GithuberDAO;

  • Un constructeur prenant un GithuberDAO en argument;

  • Une méthode public List<Githuber> getAllGithubers() qui délèguera au DAO;

  • Une méthode public Githuber getGithuber(String login) qui retourne un seul Githuber en fonction de son login (ou null si introuvable). Tu l'implémenteras aussi à l'aide d'un stream/filter de préférence, ou à défaut, d'une boucle for;

  • Une méthode public void track(String login) qui permettra de demander à l'application de suivre les actions d'un Githuber sur Github. Pour l'instant elle ne fait rien. Dans son corps tu mettras simplement un commentaire //TODO pour qu'on puisse le retrouver à l'aide du panel TODO de IntelliJ;

Enfin, tu enlèveras la référence au GithuberDAO dans la GithubersServlet et tu utiliseras à la place (une instance de) GithubersService! Verifie que l'application tourne toujours correctement!

↗
ThreadLocal - pour information
docs.oracle.com
↗
Stream examples
mkyong.com

Historique de l'IoC en Java

Pour pallier entre autres à la lourdeur de la gestion des dépendances, Rod Johnson introduisit Spring en 2004. Il permettait, moyennant une configuration XML, de construire une sorte de dépôt d'instances de classes nommées par des alias et injectées les unes dans les autres. Ce n'était alors plus le développeur qui créait les instances, c'était Spring. Il suffisait ensuite d'appeler le bon alias sur le depôt pour récupérer l'instance. On appelle ça l'inversion de contôle (IoC). Ce fut un énorme succès.

En parallèle, les développeurs commençaient à utiliser l'ancètres des annotations grâce à certains frameworks (ex: dans les commentaires avec la suite Weblogic).

Très rapidement, JavaSE 1.5 et JEE5 firent des annotations un élément à part entière de l'écosystème Java. Et alors que JBoss poussait Seam, un concurrent à Spring, Gavin King, génial inventeur d'Hibernate, l'ancètre de JPA, et commiter sur Seam, se lançait dans la JSR-299 aka "WebBeans" pour JEE6, ciblant principalement les applications JavaEE Web. King aurait pu simplement tenter de faire standardiser Seam. Mais il introduisit, en plus de l'IoC (@Inject, @Produces), le concept de "contexte" (@*Scoped), allant ainsi beaucoup plus loin. Ainsi, non seulement le Serveur d'Applications, à l'aide de WebBeans allait, à l'instar de Spring, gérer les dépendances entre les objets à la place de l'utilisateur, mais il allait aussi contrôler leur cycle de vie en fonction d'évènements propres à l'application (eg: l'arrivée d'une requête sur une servlet). Constatant que la puissance de ce concept permettrait in-fine d'englober toutes les initiatives d'injection par annotations (@EJB pour les EJB, @ManagedBean pour JSF...), la JSR-299 fut renommée CDI pour "Contexts & Dependency Injection". Elle fut aussi dédoublée avec la JSR-330 n'offrant qu'un subset, permettant ainsi au furibond Google de continuer à offrir son propre framework, Google Guice.

CDI est à présent un acteur de premier ordre dans une application JavaEE. La v2.0 est sortie avec EE8 au printemps 2017. Spring a fini par introduire les scopes (mais moins bien) afin d'être conforme à la JSR, Seam est abandonné, et Guice est encore un peu utilisé dans le monde Android. A présent, on écrit:

java
@*Scoped
public class GithubersDAO {  
    @Resource private javax.sql.DataSource myDs;  
    // and in a method
        Connection connection = myDs.getConnection(...);  

et:

java
@*Scoped
public class GithubersService {  
    @Inject private GithubersDAO dao;

et surtout:

java
@WebServlet(urlPatterns = {"/mypath"})  
public class MyServlet extends HttpServlet {  
    @Inject  
  private GithubersService ghs; //easy peasy !!!

Sache aussi qu'on peut facilement écrire des extensions CDI, ou en trouver sur internet, la plus connue étant "Apache DeltaSpike".

↗
↗
What's new in CDI 2.0
docs.jboss.org
↗
Apache DeltaSpike CDI extensions
deltaspike.apache.org
↗
CDI @ Oracle
docs.oracle.com
↗
CDI introduction 2
dzone.com
↗
CDI introduction 1
dzone.com
↗
Injection de dépendances
fr.wikipedia.org

Mise en route sur CDI

Tout d'abord tu ne vas plus pouvoir utiliser Tomcat car ce n'est pas vraiment un Serveur d'Applications. Il va te falloir installer Wildfly (JBoss), Glassfish (Oracle, reference impl) Payara (fork de Glassfish)...voire Weblogic si tu veux avoir un avant-goût de l'enfer!

Mode opératoire:

  • Récupère le serveur "Full" de ton choix

  • Extrais-le dans /opt comme dans les quêtes IDE et Maven

  • Rajoute le chemin /opt/wildfly/bin ou /opt/payara5/bin dans ton PATH pour pouvoir appeler le CLI du serveur d'application.

  • Attention, à ce jour, Payara ne tourne pas avec une JDK > 11!

Dans le menu Run > Edit Configurations de IntelliJ, tu vas rajouter (bouton + vert) un serveur local, dire de lancer par défaut http://localhost:8080/GithubTracker/githubers, choisir un domaine par défaut, et y déployer le war explosé de ton application (soit avec une étape de build build artifacts en plus de build soit avec une seule étape maven goal contenant clean package . Avec Payara ça donnera ça, mais tu es libre d'utiliser WildFly (choisir serveur JBoss Local) ou TomEE:

Tu peux ensuite lancer l'application via le panel "Application Servers" avec les boutons Run ou Debug. Verifie que l'application tourne toujours correctement!

Un serveur d'application contient une implémentation de CDI (eg /glassfish/modules/cdi-api.jar dans Payara), mais tu vas devoir déclarer les API des JSR-299 et 330 dans ton pom.xml pour pouvoir coder avec.

xml
<!-- Injection and CDI -->  
<dependency>  
    <groupId>javax.inject</groupId>  
    <artifactId>javax.inject</artifactId>  
    <version>1</version>  
    <scope>provided</scope>  
</dependency>  
<dependency>  
    <groupId>javax.enterprise</groupId>  
    <artifactId>cdi-api</artifactId>  
    <version>2.0.SP1</version>  
    <scope>provided</scope>  
</dependency>

Bien que ce ne soit plus nécessaire en JEE7 ou JEE8, certains serveurs d'appli exigent toujours que tu créées un fchier /webapp/WEB-INF/beans.xml vide pour activer CDI. Va comprendre.

Et enfin, au lieu d'obtenir ton GithubersService et ton instance de GithuberDAO avec des new, tu vas utiliser @Inject. Il y a deux manières de faire que tu vas imédiatement expérimenter:

  • Injecte directement GithubersService comme propriété de GithubersServlet

  • Et injecte GithuberDAO (pas DumbGithiberDAO) dans GithubersService via un constructeur annoté @Inject. Au passage, tu annoteras GithubersService et DumbGithiberDAO avec @Dependent, ce qui veut dire qu'elles existeront aussi longtemps que les classes qui en dépendent (ie: GithubersServlet). Pour que l'exercice soit parlant, il ne faut pas que DumbGithiberDAO soit instancié ou référencé une seule fois dans ton code! Verifie que l'application tourne toujours correctement!

↗
Injection points
docs.jboss.org
↗
Variables d'env sous Ubuntu
help.ubuntu.com
↗
Toujours pas sûr? Voir le Filesystem Hierarchy Standard!
pathname.com
↗
Où dois-je installer mes applications ?
askubuntu.com
↗
Payara downloads
payara.fish

Challenge

Utilise CDI dans l'application GithubTracker

Pour cette quête, le challenge consite juste à vérifier que tu as su mettre en pratique toutes les consignes qui t'ont été données tout au long de cette quête.

Critères de validation

  • Toutes les classes et les annotations demandées au long de cette quête sont présentes;
  • @Inject et @Dependent sont utilisées;
  • L'application fonctionne sur un serveur Full EE sur /githubers