Java EE 7 e WebSocket (con WildFly e AngularJS) – Parte 1

Per rendere i nostri applicativi web sempre più reattivi, la gestione asincrona degli eventi è fondamentale. WebSocket può essere una soluzione, essendo una tecnologia che possiamo ormai considerare consolidata. Non è una novità nemmeno per il vecchio e caro mondo Java, che ci ha sempre provato con l'”architettura” Comet, e soprattutto con Java EE 7.

Meglio tardi che mai

La piattaforma Java Enterprise ci mette sempre un po’ di tempo prima di recepire i fermenti del mondo della programmazione: i processi di standardizzazione sicuramente richiedono tempo (e pazienza da parte nostra).

Anche noi di CoseNonJaviste ci siamo presi il nostro tempo prima di affrontare l’argomento, visto che tra poco la piattaforma Java EE 7 spegnerà la seconda candelina. Di fatto però, di Application Server certificati Java EE 7 non se ne trovano molti in giro, quindi l’attesa è giustificabile 😉 . Secondo wikipedia, si contano su una mano: Oracle con GlassFish 4, Red Hat con WildFly 8 e 9 e JEUS 8 di TmaxSoft (IBM non pervenuta al momento…). Se si considera che WildFly 8.2 è uscito a novembre 2014 e la versione 9 al momento è in CR2, forse è proprio adesso il momento giusto per parlare di WebSocket in WildFly 8.2 da poter usare in produzione!

E i browser? Posso davvero usare WebSocket?

Ancora una volta ci viene in aiuto Can I Use:

Can I Use WebSocket?

L’unica cosa che dobbiamo tenere sott’occhio è come al solito è Internet Explorer, e in particolare le versioni 8 e 9. Diciamo che la 8 non la supporta più nemmeno Google, quindi perché dovremmo farlo noi? Dite che un vostro cliente la vuole e la usa ancora? Di solito rispondo così: “hai voluto gli applicativi web, comincia ad usare un browser che li supporta. Mi stai chiedendo una di fare una Ferrari a metano, è questo che vuoi?!?”.

L’unico vero ostacolo è quindi IE 9: secondo Net Market Share, l’uso di IE 9 a livello mondiale ad Aprile 2015 era dell’8,10%.

Netmarketshare Aprile 2015

Colpisce invece un 16,05% su IE 8: possibile che siano ancora così tanti Windows XP in circolazione?

Una volta rassicurati da numeri e statistiche, cominciamo ad usare le WebSocket con l’animo in pace!

Un po’ di teoria

Cerchiamo di capire prima in cosa consiste WebSocket, indipendentemente dalla tecnologia implementativa. Per definizione, WebSocket fornisce una comunicazione bi-direzionale, full-duplex e real-time tra client e server, tre presunte buzzword che in realtà significano:

bi-direzionale
la comunicazione tra client e server può avvenire sia in un senso che nell’altro. Il client sarà quello che inizializza la comunicazione con il server, dopodiché assumeranno un ruolo “peer”, alla pari, dove entrambe le parti potranno prendere l’iniziativa di mandare messaggi e saranno in ascolto a messaggi in arrivo. La connessione avviene a livello TCP ed è sempre la stessa, indipendentemente dal numero di messaggi scambiato (a differenza dell’HTTP dove ogni richiesta/risposta ne apre una nuova), riducendo l’overhead degli header HTTP e promettendo maggior scalabilità.
full-duplex
la comunicazione bi-direzionale non è bloccante, ovvero il client può mandare un messaggio al server (o viceversa) senza l’attesa della risposta, permettendo quindi un tipo di comunicazione che si può definire real-time (dal punto di vista della UX).

Come funziona in realtà? Il protocollo WebSocket è fatto da due fasi:

Handshake
il client invia una richiesta di “Upgrade” del protocollo HTTP (supportata da HTTP 1.1) dove si chiede di cambiare protocollo di comunicazione, come per esempio:

