Tutoriel sur l'intégration de CDI, EJB et WebSocket

Image non disponible

Cet article présente l'intégration de CDI, des EJB et de WebSocket dans un même projet.

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

Article lu   fois.

Les deux auteurs

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Image non disponible

L'arrivée de Java EE 7 a permis l'introduction native des WebSocket dans nos applications Web. Ces WebSocket, côté serveur, viennent répondre aux nouveautés apportées notamment par HTML5. Cependant, un problème se pose en voulant y intégrer d'autres mécanismes JEE7.

C'est le cas notamment des EJB3 qui ne peuvent pour l'instant pas être injectés directement (constaté sur le serveur Wildfly 8.1.0 de Jboss). Cela pose évidemment un problème si l'on souhaite pouvoir accéder à un ou plusieurs services applicatifs à partir d'une classe WebSocket.

II. Rappel sur les WebSocket

Très rapidement, un WebSocket, qu'est-ce que c'est ? Le terme WebSocket désigne avant tout un protocole. Ce protocole permet au serveur et au client de dialoguer de manière bidirectionnelle.

Par exemple, le protocole HTTP est unidirectionnel. Le client interroge, le serveur répond. D'autres protocoles orientés événements vont permettre au client de « s'abonner » aux publications du serveur. L'utilisation de WebSocket permet aux deux entités de dialoguer en « full-duplex ».

Cela signifie que les intervenants ont la possibilité d'émettre à tout moment, sans devoir attendre une réponse. Ex. :

Image non disponible

Ceci permet techniquement la réalisation d'interfaces beaucoup plus réactives et dynamiques. On peut facilement envisager l'utilisation de ce protocole pour la réalisation d'un simple chat, ou même d'un jeu web où le serveur pourra lui-même forcer le client à se rafraîchir (plus de boucles while).

III. WebSocket côté JavaScript

L'implémentation de l'API WebSocket en JavaScript est assez simple et ne pose normalement pas de problème. Voici un exemple très rapide :

 
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.
29.
// on crée l'URL qui nous intéresse
_wsUri = "ws://" + (document.location.hostname == "" ? "localhost" : document.location.hostname) + ":" +(document.location.port == "" ? "8080" : document.location.port) + "/monappli_front/postWebSocket";
 
// on déclare notre object WebSocket en lui transmettant l'URI
_websocket = new WebSocket(_wsUri);
 
// puis on définit les fonctions onopen, onmessage et onerror.
// onopen sera appelé lors de l'envoi du premier message
_websocket.onopen = function(evt) {
console.log("Websocket connected ");
 
sendPost("test");
 
};
 
_websocket.onmessage = function(evt) {
console.log("WebSocket event received" );
var jsonData = jQuery.parseJSON(evt.data);
/** Ici, on place le code permettant à notre application web de se rafraîchir
**/
 
};
 
_websocket.onerror = function(evt) {
console.log("lol");
 
};
// on peut désormais communiquer via ce websocket avec la méthode send
_websocket.send(message);

IV. WebSocket Côté JEE

IV-A. Déclaration d'une classe en tant que WebSocket

À présent que le client est prêt, intéressons-nous au côté serveur. Pour déclarer une classe Java comme étant un point d'entrée WebSocket, il suffit de renseigner l'annotation @ServerEndpoint.

Ex. :

 
Sélectionnez
1.
2.
3.
4.
5.
@Singleton
@ServerEndpoint(value = "/postWebSocket", decoders = { PostDecoder.class }, encoders = { ListMarkerEncoder.class, SessionIdEncoder.class })
public class PositionWebSocket {
……
}

Ici, le point d'accès de mon WebSocket sera :

ws://localhost:8080/monApplis/postWebSocket

IV-B. Déclaration d'encoders/decoders

Les paramètres decoders et encoders vont nous permettre de désigner des classes en charge de l'encodage/décodage des messages reçus et envoyés par le serveur.

