Accueil Nos publications Blog Intégrer CDI, EJBs et WEBSOCKET dans un projet

Intégrer CDI, EJBs et WEBSOCKET dans un projet

illuIntegrationCDI_EJB

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). Ceci pose évidemment un problème si l’on souhaite pouvoir accéder à un ou plusieurs services applicatifs à partir d’une classe WebSocket.

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 ».

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

illu1

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).

 

WEBSOCKET COTE 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 :

 


// 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 de 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);

 

WEBSOCKET coté JEE

Déclaration d’une classe en tant que WebSocket

A 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 :


@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”

 

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 :

 


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 :

 


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;
}

 

Déclaration des méthodes du WebSocket

La partie serveur aura besoin de déclarer 3 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 :

 


        @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;
}

 

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.

 

periodicCom

 

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).

vader

MAIS !

Les outils Java EE 7 permettent de répondre à ces 2 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 EJBs de communiquer avec la classe WebSocket.

 

Pattern de communication entre EJB, CDI et WebSocket

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

 

comuCDIEJB

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.

 

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éthode @Observer qui vont “écouter” les évènements d’un type donné. Ceci va nous permettre notamment d’écouter les évènements de notre Timer.
  • la déclaration de méthode @Producer qui vont être appelée 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 envoyer vers ce lien.

 

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 :

 


@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 :

 


// 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 :

 


package com.ccrcsoft.geovote.common.util.event;

import java.util.Date;

    public class TimeEvent extends Event<Date> {
      ....
    }

 

Déclaration de l’observer

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

 


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.

 

Conclusion

On pourrait dire que pour exploiter complètement JEE, il est primordial d’avoir une vision d’ensemble sur ce que les différends 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.