Request Headers
Connection: Upgrade
Host: localhost:8080
Origin: http://localhost:8080
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: B33IXLTDWePjYuUIoa4UpQ==
Sec-WebSocket-Version: 13
Upgrade: websocket

Il server risponde con codice HTTP 101 se supporta l’upgrade:

Response Headers
Status Code: 101 Switching Protocols
Connection: Upgrade
Origin: http://localhost:8080
Sec-WebSocket-Accept: Tuc3BQtHx2yeyN3D7Ge8k8+epjY=
Sec-WebSocket-Location: ws://localhost:8080/socket/echo-message
Upgrade: WebSocket

Paradossalmente, anche un codice 200 non va bene 🙂 .
Data Transfer
a questo punto i nodi “peer” della connessione si possono scambiare messaggi in modo bi-direzionale, senza attendersi una risposta reciproca. Soltanto quando uno dei due decide di chiudere la connessione, allora si ha una risposta di conferma dall’altra parte.

Di seguito quindi vediamo come implementare uno scenario classico in cui una pagina web richiede un’operazione time consuming al server, aprendo una WebSocket, il quale risponderà con un messaggio verso il client dopo che l’operazione sarà conclusa. Qualcosa del tipo:

Time consuming operation with websocket

Onestamente questo credo sia uno dei tanti scenari piuttosto realistici in cui le WebSocket possano essere impiegate: capire quindi tutti gli attori in gioco e i loro scope è fondamentale per non avere strane sorprese a runtime.

E’ disponibile un progetto di esempio su GitHub ed è ospitato anche su OpenShift per vedere come comporta: si tratta di una semplice web application in Java e AngularJS testata su WildFly 8.2. Per eseguire l’operazione time consuming in modo asincrono rispetto al messaggio di richiesta vengono usate due strategie, una in puro stile Java EE 6, l’altra con l’aiuto di RxJava e le Concurrency Utils di Java EE 7:

  • Sono stati usati gli EJB Asincroni per gestire l’elaborazione in un nuovo thread, mentre per notificare la fine dell’operazione gli Eventi CDI: l’endpoint starà in ascolto all’evento, il quale indicherà su quale sessione WebSocket indirizzare la risposta.
  • Con RxJava è possibile sottoscrivere un subscriber ad un observable eseguito su un nuovo thread, grazie alla nuova API per la gestione della concorrenza in Java EE 7. Questo esempio verrà affrontato nei prossimi post: come vedremo, semplificheranno notevolmente il codice rispetto alla prima soluzione.

Il progetto è stato creato a partire dall’archetipo per WildFly 8.2. Per la compilazione delle WebSocket però è necessario aggiungere la seguente dipendenza maven:

<dependency>
   <groupId>org.jboss.spec.javax.websocket</groupId>
   <artifactId>jboss-websocket-api_1.0_spec</artifactId>
   <version>1.0.0.Final</version>
   <scope>provided</scope>
</dependency>

Il server

La nuova specifica Java API for WebSocket 1.0 (JSR 356) introdotta in Java EE 7, di cui WildFly è implementazione certificata, definisce:

  • WebSocket Endpoint: punto di ingresso di un servizio del server esposto come WebSocket.
  • Annotations: una serie di annotazioni per definire l’endpoint e gli eventi di callback per la gestione della WebSocket:
    • @ServerEndpoint: definisce l’url di connessione alla socket e altri parametri di configurazione.
    • @onOpen: callback chiamata all’apertura della connessione.
    • @onMessage: callback che gestisce tutti i messaggi in arrivo.
    • @onError: gestisce le situazioni di errore sul server.
    • @onClose: callback chiamata alla chiusura della connessione.
  • WebSocket Session: interfaccia di gestione della sessione WebSocket. Permette di reperire le informazioni sulla sessione, interagire con il client, monitorare lo stato della connessione e così via.
  • Tipologia di messaggi: è possibile scambiare testo, stream di byte o oggetti java come singoli messaggi o sequenza di messaggi parziali (per gli stream in particolar modo).

