If your application has real-time requirements like within a chat, the WebSocket technology might be a good fit. With WebSockets, you can create two-way communication channels between a server and a client. The JSR 356 specification defines a standard way of creating and managing WebSocket clients and servers for Java. It's also part of Jakarta EE as Jakarta EE WebSocket with the current version 1.1. Learn how to use the Jakarta EE WebSocket specification within this blog post while writing a small stock exchange application.
Jakarta EE 8 project setup
To create the sample application I make use of one of my Maven Archetypes. The Maven project uses Java 11 and the Jakarta EE platform dependency with version 8.0.0:
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 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.rieckpil.blog</groupId> <artifactId>websockets-with-jakarta-ee</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <jakarta.jakartaee-api.version>8.0.0</jakarta.jakartaee-api.version> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-api</artifactId> <version>${jakarta.jakartaee-api.version}</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>websockets-with-jakarta-ee</finalName> <!-- further plugins --> </build> </project> |
Next, I'm using Open Liberty for the runtime. The server.xml
to configure Open Liberty is straightforward:
1 2 3 4 5 6 7 8 9 10 11 | <?xml version="1.0" encoding="UTF-8"?> <server description="new server"> <featureManager> <feature>javaee-8.0</feature> </featureManager> <httpEndpoint id="defaultHttpEndpoint" httpPort="9080" httpsPort="9443"/> <quickStartSecurity userName="duke" userPassword="dukeduke"/> </server> |
As we are using our own server configuration, the open-liberty:kernel-java11
Docker image fits perfect for the base image:
1 2 3 | FROM open-liberty:kernel-java11 COPY --chown=1001:0 target/websockets-with-jakarta-ee.war /config/dropins/ COPY --chown=1001:0 server.xml /config |
Creating a WebSocket endpoint
We have two options to create WebSocket endpoints. First, we can create them programmatically while extending the javax.websocket.Endpoint
class. Second, we can use the @ServerEndpoint
annotation.
For this example, I'm using the approach with annotations. This allows us to use further annotations like @OnOpen
, @OnMessage
, etc. on methods of our endpoint class. With these we can define how to handle the different lifecycle phases of establishing a connection, sending a message, and closing the connection:
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 | @ServerEndpoint(value = "/stocks") public class StockExchangeEndpoint { private static Set<Session> sessions = new HashSet<>(); public static void broadcastMessage(String message) { for (Session session : sessions) { try { session.getBasicRemote().sendObject(message); } catch (IOException | EncodeException e) { e.printStackTrace(); } } } @OnOpen public void onOpen(Session session) { System.out.println("WebSocket opened: " + session.getId()); sessions.add(session); } @OnMessage public void onMessage(String message, Session session) { System.out.println("Stock information received: " + message + " from " + session.getId()); try { session.getBasicRemote().sendObject(message); } catch (IOException | EncodeException e) { e.printStackTrace(); } } @OnError public void onError(Session session, Throwable throwable) { System.out.println("WebSocket error for " + session.getId() + " " + throwable.getMessage()); } @OnClose public void onClose(Session session, CloseReason closeReason) { System.out.println("WebSocket closed for " + session.getId() + " with reason " + closeReason.getCloseCode()); sessions.remove(session); } } |
In the example above, I've added a method for each of these lifecycle phases. As we want to broadcast messages to all of our subscribers, later on, we can store all active sessions in a Set
. Once a message arrives at our WebSocket endpoint, I'm replying with the same message to the sender using session.getBasicRemote().sendObject(message)
.
With this setup, our WebSocket endpoint is now available at ws://localhost:9080/stocks
and waiting for clients to connect.
Adding support to encode and decode JSON with Jakarta EE
The specification defines only two default data formats for WebSocket messages: Text and Binary. Fortunately, Jakarta EE WebSocket allows us to add custom decoders and encoders for our messages. To demonstrate this, I'll create a decoder and encoder for JSON based messages.
Creating a custom encoder is done by implementing either Encoder.Text<T>
or Encoder.Binary<T>
(or their streaming equivalents). A naive text-based JSON encoder might look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class JSONTextEncoder implements Encoder.Text<JsonObject> { @Override public String encode(JsonObject object) throws EncodeException { return object.toString(); } @Override public void init(EndpointConfig config) { } @Override public void destroy() { } } |
Writing a custom decoder works similarly. You just have to implement one additional method willDecode(String s)
to determine whether or not your decoder is able to decode an incoming message. To determine this, I'm trying to parse the incoming message to a JSON object (there might be better solutions):
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 | public class JSONTextDecoder implements Decoder.Text<JsonObject> { @Override public JsonObject decode(String s) throws DecodeException { try (JsonReader jsonReader = Json.createReader(new StringReader(s))) { return jsonReader.readObject(); } } @Override public boolean willDecode(String s) { try (JsonReader jsonReader = Json.createReader(new StringReader(s))) { jsonReader.readObject(); return true; } catch (JsonException e) { return false; } } @Override public void init(EndpointConfig config) { } @Override public void destroy() { } } |
Once your custom decoder and encoder are ready to use, you have to configure them in your server endpoint like the following:
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 | @ServerEndpoint(value = "/stocks", decoders = {JSONTextDecoder.class}, encoders = {JSONTextEncoder.class}) public class StockExchangeEndpoint { public static void broadcastMessage(JsonObject message) { for (Session session : sessions) { try { session.getBasicRemote().sendObject(message); } catch (IOException | EncodeException e) { e.printStackTrace(); } } } @OnMessage public void onMessage(JsonObject message, Session session) { System.out.println("Stock information received: " + message + " from " + session.getId()); try { session.getBasicRemote().sendObject(message); } catch (IOException | EncodeException e) { e.printStackTrace(); } } // the rest stays the same } |
With this configuration, we can now accept JsonObject
as a message type for our @OnMessage
method and also send JsonObject
data to our clients.
Connecting to the WebSocket endpoint using Jakarta EE
If you want to connect to a WebSocket endpoint from a different Jakarta EE application, you can create a class with @ClientEndpoint
. Within this class you can now use the same lifecycle annotations that you've already seen in the server endpoint (@OnMessage
, etc.):
1 2 3 4 5 6 7 8 | @ClientEndpoint(decoders = {JSONTextDecoder.class}, encoders = {JSONTextEncoder.class}) public class StockExchangeClient { @OnMessage public void processMessageFromStockExchangeServer(JsonObject message) { System.out.println("Message came from the Stock Exchange server: " + message); } } |
Also, make sure to add your custom decoders and encoders.
Next, to connect the client to an actual endpoint, you can use the WebSocketContainer
class and connect to a given URL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class StockExchangeNotifier { private Session session; @PostConstruct public void init() { WebSocketContainer container = ContainerProvider.getWebSocketContainer(); try { this.session = container.connectToServer(StockExchangeClient.class, new URI("ws://localhost:9080/stocks")); } catch (DeploymentException | IOException | URISyntaxException e) { e.printStackTrace(); } } } |
In this example, as I'm printing each client connection and the incoming message to the client, the output looks like the following:
1 2 3 | WebSocket opened: N9Im5dyQIbC_itLvFqmsWM- Message came from the Stock Exchange server: {"stock":"DKE-42","price":167.91} Message came from the Stock Exchange server: {"stock":"DKE-42","price":142.98} |
Furthermore, I'm using an EJB timer to broadcast a new WebSocket message every five seconds:
1 2 3 4 5 6 7 8 9 10 11 | @Schedule(second = "*/5", minute = "*", hour = "*", persistent = false) public void sendNewStockExchangeInformation() { JsonObject stockInformation = Json.createObjectBuilder() .add("stock", "DKE-42") .add("price", new BigDecimal(ThreadLocalRandom.current() .nextDouble(250.00)) .setScale(2, RoundingMode.DOWN)) .build(); StockExchangeEndpoint.broadcastMessage(stockInformation); } |
Connecting to the WebSocket endpoint with JavaScript
In addition, we can now connect to our WebSocket endpoint using JavaScript. The WebSocket standard API is part of every browser and can be used right out-of-the-box.
For a short demo, I'm creating a frontend to connect to the endpoint, send messages with a form and display all incoming messages like the following:
Lastly, the JavaScript part to make this work is the following:
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 | const webSocket = new WebSocket('ws://localhost:9080/stocks'); webSocket.onerror = function (event) { onError(event) }; webSocket.onopen = function (event) { onOpen(event) }; webSocket.onmessage = function (event) { onMessage(event) }; function onMessage(event) { const eventPayload = JSON.parse(event.data); document.getElementById('stockInformation').innerHTML += `<tr><td>${eventPayload.stock}</td><td>${eventPayload.price} $</td></tr>`; } function onOpen(event) { document.getElementById('connectionMessage').innerHTML = 'Connection established'; } function onError(event) { alert('An error occurred:' + event.data); } function send() { const payload = { 'stock': document.getElementById('stockName').value, 'price': document.getElementById('stockPrice').value }; webSocket.send(JSON.stringify(payload)); } |
You can find the HTML markup for this example on GitHub.
Have fun writing applications with Jakarta EE WebSocket,
Phil