Перейти к основному содержимому

Запланированная отправка WebSocket с помощью Spring Boot

· 6 мин. чтения

1. Обзор

В этом руководстве мы увидим, как отправлять запланированные сообщения с сервера в браузер с помощью WebSockets . Альтернативой может быть использование событий, отправленных сервером (SSE), но мы не будем рассматривать это в этой статье.

Spring предоставляет множество вариантов планирования. Во-первых, мы рассмотрим аннотацию @Scheduled . Затем мы увидим пример с методом Flux::interval , предоставленным Project Reactor. Эта библиотека доступна «из коробки» для приложений Webflux и может использоваться как отдельная библиотека в любом проекте Java.

Также существуют более продвинутые механизмы, такие как планировщик Quartz , но мы не будем их рассматривать.

2. Простое приложение для чата

В предыдущей статье мы использовали WebSockets для создания чат-приложения. Давайте расширим его новой функцией: чат-ботами. Эти боты являются компонентами на стороне сервера, которые отправляют запланированные сообщения в браузер.

2.1. Зависимости Maven

Начнем с установки необходимых зависимостей в 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

Мы будем использовать библиотеку JavaFaker для генерации сообщений наших ботов. Эта библиотека часто используется для генерации тестовых данных. Здесь мы добавим в наш чат гостя по имени « Чак Норрис ».

Давайте посмотрим код:

Faker faker = new Faker();
ChuckNorris chuckNorris = faker.chuckNorris();
String messageFromChuck = chuckNorris.fact();

Faker предоставит фабричные методы для различных генераторов данных. Мы будем использовать генератор ChuckNorris . Вызов chuckNorris.fact() отобразит случайное предложение из списка предопределенных сообщений.

2.3. Модель данных

Приложение чата использует простой POJO в качестве оболочки сообщения:

public class OutputMessage {

private String from;
private String text;
private String time;

// standard constructors, getters/setters, equals and hashcode
}

Собрав все вместе, вот пример того, как мы создаем сообщение чата:

OutputMessage message = new OutputMessage(
"Chatbot 1", "Hello there!", new SimpleDateFormat("HH:mm").format(new Date())));

2.4. Сторона клиента

Наш чат-клиент представляет собой простую HTML-страницу. Он использует клиент SockJS и протокол сообщений STOMP .

Посмотрим, как клиент подписывается на тему:

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

Сначала мы создали клиент Stomp по протоколу SockJS. Затем подписка на тему служит каналом связи между сервером и подключенными клиентами.

В нашем репозитории этот код находится в webapp/bots.html . Мы получаем к нему доступ при локальном запуске по адресу http://localhost:8080/bots.html . Конечно, нам нужно настроить хост и порт в зависимости от того, как мы развертываем приложение.

2.5. на стороне сервера

Мы видели, как настроить WebSockets в Spring в предыдущей статье. Давайте немного изменим эту конфигурацию:

@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();
}
}

Для отправки наших сообщений мы используем служебный класс SimpMessagingTemplate . По умолчанию он доступен как @Bean в контексте Spring. Мы можем видеть, как он объявляется через автоконфигурацию, когда AbstractMessageBrokerConfiguration находится в пути к классам. Поэтому мы можем внедрить его в любой компонент Spring.

После этого мы используем его для публикации сообщений в теме /topic/pushmessages . Мы предполагаем, что в нашем классе этот bean-компонент внедрен в переменную с именем simpMessagingTemplate :

simpMessagingTemplate.convertAndSend("/topic/pushmessages", 
new OutputMessage("Chuck Norris", faker.chuckNorris().fact(), time));

Как показано ранее в нашем примере на стороне клиента, клиент подписывается на эту тему для обработки сообщений по мере их поступления.

3. Планирование push-сообщений

В экосистеме Spring мы можем выбирать из множества методов планирования. Если мы используем Spring MVC, аннотация @Scheduled станет естественным выбором из-за ее простоты. Если мы используем Spring Webflux, мы также можем использовать метод Project Reactor Flux::interval . Мы увидим по одному примеру каждого.

3.1. Конфигурация

Наши чат-боты будут использовать генератор Чака Норриса от JavaFaker. Мы настроим его как bean-компонент, чтобы мы могли внедрить его туда, где нам это нужно.

@Configuration
class AppConfig {

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

3.2. Использование @Scheduled

Боты в нашем примере являются запланированными методами. Когда они запускаются, они отправляют наши POJO OutputMessage через WebSocket, используя SimpMessagingTemplate .

Как следует из `` названия, аннотация @Scheduled допускает повторное выполнение методов . С его помощью мы можем использовать простое планирование на основе скорости или более сложные выражения «cron».

Давайте напишем наш первый чат-бот:

@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));
}

}

Мы аннотируем метод sendMessage с помощью @Scheduled(fixedRate = 5000). Это заставляет sendMessage запускаться каждые пять секунд. Затем мы используем экземпляр simpMessagingTemplate для отправки сообщения OutputMessage в тему. Экземпляры simpMessagingTemplate и chuckNorris внедряются из контекста Spring в качестве параметров метода.

3.3. Использование Flux::interval()

Если мы используем WebFlux, мы можем использовать оператор Flux::interval . Он будет публиковать бесконечный поток длинных элементов, разделенных выбранной продолжительностью `` .

Теперь давайте воспользуемся Flux в нашем предыдущем примере. Цель будет заключаться в том, чтобы отправлять цитату Чака Норриса каждые пять секунд. Во-первых, нам нужно реализовать интерфейс 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));
}
}

Здесь мы используем внедрение конструктора для установки экземпляров simpMessagingTemplate и chuckNorris . На этот раз логика планирования находится в afterPropertiesSet(), которую мы переопределяем при реализации InitializingBean . Метод запустится, как только запустится служба.

Оператор интервала выдает Long каждые пять секунд. Затем оператор карты отбрасывает это значение и заменяет его нашим сообщением. Наконец, мы подписываемся на Flux , чтобы запускать нашу логику для каждого сообщения.

4. Вывод

В этом руководстве мы увидели, что служебный класс SimpMessagingTemplate упрощает отправку сообщений сервера через WebSocket. Кроме того, мы видели два способа планирования выполнения фрагмента кода.

Как всегда, исходный код примеров доступен на GitHub .