Vediamo quindi subito un esempio di codice che implementa lo scenario del sequence diagram:

@ServerEndpoint(value = "/socket/echo-message")
public class EchoMessageWebSocket {

    private static final Logger LOGGER = Logger.getLogger(EchoMessageWebSocket.class.getName());

    @Inject
    private SessionHolder sessionHolder;

    @Inject
    private EchoMessageService messageService;

    @OnOpen
    public void onOpen(Session session) {
        sendLog(session, logWithDetails("Session " + session.getId() + " connected"));
        sessionHolder.add(session);
    }

    @OnMessage
    public String onMessage(String message, Session session) throws InterruptedException {
        // ... maggiori dettagli più avanti
    }

    @OnClose
    public void onClose(CloseReason reason, Session session) {
        LOGGER.info(logWithDetails("Session " + session.getId() + " disconnected"));
        LOGGER.info("Close code: " + reason.getCloseCode() + "; close phrase: " + reason.getReasonPhrase());
        sessionHolder.remove(session);
    }

    @OnError
    public void onError(Throwable error, Session session) throws IOException, EncodeException {
        LOGGER.log(Level.SEVERE, error.getMessage(), error);
        if (session.isOpen()) {
            session.getBasicRemote().sendText(error.getMessage());
        }
    }

    public void onEchoMessage(@Observes Message message) throws IOException {
        // ... maggiori dettagli più avanti
    }

    private String logWithDetails(String message) {
        return message + " on thread " + Thread.currentThread().getId() + " and bean hash " + this.hashCode();
    }

    private void sendLog(Session session, String log) throws IOException, EncodeException {
        LOGGER.info(log);
        session.getBasicRemote().sendObject(new Message(log, session.getId()));
    }
}

L’annotazione principale è @ServerEndpoint: definisce l’indirizzo al quale verrà aperta la socket. Le altre annotazioni evidenziate hanno un nome molto eloquente: da notare invece gli argomenti che “arrivano” ai metodi annotati: sia nel caso di apertura (@OnOpen) o chiusura (@OnClose) della socket, sia all’arrivo di un messaggio (@OnMessage) o in caso di errore del server (@onError), è sempre disponibile l’oggetto Session, che permette di identificare il chiamante in modo univoco ed interagire con esso.

Nel metodo che gestisce l’evento di ricezione del messaggio (annotato con @OnMessage), è ovviamente disponibile anche il testo del messaggio, in questo caso in formato stringa.

    @OnMessage
    public String onMessage(String message, Session session) throws InterruptedException {
        Message msg = new Message();
        msg.setMessage(message);
        msg.setSessionId(session.getId());

        messageService.echo(msg);

        String response = "Message '" + message + "' received";
        LOGGER.info(logWithDetails(response));
        return response;
    }

Il metodo delega l’esecuzione dell’operazione time consuming ad un EJB Asincrono (EchoMessageService), in modo da rendere asincrono l’avvio dell’operazione time consuming con la sua esecuzione. Nell’oggetto Message passato all’EJB, oltre che al testo, viene salvato anche l’id della sessione websocket in modo da ritrovare il client giusto a cui rispondere. In questo caso, il metodo onMessage ritorna immediatamente un messaggio al client (response), ma poteva anche non restituire niente (void sulla firma del metodo).

Al termine del metodo dell’EJB, verrà lanciato un evento CDI:

@Stateless
public class EchoMessageService {

    @Inject
    private Event<Message> echoComplete;

    @Asynchronous
    public void echo(Message message) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        message.setMessage("Echo " + message.getMessage());
        echoComplete.fire(message);
    }
}

a cui è in ascolto (grazie all’annotazione @Observes) il metodo onEchoMessage(@Observes Message message) di EchoMessageWebSocket. Dal messaggio sarà recuperato l’id della sessione a cui rispondere:

