This guide walks you through creating a "hello world" STOMP messaging server with Spring.
The server will accept a message carying the user's name. In response, it will push a greeting into a queue that the client is subscribed to.
Like all Spring's Getting Started guides, you can start from scratch and complete each step, or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.
To start from scratch, move on to Set up the project.
To skip the basics, do the following:
git clone https://github.com/springframework-meta/gs-rest-service.git
gs-rest-service/initial
.When you're finished, you can check your results against the code in gs-rest-service/complete
.
First you set up a basic build script. You can use any build system you like when building apps with Spring, but the code you need to work with Maven and Gradle is included here. If you're not familiar with either, refer to Building Java Projects with Maven or Building Java Projects with Gradle.
In a project directory of your choosing, create the following subdirectory structure; for example, with mkdir -p src/main/java/hello
on *nix systems:
└── src
└── main
└── java
└── hello
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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.samples</groupId>
<artifactId>gs-messaging-stomp-websocket</artifactId>
<packaging>war</packaging>
<version>0.1.0</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>4.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0-rc5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1-b09</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.6.4</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.4</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>1.0.0.M1</version>
</dependency>
<!-- Required when the "stomp-broker-relay" profile is enabled -->
<dependency>
<groupId>org.projectreactor</groupId>
<artifactId>reactor-tcp</artifactId>
<version>1.0.0.M1</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1</version>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<url>http://repo.springsource.org/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>tomcat-snapshots</id>
<url>https://repository.apache.org/content/repositories/snapshots</url>
<snapshots><enabled>true</enabled></snapshots>
<releases><enabled>false</enabled></releases>
</repository>
<repository>
<id>java-net-snapshots</id>
<url>https://maven.java.net/content/repositories/snapshots</url>
<snapshots><enabled>true</enabled></snapshots>
<releases><enabled>false</enabled></releases>
</repository>
</repositories>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
This guide is using Spring Boot's starter POMs.
Note to experienced Maven users who are unaccustomed to using an external parent project: you can take it out later, it's just there to reduce the amount of code you have to write to get started.
Now that you've set up the project and build system, you can create your STOMP message service.
Begin the process by thinking about service interactions.
The service will accept messages containing a name in a STOMP message whose body is a JSON object. If the name given is "Fred", then the message might look something like this:
{
"name": "Fred"
}
To model the message carrying the name, you can create a plain old Java object with a name
property and a corresponding getName()
method:
src/main/java/hello/HelloMessage.java
package hello;
public class HelloMessage {
private String name;
public String getName() {
return name;
}
}
Upon receiving the message and extracting the name, the service will process it by creating a greeting and publishing that greeting on a separate queue that the client is subscribed to. The greeting will also be a JSON object, which might look something like this:
{
"content": "Hello, Fred!"
}
To model the greeting representation, you another plain old Java object with a content
property and corresponding getContent()
method:
src/main/java/hello/Greeting.java
package hello;
public class Greeting {
private String content;
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
Note: As you see in steps below, Spring uses the Jackson JSON library to automatically marshal instances of type
Greeting
into JSON.
Next, you'll create the controller to receive the hello message and send a greeting message.
In Spring's approach to working with STOMP messaging, STOMP messages can be handled by a controller. These components are easily identified by the [@Controller
][AtController] annotation, and the GreetingController
below is mapped to handle messages published on the "/app/hello" destination.
src/main/java/hello/GreetingController.java
package hello;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
@Controller
public class GreetingController {
private SimpMessageSendingOperations messagingTemplate;
@Autowired
public GreetingController(SimpMessageSendingOperations messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
@MessageMapping("/app/hello")
public void greeting(@RequestBody HelloMessage message) throws Exception {
Thread.sleep(3000); // simulated delay
Greeting greeting = new Greeting("Hello, " + message.getName() + "!");
messagingTemplate.convertAndSend("/queue/greetings", greeting);
}
}
This controller is concise and simple, but there's plenty going on under the hood. Let's break it down step by step.
The @MessageMapping
annotation ensures that if a message is published on the "/app/hello" destination, then the greeting()
method will called.
@RequestBody
binds the payload of the message to a HelloMessage
object which is passed into greeting()
.
Internally, the implementation of the method simulates a processing delay by causing the thread to sleep for 3 seconds. This is to demonstrate that after the client sends a message, the server can take as long as it needs to process the message asynchronously. The client may continue with whatever work it needs to do without waiting on the response.
After the 3 second delay, the greeting()
method creates a new Greeting
object, setting its content to say "Hello" to the name from the HelloMessage
. It then calls convertAndSend()
on the injected SimpMessageSendingOperations
to send the Greeting
on the "/queue/greetings" destination.
The Greeting
object must be converted to JSON. Thanks to Spring's HTTP message converter support, you don't need to do this conversion manually. When you configure Spring for STOMP messaging, you'll inject SimpMessagingTemplate
with an instance of MappingJackson2MessageConverter
. It will be used to convert the Greeting
instance to JSON.
TODO: This is extremely ugly at the moment, with many beans in play. Rossen says that SPR-10835 will be resolved in time for RC1, so fill in this section then.
In order to deploy the application to Tomcat, you'll need to add a bit more configuration.
First, you'll need to configure Spring's [DispatcherServlet
[DispatcherServlet] to serve static resources so that it will serve index.html. This can be done by creating a configuration class that overrides the configureDefaultServletHandling()
method of WebMvcConfigurerAdapter
and calls enable()
on the given DefaultServletHandlerConfigurer
:
src/main/java/hello/WebConfig.java
package hello;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.handler.SimpleUserQueueSuffixResolver;
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
import org.springframework.messaging.simp.stomp.StompProtocolHandler;
import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
import org.springframework.messaging.support.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.support.converter.MessageConverter;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.sockjs.SockJsHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsService;
import org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService;
@Configuration
@EnableWebMvc
@ComponentScan
@EnableAsync
public class WebConfig extends WebMvcConfigurerAdapter {
private final MessageConverter<?> messageConverter = new MappingJackson2MessageConverter();
private final SimpleUserQueueSuffixResolver userQueueSuffixResolver = new SimpleUserQueueSuffixResolver();
@Bean
public SimpleUrlHandlerMapping handlerMapping() {
SockJsService sockJsService = new DefaultSockJsService(taskScheduler());
HttpRequestHandler requestHandler = new SockJsHttpRequestHandler(sockJsService, webSocketHandler());
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
hm.setOrder(-1);
hm.setUrlMap(Collections.singletonMap("/hello/**", requestHandler));
return hm;
}
// WebSocketHandler supporting STOMP messages
@Bean
public WebSocketHandler webSocketHandler() {
StompProtocolHandler stompHandler = new StompProtocolHandler();
stompHandler.setUserQueueSuffixResolver(this.userQueueSuffixResolver);
SubProtocolWebSocketHandler webSocketHandler = new SubProtocolWebSocketHandler(dispatchChannel());
webSocketHandler.setDefaultProtocolHandler(stompHandler);
webSocketHandlerChannel().subscribe(webSocketHandler);
return webSocketHandler;
}
// MessageHandler for processing messages by delegating to @Controller annotated methods
@Bean
public AnnotationMethodMessageHandler annotationMessageHandler() {
AnnotationMethodMessageHandler handler =
new AnnotationMethodMessageHandler(dispatchMessagingTemplate(), webSocketHandlerChannel());
handler.setDestinationPrefixes(Arrays.asList("/app/"));
handler.setMessageConverter(this.messageConverter);
dispatchChannel().subscribe(handler);
return handler;
}
// MessageHandler that acts as a "simple" message broker
// See DispatcherServletInitializer for enabling/disabling the "simple-broker" profile
@Bean
public SimpleBrokerMessageHandler simpleBrokerMessageHandler() {
SimpleBrokerMessageHandler handler = new SimpleBrokerMessageHandler(webSocketHandlerChannel());
handler.setDestinationPrefixes(Arrays.asList("/topic/", "/queue/"));
dispatchChannel().subscribe(handler);
return handler;
}
// MessageHandler that resolves destinations prefixed with "/user/{user}"
// See the Javadoc of UserDestinationMessageHandler for details
@Bean
public UserDestinationMessageHandler userMessageHandler() {
UserDestinationMessageHandler handler = new UserDestinationMessageHandler(
dispatchMessagingTemplate(), this.userQueueSuffixResolver);
dispatchChannel().subscribe(handler);
return handler;
}
// MessagingTemplate (and MessageChannel) to dispatch messages to for further processing
// All MessageHandler beans above subscribe to this channel
@Bean
public SimpMessageSendingOperations dispatchMessagingTemplate() {
SimpMessagingTemplate template = new SimpMessagingTemplate(dispatchChannel());
template.setMessageConverter(this.messageConverter);
return template;
}
@Bean
public SubscribableChannel dispatchChannel() {
return new ExecutorSubscribableChannel(asyncExecutor());
}
// Channel for sending STOMP messages to connected WebSocket sessions (mostly for internal use)
@Bean
public SubscribableChannel webSocketHandlerChannel() {
return new ExecutorSubscribableChannel(asyncExecutor());
}
// Executor for message passing via MessageChannel
@Bean
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setCorePoolSize(8);
executor.setThreadNamePrefix("MessageChannel-");
return executor;
}
// Task executor for use in SockJS (heartbeat frames, session timeouts)
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setThreadNamePrefix("SockJS-");
taskScheduler.setPoolSize(4);
return taskScheduler;
}
// Allow serving HTML files through the default Servlet
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
You'll also need to configure DispatcherServlet
. This is most easily done by creating a class that extends AbstractAnnotationConfigDispatcherServletInitializer
:
src/main/java/hello/HelloServletInitializer.java
package hello;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class HelloServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setInitParameter("dispatchOptionsRequest", "true");
}
}
Here, DispatcherServlet
is mapped to "/". Also, WebConfig
and EndpointConfig
are specified, respectively, as the servlet and root configuration classes.
Now you're ready to build and deploy the application to Tomcat 8. Start by building the WAR file:
mvn package
Then copy the WAR file to Tomcat 8's trunk/output/webapps
directory.
Finally, restart Tomcat 8:
output/bin/shutdown.sh
output/bin/startup.sh
After the application starts, point your browser at http://localhost:8080/gs-messaging-stomp-websocket and click the "Connect" button.
Upon opening a connection, you will be asked for your name. Enter your name and click "Send". Your name will be sent to the server as a JSON message over STOMP. The server will send a message back with a "Hello" greeting that will be displayed on the page. At this point, you may choose to send another name or you can click the "Disconnect" button to close the connection.
Congratulations! You've just developed a STOMP-based messaging service with Spring.