Scheduled WebSocket Push with Spring Boot – 用Spring Boot进行预定的WebSocket推送

最后修改: 2020年 12月 17日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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.

在本教程中,我们将看到如何使用WebSockets从服务器向浏览器发送预定消息。另一种方法是使用服务器发送的事件(SSE),但我们将在本文中不涉及这个问题。

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.

Spring提供了多种调度选项。首先,我们将介绍@Scheduled 注释。然后,我们将看到一个由Project Reactor提供的Flux::interval方法的例子。这个库在Webflux应用程序中是开箱即用的,而且它可以作为一个独立的库在任何Java项目中使用。

Also, more advanced mechanisms exist, like the Quartz scheduler, but we won’t be covering them.

此外,还有更高级的机制存在,比如Quartz调度器,但我们不会涉及它们。

2. A Simple Chat Application

2.一个简单的聊天应用程序

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.

前一篇文章中,我们使用WebSockets来构建一个聊天应用程序。让我们用一个新的功能来扩展它:聊天机器人。这些机器人是将预定的消息推送到浏览器的服务器端组件。

2.1. Maven Dependencies

2.1.Maven的依赖性

Let’s start by setting the necessary dependencies in Maven. To build this project, our pom.xml should have:

让我们先在Maven中设置必要的依赖项。要构建这个项目,我们的pom.xml应该有。

<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

2.2.JavaFaker的依赖性

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.

我们将使用JavaFaker库来生成我们机器人的信息。这个库经常被用来生成测试数据。在这里,我们将向我们的聊天室添加一个名为”Chuck Norris“的客人。

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.

Faker将为各种数据生成器提供工厂方法。我们将使用ChuckNorris发生器。对chuckNorris.fact()的调用将从预定义的信息列表中显示一个随机句子。

2.3. Data Model

2.3 数据模型

The chat application uses a simple POJO as the message wrapper:

聊天应用程序使用一个简单的POJO作为消息包装器。

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

2.4.客户端

Our chat client is a simple HTML page. It uses a SockJS client and the STOMP message protocol.

我们的聊天客户端是一个简单的HTML页面。它使用一个SockJS客户端STOMP消息协议。

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.

首先,我们通过SockJS协议创建了一个Stomp客户端。然后,主题订阅作为服务器和连接的客户端之间的通信渠道。

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.

在我们的资源库中,这段代码在webapp/bots.html。我们在本地运行时访问它,地址是http://localhost:8080/bots.html。当然,我们需要根据我们部署应用程序的方式来调整主机和端口。

2.5. Server-Side

2.5.服务器端

We’ve seen how to configure WebSockets in Spring in a previous article. Let’s modify that configuration a little bit:

我们已经在之前的文章中看到了如何配置Spring中的WebSockets。让我们稍微修改一下这个配置。

@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.

为了推送我们的消息,我们使用实用类SimpMessagingTemplate。默认情况下,它被作为Spring Context中的@Bean提供。我们可以看到当AbstractMessageBrokerConfiguration在classpath中时,它是如何通过自动配置来声明的。因此,我们可以将它注入到任何Spring组件中。

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:

之后,我们用它来发布消息到主题/topic/pushmessages。我们假设我们的类在一个名为simpMessagingTemplate的变量中注入了该bean。

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

3.安排推送信息

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.

在Spring生态系统中,我们可以从各种调度方法中进行选择。如果我们使用Spring MVC,@Scheduled注解因其简单性而成为自然选择。如果我们使用Spring Webflux,我们也可以使用Project Reactor的Flux::interval方法。我们将看到各自的一个例子。

3.1. Configuration

3.1 配置

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.

我们的聊天机器人将使用JavaFaker的Chuck Norris发生器。我们将把它配置成一个Bean,这样我们就可以把它注入我们需要的地方。

@Configuration
class AppConfig {

    @Bean
    public ChuckNorris chuckNorris() {
        return (new Faker()).chuckNorris();
    }
}

3.2. Using @Scheduled

3.2.使用@Scheduled

Our example bots are scheduled methods. When they run, they send our OutputMessage POJOs through a WebSocket using SimpMessagingTemplate.

我们的示例机器人是预定的方法。当它们运行时,它们通过使用SimpMessagingTemplate的WebSocket发送我们的OutputMessage POJOs。

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.

顾名思义,@Scheduled注释允许重复执行方法。通过它,我们可以使用简单的基于速率的调度或更复杂的 “cron “表达式。

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.

我们给sendMessage方法加上@Scheduled(fixedRate = 5000)的注释。这使得sendMessage每五秒运行一次。然后,我们使用simpMessagingTemplate实例来发送OutputMessage到主题。simpMessagingTemplatechuckNorris实例被作为方法参数从Spring上下文中注入。

3.3. Using Flux::interval()

3.3.使用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 Duration.

如果我们使用WebFlux,我们可以使用Flux::interval操作符。它将发布一个无限的Long项目流,并以一个选定的Duration相分隔。

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:

现在,让我们用我们之前的例子来使用Flux。我们的目标将是每五秒钟发送一条来自Chuck Norris的报价。首先,我们需要实现InitializingBean接口,以便在应用程序启动时订阅Flux

@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.

这里,我们使用构造函数注入来设置simpMessagingTemplatechuckNorris实例。这一次,调度逻辑在afterPropertiesSet()中,我们在实现InitializingBean时覆盖了这个方法。该方法将在服务启动后立即运行。

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.

interval操作符每五秒钟发出一个Long。然后,map操作符丢弃该值并将其替换为我们的消息。最后,我们subscribeFlux,为每个消息触发我们的逻辑。

4. Conclusion

4.总结

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.

在本教程中,我们看到实用类SimpMessagingTemplate使得通过WebSocket推送服务器消息变得容易。此外,我们还看到了两种调度执行一段代码的方法。

As always, the source code for the examples is available over on GitHub.

像往常一样,这些例子的源代码可以在GitHub上找到