public void onEchoMessage(@Observes Message message) throws IOException {
   Optional<Session> session = sessionHolder.get(message.getSessionId());
   if (session.isPresent()) {
      sendLog(session.get(), logWithDetails("Sending response to session " + session.get().getId()));
      session.get().getBasicRemote().sendText(message.getMessage());
   } else {
      LOGGER.severe("Session " + message.getSessionId() + " not present or already closed!");
   }
}

L’oggetto SessionHolder restituisce un Optional: visto che siamo in Java 8 sfruttiamo gli Optional, come ci ha insegnato Giampaolo tempo addietro!

Apparte il dettaglio di programmazione, il messaggio testuale di risposta al client viene inviato tramite il metodo sendText sulla sessione corretta.

Gli scope

Da alcune prove con WildFly (non è detto che sia da specifica, potrebbe essere un dettaglio implementativo), emerge che l’endpoint del servizio non è associabile a nessuno scope CDI: accade invece che viene creata un’istanza dell’endpoint per ogni connessione aperta: se vogliamo quindi avere accesso a tutte le connessioni aperte è necessario appoggiarsi ad un bean @ApplicationScoped come l’oggetto SessionHolder creato ad hoc per l’esempio, che contiene una mappa delle sessioni aperte.

E’ così che il metodo onEchoMessage, in ascolto all’evento CDI, recupera la sessione websocket giusta a cui rispondere.

Attenzione però: l’istanza di EchoMessageWebSocket in cui viene eseguito il metodo non è la stessa della websocket aperta (come farebbe effettivamente a ritrovarla?), ma è gestita dal contesto CDI: dato che EchoMessageWebSocket non ha nessuna annotazione CDI, essa assumerà lo scope @Default, ovvero viene ricreata da CDI ad ogni chiamata al metodo onEchoMessage. Provando ad inviare un messaggio al server, dai log dell’interfaccia si vede infatti che:

Websocket client log

Senza scendere nel dettaglio dell’interfaccia (che è semplice e che comunque affronteremo nel prossimo post), diamo un’occhiata a cosa succede inviando un messaggio al server. Gli eventi di apertura, ricezione del messaggio e chiusura della connessione avvengono tutti sullo stesso oggetto con hash 23253379, e, come c’era da aspettarselo, su thread diversi (anche se dello stesso thread pool). L’invio della risposta (quello gestito dall’evento CDI), invece è su un thread pool a sé stante (a livello EJB), eseguito quindi su un nuovo thread e su una nuova istanza di EchoMessageWebSocket (con hash 28899252).

Che significa quindi? Che i metodi di callback gestiti da WebSocket saranno sempre eseguiti su un’istanza diversa di EchoMessageWebSocket rispetto all’esecuzione di onEchoMessage!! Questo perché abbiamo mischiato WebSocket con CDI! Grazie però all’oggetto SessionHolder possiamo aggirare il problema e recuperare la sessione a cui rispondere.

Finora abbiamo visto come è possibile ricevere messaggi testuali via WebSocket: fortunatamente è possibile passare anche degli oggetti, basta dire al server come codificarli e decodificarli.

Decoders e Encoders

Se proviamo a cambiare la firma del metodo che riceve i messaggi in:

@OnMessage
public Message onMessage(Message msg, Session session) {
...
}

Ovvero degli oggetti in input e output, il compilatore si arrabbia e ci invita a definire un decoder e un encoder per sapere come gestirli. Questi andranno specificati nell’annotation @ServerEndpoint così:

@ServerEndpoint(value = "/socket/echo-message", encoders = MessageEncoder.class, decoders = MessageDecoder.class)
public class EchoMessageWebSocket {
...
}

Ma come sono fatte queste classi? Dal momento che con WebSocket si possono inviare e ricevere messaggi in formato testo o binario, sia “one-shot” che come a blocchi, come stream di dati, sono disponibili nella specifica diverse interfacce da implementare. Basta quindi implementare una delle sotto-interfacce di javax.websocket.Encoder e javax.websocket.Decoder a seconda se si voglia elaborare del testo, dei dati binari, sia che sia una comunicazione in un unico blocco o a stream.

