How to build an application using the JakartaEE WebSocket API

How to build an application using the JakartaEE WebSocket API

jakartaee_websocket_demo

Here in this article we will try to use JakartaEE WebSocket API to build a real time data feed application providing customer with price and volume data.

Test Environment

  • OpenJDK 17.0.13
  • Eclipse GlassFish 7.0.20
  • Apache Maven 3.9.1
  • VSCode Editor 1.97.2

What are WebSockets

WebSockets are a communication protocol that enables full-duplex, real-time communication between a client and a server over a single, persistent TCP connection, unlike traditional HTTP requests which are unidirectional and require a new connection for each request.

NOTE: This demo uses sample examples from Using the Tutorial Examples Jakarta EE 10 tutorial and customized to build and deploy to custom glassfish server installation location.

If you are interested in watching the video. Here is the YouTube video on the same step by step procedure outlined below.

Procedure

Step1: Ensure the following Pre-requisite installed

Ensure that you have JDK, Maven and Glassfish Enterprise application server installed on your workstation.

admin@fedser:~$ javac --version
javac 17.0.13

admin@fedser:~$ mvn --version
Apache Maven 3.9.1 (Red Hat 3.9.1-3)

Download and install the Glassfish Enterprise Application server. Also ensure that your PATH variable is set to the bin directory of the glassfish application server.

admin@fedser:~$ export PATH=$PATH:/home/admin/middleware/stack/glassfish7/bin

Update your glassfish server adminconsole user “admin” password by editing the following file.

admin@fedser:~$ cat /home/admin/middleware/stack/glassfish7/glassfish/domains/password.properties 
#AS_ADMIN_PASSWORD=adminadmin
AS_ADMIN_PASSWORD=admin@1234

Ensure that the Glassfish application server is up and running.

admin@fedser:~$ asadmin start-domain
Waiting for domain1 to start .........
Waiting finished after 8,101 ms.
Successfully started the domain : domain1
domain  Location: /home/admin/middleware/stack/glassfish7/glassfish/domains/domain1
Log File: /home/admin/middleware/stack/glassfish7/glassfish/domains/domain1/logs/server.log
Admin Port: 4,848
Command start-domain executed successfully.

Validate if you able to access the Admin Console page for the Glassfish Server.

URL: http://localhost:4848/common/index.jsf

Step2: Create a maven jakartaee minimal project

Here we will create a maven archetype project using the CLI with the below mentioned options to generate a minimal jakartaee version 10 project.

admin@fedser:vscodeprojects$ mvn archetype:generate -DarchetypeArtifactId="jakartaee10-minimal" -DarchetypeGroupId="org.eclipse.starter" -DarchetypeVersion="1.1.0" -DgroupId="com.stack" -DartifactId="websocketdemo"

admin@fedser:vscodeprojects$ cd websocketdemo/
admin@fedser:servletadvdemo$ rm -rf src/main/java/com/stack/websocketdemo/*
admin@fedser:servletadvdemo$ rm -rf src/main/resources/*

Step3: Create a Websocket endpoint

Here we are going to implement server websocket endpoint using ETFEndpoint class, which stores all connected sessions in a queue and provides a method that the enterprise bean calls when there is new information available to send. The lifecycle methods of the endpoint add and remove sessions to and from the queue.

admin@fedser:websocketdemo$ cat src/main/java/com/stack/websocketdemo/ETFEndpoint.java 
package com.stack.websocketdemo;

import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;

@ServerEndpoint("/dukeetf")
public class ETFEndpoint {

    private static final Logger logger = Logger.getLogger("ETFEndpoint");

    /* Queue for all open WebSocket sessions */
    static Queue<Session> queue = new ConcurrentLinkedQueue<>();

    /* PriceVolumeBean calls this method to send updates */
    public static void send(double price, int volume) {
        String msg = String.format("%.2f / %d", price, volume);
        try {
            /* Send updates to all open WebSocket sessions */
            for (Session session : queue) {
                session.getBasicRemote().sendText(msg);
                logger.log(Level.INFO, "Sent: {0}", msg);
            }
        } catch (IOException e) {
            logger.log(Level.INFO, e.toString());
        }
    }

    @OnOpen
    public void openConnection(Session session) {
        /* Register this connection in the queue */
        queue.add(session);
        logger.log(Level.INFO, "Connection opened.");
    }

    @OnClose
    public void closedConnection(Session session) {
        /* Remove this connection from the queue */
        queue.remove(session);
        logger.log(Level.INFO, "Connection closed.");
    }

    @OnError
    public void error(Session session, Throwable t) {
        /* Remove this connection from the queue */
        queue.remove(session);
        logger.log(Level.INFO, t.toString());
        logger.log(Level.INFO, "Connection error.");
    }
    
}

Step3: Create Enterprise Bean class

The enterprise bean uses the timer service to generate new price and volume information every second. The enterprise bean calls the send method of the ETFEndpoint class in the timeout method.

  • @PostConstruct: This annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization. This method MUST be invoked before the class is put into service.
  • @Singleton: This annotation marks a session bean as a singleton, meaning that only one instance of the bean exists for the entire application lifecycle.
  • @Startup: This annotation, when used in conjunction with @Singleton, instructs the EJB container to initialize the singleton session bean instance during application startup. This ensures that the EJB session bean to be initialized early during application startup rather than waiting for the container creates an instance of the bean only when a request is received.

