I. Introduction▲
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. :
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 :
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. :
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 :
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 :
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 :
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.
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).
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 :
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 :
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 :
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 :
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 :
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.