Per esempio, nel caso del nostro oggetto Message, il relativo encoder sarà:

public class MessageEncoder implements Encoder.Text<Message> {

    @Override
    public String encode(Message message) throws EncodeException {
        return Json.createObjectBuilder()
                .add("message", message.getMessage())
                .add("sessionId", message.getSessionId())
                .build().toString();
    }

    @Override
    public void init(EndpointConfig endpointConfig) {
    }

    @Override
    public void destroy() {
    }
}

Per trasformare un oggetto in una stringa JSON, in Java esistono innumerevoli modi, come per esempio il mitico Google Gson. Finalmente però in Java EE 7 si sono decisi a inserire una API nativa per la gestione dei JSON: con una buona interfaccia fluente, è possibile creare una stringa in formato JSON a partire dal nuovo oggetto-builder Json.
L’archetipo con cui è stato creato il progetto di demo per WildFly 8.2 non dispone della dipendenza per poter compilare questo codice: è quindi necessario includere la dipendenza Maven alla specifica JSR-353

<dependency>
   <groupId>org.jboss.spec.javax.json</groupId>
   <artifactId>jboss-json-api_1.0_spec</artifactId>
   <version>1.0.0.Final</version>
   <scope>provided</scope>
</dependency>

Vi lascio immaginare come sarà il decoder, che implementa l’interfaccia Decoder.Text.

Per inviare un messaggio di tipo oggetto infine, dovrà essere usato il metodo sendObject invece di sendText, come per esempio nel metodo che sta in ascolto ai messaggi arrivati:

public void onEchoMessage(@Observes Message message) throws IOException, EncodeException {
   Optional<Session> session = sessionHolder.get(message.getSessionId());
   if (session.isPresent()) {
      LOGGER.info(logWithDetails("Sending response to session " + session.get().getId()));
      session.get().getBasicRemote().sendObject(message);
   } else {
      LOGGER.severe("Session " + message.getSessionId() + " not present or already closed!");
   }
}

Gestione dell’errore lato server

Fin’ora abbiamo sottovalutato la gestione delle eccezioni durante la comunicazione. Può capitare infatti che il json che arriva al server non sia decodificabile, o si verificano altri errori a runtime durante l’elaborazione del messaggio che devono essere notificati in qualche modo al client. Come fare quindi? La minima cosa da fare è annotare un metodo con @OnError:

@OnError
public void onError(Throwable error, Session session) throws IOException, EncodeException {
   LOGGER.log(Level.SEVERE, error.getMessage(), error);
   if (session.isOpen()) {
      session.getBasicRemote().sendObject(new Message(error.getMessage(), session.getId()));
   }
}

Il metodo onError riceve l’errore e la sessione nella quale si è verificato l’errore, semplificandone la gestione. In questo modo, previa verifica di connessione ancora aperta, è possible inviare l’eccezione al client in modo che capisca che qualcosa è andato storto.

Dal momento che con WebSocket non possiamo avvalerci della semantica dei codici http, il client come fa a capire che ha ricevuto un messaggio di errore? E’ necessario quindi definire un “protocollo di comunicazione” chiaro e semantico in modo che il client riesca a riconoscere la natura del messaggio che riceve e agire di conseguenza. In poche parole, potremmo aggiungere, per esempio, un attributo status all’oggetto Message in modo da definire la natura del messaggio e permettere al client di agire di conseguenza. Nel progetto di esempio, se qualcosa va storto sul server, i messaggi a video vengono colorati di rosso.

Conclusioni

Convinti dai numeri sui browser più usati, è l’ora quindi di usare WebSocket senza timori. L’implementazione della specifica lato server è estremamente semplice e ricalca gli eventi che vedremo lato client nel prossimo post. Basta quindi includere un paio di dipendenze Maven in più rispetto all’archetipo, ovvero WebSocket API (JSR-386) e JSON API (JSR-353), e WildFly è già pronto ad eseguire la vostra nuova WebSocket.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+