L'encodage doit être réalisé par une classe implémentant l'interface javax.websocket.Encoder :

 
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 ListMarkerEncoder implements Encoder.Text<List<Marker>>{
 
    Logger _l = Logger.getLogger(ListMarkerEncoder.class);
 
    @Override
    public void destroy() {
        // TODO Auto-generated method stub
 
    }
 
    @Override
    public void init(EndpointConfig arg0) {
        // TODO Auto-generated method stub
 
    }
 
    /**
                *Méthode appelée lors de l'envoi d'un message par le WebSocket serveur. On transforme une liste d'objets
    * Marker en chaine Json.
**/
@Override
    public String encode(List<Marker> arg0) throws EncodeException {
        JsonArray result;
        JsonArrayBuilder builder = Json.createArrayBuilder();
        // construction de la réponse au format JSON
                result = builder.build();
        return result.toString();
    }

Le décodage doit être réalisé par une classe implémentant l'interface javax.websocket.Decoder :

 
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.
public class PostDecoder implements Decoder.Text<Post>{
 
    Logger _l = Logger.getLogger(PostDecoder.class);
 
    @Override
    public void destroy() {
        // TODO Auto-generated method stub
 
    }
 
    @Override
    public void init(EndpointConfig arg0) {
        // TODO Auto-generated method stub
 
    }
 
    @Override
    public Post decode(String arg0) throws DecodeException {
        Post post = null;
        final JsonParser parser = Json.createParser(new StringReader(arg0));
        // lecture du message au format JSon
        return post;
    }

IV-C. Déclaration des méthodes du WebSocket

La partie serveur aura besoin de déclarer trois méthodes appelées lors des événements suivants :

  • nouvelle connexion ;
  • nouveau message ;
  • fermeture de connexion.

Ces événements vont être implémentés par annotations de la manière suivante :

 
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.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
  @OnOpen
public void onOpen(Session session) {
    _l.info("Websocket session created !!! session : "                           + session.getId());
    MonappliWebSocketSessionHolder sessionHolder = new
                       MonappliWebSocketSessionHolder();
    sessionHolder.set_session(session);
    _webSocketSessionHolderMap
                       .put(session.getId(), sessionHolder);
    _l.info("Actually "
                       + _webSocketSessionHolderMap.size()
                       + " existing ");
 
    try {
        session.getBasicRemote()
                       .sendObject(session);
    } catch (EncodeException e) {
        //TODO
    } catch (ClosedChannelException e) {
        //TODO
    } catch (Exception e) {
        //TODO
    }
 
}
 
/*
 * (non-Javadoc)
 *
 * @see
 * com.ccrcsoft.monappli.ws.websocket
     * .PositionWebSocket#onClose(javax.websocket.Session,
     * javax.websocket.CloseReason)
 */
@OnClose
public void onClose(Session session, CloseReason reason) {
    this.closeSession((monappliWebSocketSessionHolder)                            _webSocketSessionHolderMap
               .get(session.getId()));
    _l.info("Websocket session closed !!! reason : "
            + reason.getReasonPhrase());
}
 
@OnMessage
public String receivingPost(String post) {
    _l.info("Receiving message from outtaspace !!! post : " + post);
    return "received " + post;
}

IV-D. Premier bilan

Pour l'instant, on n'a pas encore de réelle problématique. Lors de l'affichage de la page contenant le code JavaScript, la connexion entre le client et le serveur se réalise.

Cependant, sous cette forme, le serveur n'envoie pas encore de données, à part un message lors de la connexion. Ce qui ne correspond pas vraiment à ce que l'on cherche, à savoir une communication bidirectionnelle où le serveur peut rafraîchir lui-même le client si l'envie lui prend.

On pourrait donc vouloir réaliser un pattern dans lequel un processus coté serveur s'exécuterait en boucle pour mettre à jour régulièrement le client.

Image non disponible

De plus, on aimerait bien pouvoir, à partir de la classe WebSocket, contacter des services déjà développés dans notre application.

NOTE : on part du principe que nos services sont détenus par des EJB3 (nous sommes dans un contexte Java EE 7 sans utilisation de Spring).

Hélas, l'injection (via @EJB où @Inject) d'EJB3 dans une classe déjà annotée @WebSocket n'est pas implémentée et ne fonctionne pas (sur WildFly 8.1.0).

Image non disponible

MAIS !

Les outils Java EE 7 permettent de répondre à ces deux problématiques. Il est possible d'utiliser pour cela :

  • un Timer EJB pour déclencher un processus périodique ;
  • les événements CDI pour permettre aux couches EJB de communiquer avec la classe WebSocket.

V. Pattern de communication entre EIB, CDI et WebSocket

En utilisant CDI et un Timer EJB on peut obtenir le pattern suivant :

Image non disponible

On va donc :

  • déclarer un EJB contenant une méthode annotée @Schedule. Ceci va permettre de définir une période de réactivation de l'envoi d'événements ;
  • déclarer un événement, dans une classe TimeEvent, qui sera envoyé périodiquement. Cette classe va contenir les informations à transmettre au WebSocket ;
  • Déclarer dans la classe WebSocket une méthode annotée @Observer. Cette méthode va « écouter » la production d'événements pour les traiter ensuite.

V-A. Rappel sur CDI

Pour comprendre la suite, un petit rappel sur CDI est nécessaire. CDI (Context and Dependency Injection) est une spécification Java EE 6+ qui permet notamment l'injection de dépendances par annotations au sein d'un conteneur JEE.

CDI amène également l'utilisation des mécaniques suivantes :

  • la détermination de qualificateurs permettant un typage des objets injectés ;
  • la gestion des scopes des objets injectés (session, request…) ;
  • la déclaration de méthodes @Observer qui vont « écouter » les événements d'un type donné. Cela va nous permettre notamment d'écouter les événements de notre Timer ;
  • la déclaration de méthodes @Producer qui vont être appelées lors de l'injection d'objets qualifiés (qui sont typés par un qualificateur) ;
  • et beaucoup d'autres…

Il y aurait beaucoup de choses à dire sur CDI ici, mais afin de ne pas trop digresser, je me permets de vous renvoyer vers ce lien.

V-B. Déclaration du TIMER

Et donc, revenons à nos moutons. Déclarer le Timer revient à créer un EJB contenant une méthode annotée @Schedule :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
@Stateless
public class MyEJBTimer implements EventTimer {
 
    // la période définie ici envoie un événement toutes les 3        // 0 secs
    @Schedule(second="*/30", minute="*", hour="*")
    public void produceWebSocketTimeEvent(Timer t){
        // ma cla
        TimeEvent event = new TimeEvent();
        event.set_value(new Date());
        // production de l'évènement
        _timeEvent.fire(event);
    }
 
    // Objet de la classe javax.enterprise.event.Event qui per        // met de gérer mes TimeEvent
    @Inject @TimeEventQual Event<TimeEvent> _timeEvent;
 
}

On crée donc une méthode qui va, toutes les 30 secondes, envoyer un événement TimeEvent.

Les annotations CDI @Inject et @TimeEventQual signifient que _timeEvent va être injecté et initialisé par CDI et qu'il sera qualifié par @TimeEventQual.

Pour déclarer le qualificateur @TimeEventQual, il faut créer une annotation qui contiendra le code suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// définit l'annotation comme un qualificateur
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
// l'annotation peut être appliquée aux méthodes, aux champs et au// x paramètres
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.PARAMETER,ElementType.TYPE})
public @interface TimeEventQual {
 
}

