1. Overview
In this tutorial, we’ll see how to send scheduled messages from a server to the browser using WebSockets. An alternative would be using Server sent events (SSE), but we won’t be covering that in this article.
Spring provides a variety of scheduling options. First, we’ll be covering the @Scheduled annotation. Then, we’ll see an example with Flux::interval method provided by Project Reactor. This library is available out-of-the-box for Webflux applications, and it can be used as a standalone library in any Java project.
Also, more advanced mechanisms exist, like the Quartz scheduler, but we won’t be covering them.
2. A Simple Chat Application
In a previous article, we used WebSockets to build a chat application. Let’s extend it with a new feature: chatbots. Those bots are the server-side components that push scheduled messages to the browser.
2.1. Maven Dependencies
Let’s start by setting the necessary dependencies in Maven. To build this project, our pom.xml should have:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
2.2. JavaFaker Dependency
We’ll be using the JavaFaker library to generate our bots’ messages. This library is often used to generate test data. Here, we’ll add a guest named “Chuck Norris” to our chat room.
Let’s see the code:
Faker faker = new Faker();
ChuckNorris chuckNorris = faker.chuckNorris();
String messageFromChuck = chuckNorris.fact();
The Faker will provide factory methods for various data generators. We’ll be using the ChuckNorris generator. A call to chuckNorris.fact() will display a random sentence from a list of predefined messages.
2.3. Data Model
The chat application uses a simple POJO as the message wrapper:
public class OutputMessage {
private String from;
private String text;
private String time;
// standard constructors, getters/setters, equals and hashcode
}
Putting it all together, here’s an example of how we create a chat message:
OutputMessage message = new OutputMessage(
"Chatbot 1", "Hello there!", new SimpleDateFormat("HH:mm").format(new Date())));
2.4. Client-Side
Our chat client is a simple HTML page. It uses a SockJS client and the STOMP message protocol.
Let’s see how the client subscribes to a topic:
<html>
<head>
<script src="./js/sockjs-0.3.4.js"></script>
<script src="./js/stomp.js"></script>
<script type="text/javascript">
// ...
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
// ...
stompClient.subscribe('/topic/pushmessages', function(messageOutput) {
showMessageOutput(JSON.parse(messageOutput.body));
});
});
// ...
</script>
</head>
<!-- ... -->
</html>
First, we created a Stomp client over the SockJS protocol. Then, the topic subscription serves as the communication channel between the server and the connected clients.
In our repository, this code is in webapp/bots.html. We access it when running locally at http://localhost:8080/bots.html. Of course, we need to adjust the host and port depending on how we deploy the application.
2.5. Server-Side
We’ve seen how to configure WebSockets in Spring in a previous article. Let’s modify that configuration a little bit:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// ...
registry.addEndpoint("/chatwithbots");
registry.addEndpoint("/chatwithbots").withSockJS();
}
}
To push our messages, we use the utility class SimpMessagingTemplate. By default, it’s made available as a @Bean in the Spring Context. We can see how it’s declared through autoconfiguration when the AbstractMessageBrokerConfiguration is in the classpath. Therefore, we can inject it into any Spring component.
Following that, we use it to publish messages to the topic /topic/pushmessages. We assume our class has that bean injected in a variable named simpMessagingTemplate:
simpMessagingTemplate.convertAndSend("/topic/pushmessages",
new OutputMessage("Chuck Norris", faker.chuckNorris().fact(), time));
As shown previously in our client-side example, the client subscribes to that topic to process messages as they arrive.
3. Scheduling Push Messages
In the Spring ecosystem, we can choose from a variety of scheduling methods. If we use Spring MVC, the @Scheduled annotation comes as a natural choice for its simplicity. If we use Spring Webflux, we can also use Project Reactor’s Flux::interval method. We’ll see one example of each.
3.1. Configuration
Our chatbots will use the JavaFaker’s Chuck Norris generator. We’ll configure it as a bean so we can inject it where we need it.
@Configuration
class AppConfig {
@Bean
public ChuckNorris chuckNorris() {
return (new Faker()).chuckNorris();
}
}
3.2. Using @Scheduled
Our example bots are scheduled methods. When they run, they send our OutputMessage POJOs through a WebSocket using SimpMessagingTemplate.
As its name implies, the @Scheduled annotation allows the repeated execution of methods. With it, we can use simple rate-based scheduling or more complex “cron” expressions.
Let’s code our first chatbot:
@Service
public class ScheduledPushMessages {
@Scheduled(fixedRate = 5000)
public void sendMessage(SimpMessagingTemplate simpMessagingTemplate, ChuckNorris chuckNorris) {
String time = new SimpleDateFormat("HH:mm").format(new Date());
simpMessagingTemplate.convertAndSend("/topic/pushmessages",
new OutputMessage("Chuck Norris (@Scheduled)", chuckNorris().fact(), time));
}
}
We annotate the sendMessage method with @Scheduled(fixedRate = 5000). This makes sendMessage run every five seconds. Then, we use the simpMessagingTemplate instance to send an OutputMessage to the topic. The simpMessagingTemplate and chuckNorris instances are injected from the Spring context as method parameters.
3.3. Using Flux::interval()
If we use WebFlux, we can use the Flux::interval operator. It will publish an infinite stream of Long items separated by a chosen D**uration.
Now, let’s use Flux with our previous example. The goal will be to send a quote from Chuck Norris every five seconds. First, we need to implement the InitializingBean interface to subscribe to the Flux at application startup:
@Service
public class ReactiveScheduledPushMessages implements InitializingBean {
private SimpMessagingTemplate simpMessagingTemplate;
private ChuckNorris chuckNorris;
@Autowired
public ReactiveScheduledPushMessages(SimpMessagingTemplate simpMessagingTemplate, ChuckNorris chuckNorris) {
this.simpMessagingTemplate = simpMessagingTemplate;
this.chuckNorris = chuckNorris;
}
@Override
public void afterPropertiesSet() throws Exception {
Flux.interval(Duration.ofSeconds(5L))
// discard the incoming Long, replace it by an OutputMessage
.map((n) -> new OutputMessage("Chuck Norris (Flux::interval)",
chuckNorris.fact(),
new SimpleDateFormat("HH:mm").format(new Date())))
.subscribe(message -> simpMessagingTemplate.convertAndSend("/topic/pushmessages", message));
}
}
Here, we use constructor injection to set the simpMessagingTemplate and chuckNorris instances. This time, the scheduling logic is in afterPropertiesSet(), which we override when implementing InitializingBean. The method will run as soon as the service starts up.
The interval operator emits a Long every five seconds. Then, the map operator discards that value and replaces it with our message. Finally, we subscribe to the Flux to trigger our logic for each message.
4. Conclusion
In this tutorial, we’ve seen that the utility class SimpMessagingTemplate makes it easy to push server messages through a WebSocket. In addition, we’ve seen two ways of scheduling the execution of a piece of code.
As always, the source code for the examples is available over on GitHub.