The tservice.createIntervalTimer(1000, 1000, new TimerConfig()); creates a new timer that:

  • Starts after 1 second,
  • Repeats every 1 second,
  • Uses the default configuration settings provided by the TimerConfig object.
admin@fedser:websocketdemo$ cat src/main/java/com/stack/websocketdemo/PriceVolumeBean.java 
package com.stack.websocketdemo;

import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.ejb.Timeout;
import jakarta.ejb.TimerConfig;
import jakarta.ejb.TimerService;

/* Updates price and volume information every second */
@Startup
@Singleton
public class PriceVolumeBean {
    /* Use the container's timer service */
    @Resource TimerService tservice;
    private Random random;
    private volatile double price = 100.0;
    private volatile int volume = 300000;
    private static final Logger logger = Logger.getLogger("PriceVolumeBean");
    
    @PostConstruct
    public void init() {
        /* Initialize the EJB and create a timer */
        logger.log(Level.INFO, "Initializing EJB.");
        random = new Random();
        tservice.createIntervalTimer(1000, 1000, new TimerConfig());
    }
    
    @Timeout
    public void timeout() {
        /* Adjust price and volume and send updates */
        price += 1.0*(random.nextInt(100)-50)/100.0;
        volume += random.nextInt(5000) - 2500;
        ETFEndpoint.send(price, volume);
    }    
}

Step4: Create CSS file

Here we are going to provide a basic CSS file to style our response data from the websocket endpoint.

admin@fedser:websocketdemo$ cat src/main/webapp/resources/css/default.css 
body {
    background-color: #ffffff;
    font-size: 12px;
    font-family: Verdana, "Verdana CE",  Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif;
    color: #000000;  
    margin: 10px;
}

h1 {
    font-family: Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif;
    border-bottom: 1px solid #AFAFAF; 
    font-size:  16px;
    font-weight: bold;
    margin: 0px;
    padding: 0px;
    color: #D20005;
}

a:link, a:visited {
  color: #045491;
  font-weight : bold;
  text-decoration: none;
}

a:link:hover, a:visited:hover  {
  color: #045491;
  font-weight : bold;
  text-decoration : underline;
}

Step5: Create an HTML file

Now let’s create the html file which will be present to the end user. This html file uses a script to connect to websocket and call a function onMessage when a data is received by the websocket endpoint. This data is further parsed and inserted into the table in the html.

admin@fedser:websocketdemo$ cat src/main/webapp/index.html 
<!DOCTYPE html>
<html>
<head>
  <title>Duke's WebSocket ETF</title>
  <link rel="stylesheet" type="text/css" href="resources/css/default.css" />
  <script type="text/javascript">
      var wsocket;
      function connect() {
          wsocket = new WebSocket("ws://localhost:8080/websocketdemo/dukeetf");
          wsocket.onmessage = onMessage;
      }
      function onMessage(evt) {
          var arraypv = evt.data.split("/");
          document.getElementById("price").innerHTML = arraypv[0];
          document.getElementById("volume").innerHTML = arraypv[1];
      }
      window.addEventListener("load", connect, false);
  </script>
</head>
<body>
    <h1>Duke's WebSocket ETF</h1>
    <table>
        <tr>
            <td style="width:100px">Ticker</td>
            <td style="text-align:center">Price</td>
            <td id="price" style="font-size:24pt;font-weight:bold;">--.--</td>
        </tr>
        <tr>
            <td style="font-size:18pt;font-weight:bold;width:100px">DKEJ</td>
            <td style="text-align:center">Volume</td>
            <td id="volume" align="right">--</td>
        </tr>
    </table>
</body>
</html>

Step6: Update pom.xml to deploy to Glassfish server

Here we are updating the pom.xml “properties” section to add “glassfish.home” directory where the glassfish server is installed. Also we are updating the plugins section with “org.codehaus.cargo” plugin to carry out automated deployment using the mvn cli to the glassfish server based on configuration properties that are provided. If you are not using this plugin you can generate the war package and deploy it manually to your glassfish server.

admin@fedser:websocketdemo$ cat pom.xml 
<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>com.stack</groupId>
    <artifactId>websocketdemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>websocketdemo</name>
    
    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <glassfish.home>/home/admin/middleware/stack/glassfish7</glassfish.home>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    
    <build>
        <finalName>websocketdemo</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.cargo</groupId>
                <artifactId>cargo-maven3-plugin</artifactId>
		<version>1.10.6</version>
                <executions>
                    <execution>
                        <id>deploy</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>redeploy</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <container>
                        <containerId>glassfish7x</containerId>
                        <type>installed</type>
                        <home>${glassfish.home}</home>
                    </container>
                    <configuration>
                        <type>existing</type>
                        <home>${glassfish.home}/glassfish/domains</home>
                        <properties>
                            <cargo.glassfish.domain.name>domain1</cargo.glassfish.domain.name>
                        </properties>
                    </configuration>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Step7: Build and Deploy Application

Now it’s time to build, package and deploy the application to glassfish application server as shown below.

admin@fedser:websocketdemo$ mvn clean package cargo:deploy

Step8: Validate Application

We can now try to access our WebSocket based application to see the real time data feed getting updated on the web page.

URL: http://localhost:8080/websocketdemo/

Hope you enjoyed reading this article. Thank you..