La classe TimeEvent peut être un simple POJO :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
package com.ccrcsoft.geovote.common.util.event;
 
import java.util.Date;
 
    public class TimeEvent extends Event<Date> {
      ....
    }

V-C. Déclaration de l'Observer

Enfin, dans notre classe WebSocket, nous pouvons déclarer une méthode @Observer :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
public void onTimeEvent(@Observes @TimeEventQual TimeEvent eventt) {
            _l.info("Nouveau timer event reçu!!!!! ");
            try {
                  session.getBasicRemote().sendObject(“Nouvel evènement!!!!”);
            } catch (Exception e) {
            }
}

On note l'utilisation des annotations CDI @Observes et @TimeEventQual. @Observes définit notre méthode onTimeEvent comme la cible d'un événement de type @TimeEventQual.

Ce qui nous permet donc de communiquer de manière périodique les informations souhaitées aux clients. De plus, notre Timer étant un EJB nous avons accès aux autres services de l'application et pouvons donc transmettre via des événements les données souhaitées.

VI. Conclusion

On pourrait dire que pour exploiter complètement JEE, il est primordial d'avoir une vision d'ensemble sur ce que les différents frameworks du standard ont à offrir. La création de WebSocket, de services Rests (pas présentée ici), d'EJB3 de CDI nous permet de réaliser des patterns extrêmement puissants (sans compter Java 8) à condition d'avoir une idée de comment les utiliser. Concrètement, j'ai trouvé cette technique sur le site suivant après avoir essuyé plusieurs échecs à vouloir implémenter de fumeuses solutions moi-même. Donc, pour finir, n'oublions jamais de regarder ce qui se fait ailleurs.

NOTE : CDI est aujourd'hui implémenté jusqu'à la version 1.2 (Weld 2.2), la version 2.0 est spécifiée, mais pas encore implémentée.

VII. Remerciements

Cet article a été publié avec l'aimable autorisation de la société SoatSoat.

Nous tenons à remercier f-leb pour sa relecture attentive de cet article et milkoseck 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 Renaud MALDONADO (soat). 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.