Java WebSockets (JSR-356) on Jetty 9.1
Jetty 9.1 is finally released, bringing Java WebSockets (JSR-356) to non-EE environments. It's awesome news and today's post will be about using this great new API along with Spring Framework. JSR-356 defines concise, annotation-based model to allow modern Java web applications easily create bidirectional communication channels using WebSockets API. It covers not only server-side, but client-side as well, making this API really simple to use everywhere. Let's get started! Our goal would be to build a WebSockets server which accepts messages from the clients and broadcasts them to all other clients currently connected. To begin with, let's define the message format, which server and client will be exchanging, as this simple Message class. We can limit ourselves to something like a String, but I would like to introduce to you the power of another new API - Java API for JSON Processing (JSR-353). package com.example.services; public class Message { private String username; private String message; public Message() { } public Message( final String username, final String message ) { this.username = username; this.message = message; } public String getMessage() { return message; } public String getUsername() { return username; } public void setMessage( final String message ) { this.message = message; } public void setUsername( final String username ) { this.username = username; } } To separate the declarations related to the server and the client, JSR-356 defines two basic annotations:@ServerEndpoint and @ClientEndpoit respectively. Our client endpoint, let's call itBroadcastClientEndpoint, will simply listen for messages server sends: package com.example.services; import java.io.IOException; import java.util.logging.Logger; import javax.websocket.ClientEndpoint; import javax.websocket.EncodeException; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; @ClientEndpoint public class BroadcastClientEndpoint { private static final Logger log = Logger.getLogger( BroadcastClientEndpoint.class.getName() ); @OnOpen public void onOpen( final Session session ) throws IOException, EncodeException { session.getBasicRemote().sendObject( new Message( "Client", "Hello!" ) ); } @OnMessage public void onMessage( final Message message ) { log.info( String.format( "Received message '%s' from '%s'", message.getMessage(), message.getUsername() ) ); } } That's literally it! Very clean, self-explanatory piece of code: @OnOpen is being called when client got connected to the server and @OnMessage is being called every time server sends a message to the client. Yes, it's very simple but there is a caveat: JSR-356 implementation can handle any simple objects but not the complex ones like Message is. To manage that, JSR-356 introduces concept of encoders and decoders. We all love JSON, so why don't we define our own JSON encoder and decoder? It's an easy task which Java API for JSON Processing (JSR-353) can handle for us. To create an encoder, you only need to implementEncoder.Text< Message > and basically serialize your object to some string, in our case to JSON string, using JsonObjectBuilder. package com.example.services; import javax.json.Json; import javax.json.JsonReaderFactory; import javax.websocket.EncodeException; import javax.websocket.Encoder; import javax.websocket.EndpointConfig; public class Message { public static class MessageEncoder implements Encoder.Text< Message > { @Override public void init( final EndpointConfig config ) { } @Override public String encode( final Message message ) throws EncodeException { return Json.createObjectBuilder() .add( "username", message.getUsername() ) .add( "message", message.getMessage() ) .build() .toString(); } @Override public void destroy() { } } } For decoder part, everything looks very similar, we have to implement Decoder.Text< Message > and deserialize our object from string, this time using JsonReader. package com.example.services; import javax.json.JsonObject; import javax.json.JsonReader; import javax.json.JsonReaderFactory; import javax.websocket.DecodeException; import javax.websocket.Decoder; public class Message { public static class MessageDecoder implements Decoder.Text< Message > { private JsonReaderFactory factory = Json.createReaderFactory( Collections.< String, Object >emptyMap() ); @Override public void init( final EndpointConfig config ) { } @Override public Message decode( final String str ) throws DecodeException { final Message message = new Message(); try( final JsonReader reader = factory.createReader( new StringReader( str ) ) ) { final JsonObject json = reader.readObject(); message.setUsername( json.getString( "username" ) ); message.setMessage( json.getString( "message" ) ); } return message; } @Override public boolean willDecode( final String str ) { return true; } @Override public void destroy() { } } } And as a final step, we need to tell the client (and the server, they share same decoders and encoders) that we have encoder and decoder for our messages. The easiest thing to do that is just by declaring them as part of @ServerEndpoint and @ClientEndpoit annotations. import com.example.services.Message.MessageDecoder; import com.example.services.Message.MessageEncoder; @ClientEndpoint( encoders = { MessageEncoder.class }, decoders = { MessageDecoder.class } ) public class BroadcastClientEndpoint { } To make client's example complete, we need some way to connect to the server usingBroadcastClientEndpoint and basically exchange messages. The ClientStarter class finalizes the picture: package com.example.ws; import java.net.URI; import java.util.UUID; import javax.websocket.ContainerProvider; import javax.websocket.Session; import javax.websocket.WebSocketContainer; import org.eclipse.jetty.websocket.jsr356.ClientContainer; import com.example.services.BroadcastClientEndpoint; import com.example.services.Message; public class ClientStarter { public static void main( final String[] args ) throws Exception { final String client = UUID.randomUUID().toString().substring( 0, 8 ); final WebSocketContainer container = ContainerProvider.getWebSocketContainer(); final String uri = "ws://localhost:8080/broadcast"; try( Session session = container.connectToServer( BroadcastClientEndpoint.class, URI.create( uri ) ) ) { for( int i = 1; i <= 10; ++i ) { session.getBasicRemote().sendObject( new Message( client, "Message #" + i ) ); Thread.sleep( 1000 ); } } // Application doesn't exit if container's threads are still running ( ( ClientContainer )container ).stop(); } } Just couple of comments what this code does: we are connecting to WebSockets endpoint atws://localhost:8080/broadcast, randomly picking some client name (from UUID) and generating 10 messages, every with 1 second delay (just to be sure we have time to receive them all back). Server part doesn't look very different and at this point could be understood without any additional comments (except may be the fact that server just broadcasts every message it receives to all connected clients). Important to mention here: new instance of the server endpoint is created every time new client connects to it (that's why peers collection is static), it's a default behavior and could be easily changed. package com.example.services; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; import javax.websocket.EncodeException; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import com.example.services.Message.MessageDecoder; import com.example.services.Message.MessageEncoder; @ServerEndpoint( value = "/broadcast", encoders = { MessageEncoder.class }, decoders = { MessageDecoder.class } ) public class BroadcastServerEndpoint { private static final Set< Session > sessions = Collections.synchronizedSet( new HashSet< Session >() ); @OnOpen public void onOpen( final Session session ) { sessions.add( session ); } @OnClose public void onClose( final Session session ) { sessions.remove( session ); } @OnMessage public void onMessage( final Message message, final Session client ) throws IOException, EncodeException { for( final Session session: sessions ) { session.getBasicRemote().sendObject( message ); } } } In order this endpoint to be available for connection, we should start the WebSockets container and register this endpoint inside it. As always, Jetty 9.1 is runnable in embedded mode effortlessly: package com.example.ws; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import com.example.config.AppConfig; public class ServerStarter { public static void main( String[] args ) throws Exception { Server server = new Server( 8080 ); // Create the 'root' Spring application context final ServletHolder servletHolder = new ServletHolder( new DefaultServlet() ); final ServletContextHandler context = new ServletContextHandler(); context.setContextPath( "/" ); context.addServlet( servletHolder, "/*" ); context.addEventListener( new ContextLoaderListener() ); context.setInitParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() ); context.setInitParameter( "contextConfigLocation", AppConfig.class.getName() ); server.setHandler( context ); WebSocketServerContainerInitializer.configureContext( context ); server.start(); server.join(); } } The most important part of the snippet above is WebSocketServerContainerInitializer.configureContext: it's actually creates the instance of WebSockets container. Because we haven't added any endpoints yet, the container basically sits here and does nothing. Spring Framework and AppConfig configuration class will do this last wiring for us. package com.example.config; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.websocket.DeploymentException; import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.websocket.jsr356.server.AnnotatedServerEndpointConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.WebApplicationContext; import com.example.services.BroadcastServerEndpoint; @Configuration public class AppConfig { @Inject private WebApplicationContext context; private ServerContainer container; public class SpringServerEndpointConfigurator extends ServerEndpointConfig.Configurator { @Override public < T > T getEndpointInstance( Class< T > endpointClass ) throws InstantiationException { return context.getAutowireCapableBeanFactory().createBean( endpointClass ); } } @Bean public ServerEndpointConfig.Configurator configurator() { return new SpringServerEndpointConfigurator(); } @PostConstruct public void init() throws DeploymentException { container = ( ServerContainer )context.getServletContext(). getAttribute( javax.websocket.server.ServerContainer.class.getName() ); container.addEndpoint( new AnnotatedServerEndpointConfig( BroadcastServerEndpoint.class, BroadcastServerEndpoint.class.getAnnotation( ServerEndpoint.class ) ) { @Override public Configurator getConfigurator() { return configurator(); } } ); } } As we mentioned earlier, by default container will create new instance of server endpoint every time new client connects, and it does so by calling constructor, in our caseBroadcastServerEndpoint.class.newInstance(). It might be a desired behavior but because we are usingSpring Framework and dependency injection, such new objects are basically unmanaged beans. Thanks to very well-thought (in my opinion) design of JSR-356, it's actually quite easy to provide your own way of creating endpoint instances by implementing ServerEndpointConfig.Configurator. TheSpringServerEndpointConfigurator is an example of such implementation: it's creates new managed bean every time new endpoint instance is being asked (if you want single instance, you can create singleton of the endpoint as a bean in AppConfig and return it all the time). The way we retrieve the WebSockets container is Jetty-specific: from the attribute of the context with name"javax.websocket.server.ServerContainer" (it probably might change in the future). Once container is there, we are just adding new (managed!) endpoint by providing our own ServerEndpointConfig (based onAnnotatedServerEndpointConfig which Jetty kindly provides already). To build and run our server and clients, we need just do that: mvn clean package java -jar target\jetty-web-sockets-jsr356-0.0.1-SNAPSHOT-server.jar // run server java -jar target/jetty-web-sockets-jsr356-0.0.1-SNAPSHOT-client.jar // run yet another client As an example, by running server and couple of clients (I run 4 of them, '392f68ef', '8e3a869d', 'ca3a06d0', '6cb82119') you might see by the output in the console that each client receives all the messages from all other clients (including its own messages): Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Hello!' from 'Client' Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #1' from '392f68ef' Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #2' from '8e3a869d' Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #7' from 'ca3a06d0' Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #4' from '6cb82119' Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #2' from '392f68ef' Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #3' from '8e3a869d' Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #8' from 'ca3a06d0' Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #5' from '6cb82119' Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #3' from '392f68ef' Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #4' from '8e3a869d' Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #9' from 'ca3a06d0' Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #6' from '6cb82119' Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #4' from '392f68ef' Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #5' from '8e3a869d' Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #10' from 'ca3a06d0' Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #7' from '6cb82119' Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #5' from '392f68ef' Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #6' from '8e3a869d' Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #8' from '6cb82119' Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #6' from '392f68ef' Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #7' from '8e3a869d' Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #9' from '6cb82119' Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #7' from '392f68ef' Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #8' from '8e3a869d' Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #10' from '6cb82119' Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #8' from '392f68ef' Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #9' from '8e3a869d' Nov 29, 2013 9:21:37 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #9' from '392f68ef' Nov 29, 2013 9:21:37 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #10' from '8e3a869d' Nov 29, 2013 9:21:38 PM com.example.services.BroadcastClientEndpoint onMessage INFO: Received message 'Message #10' from '392f68ef' 2013-11-29 21:21:39.260:INFO:oejwc.WebSocketClient:main: Stopped org.eclipse.jetty.websocket.client.WebSocketClient@3af5f6dc Awesome! I hope this introductory blog post shows how easy it became to use modern web communication protocols in Java, thanks to Java WebSockets (JSR-356), Java API for JSON Processing (JSR-353) and great projects such as Jetty 9.1! As always, complete project is available on GitHub.
December 6, 2013
·
39,935 Views
·
2 Likes
Comments
Feb 18, 2019 · Lindsay Burk
Thank you, Ken. This is right, not all frameworks play well with Java 9+ yet, but most of the time it is straightforward to fix by adding the missing dependencies (the javax.xml.ws became a module in Java 10 and got removed in Java 11 altogether):
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.xml.ws</groupId>
<artifactId>jaxws-api</artifactId>
<version>2.3.0</version>
</dependency>
Alternatively, the new Jakarta EE artifcats could be used instead (please check out https://wiki.eclipse.org/New_Maven_Coordinates).
Thank you!
Nov 14, 2018 · Lindsay Burk
This one is Spring MVC specific (https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java)
Nov 10, 2018 · Lindsay Burk
Thank you, indeed, Impl is often used to outline the implementation part but I agree with you, it is valueless in most cases and the better naming should apply, noted.
Nov 09, 2018 · Lindsay Burk
Hi Dean,
Yes, this article is about Java EE specificaly and Microprofile in particular, not microframeworks in general (there are at least two dozens of them I know about). And yes, I think the Spring significantly influenced the way the enterprise Java applications used to be built. We are not talking about monstrous application servers full of XML confiugration anymore. The goal of this article is to highlight these transformations. Thank you.
Best Regards,
Andriy Redko
Nov 09, 2018 · Lindsay Burk
The closest example I can think of is Spring Data REST (https://spring.io/projects/spring-data-rest), is it good or bad idea, different question though ...
Nov 08, 2018 · Lindsay Burk
It is easy to lose trust, and it is very difficult to get it back. Spring is certainly leading the way, the time will reveal if the enterprise Java is able to keep up with the pace.
Nov 08, 2018 · Lindsay Burk
Cannot agree with you more. The design done right, following the DDD principles f.e., is significantly more important than any technical goodies, no matter what. In your opinion, what would be the balance between design part and technical part? Comparing to this article, is the technical discussion too detailed? (not asking about design since is it miniscule).
Taking code from the real systems is not easy since it is in 99.9% a proproetary intellectual property. However, I think the good examples could be derived for sure.
Thank you for reminding one more time what really matters.
Nov 08, 2018 · Lindsay Burk
Hi Robert,
Thank you for the comment. The goal of this article is to showcase how easy it become to use the Java EE tech stack these days. It helps to understand how different pieces wire together and it is a foundation to build upon (since many real application do use relational databases and use RESTful web APIs for communication). Indeed, there is not much business logic, the reason for this is two-fold: 1) keep the post reasonably short 2) keep it easy to follow since the business requirements have to be explained before and referred to.
I hope it make sense.
Thank you for yhe !
Best Regards,
Andriy Redko
May 27, 2018 · Jordan Baker
Thank you, Irfan!
May 26, 2018 · Jordan Baker
Hi Irfan,
My pleasure. Oh I see, so you basically are using WAR-based deployment, right? Thanks!
Best Regards,
Andriy Redko
May 26, 2018 · Jordan Baker
Hi Ifran,
Thank you for the comment. Please take a look at one of the CXF sample projects, I believe this is exactly what you need: https://github.com/apache/cxf/blob/master/distribution/src/main/release/samples/jax_rs/description_openapi_v3
Best Regards,
Andriy Redko