«1. Обзор

В этой статье мы создадим простое веб-приложение, реализующее обмен сообщениями с использованием новых возможностей WebSocket, представленных в Spring Framework 4.0.

WebSockets — это двунаправленное, полнодуплексное, постоянное соединение между веб-браузером и сервером. Как только соединение WebSocket установлено, соединение остается открытым до тех пор, пока клиент или сервер не решит закрыть это соединение.

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

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

Так как это проект на основе Maven, мы сначала добавим необходимые зависимости в pom.xml:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

Кроме того, поскольку мы будем использовать JSON для сборки тела наших сообщений, нам нужно добавить зависимости Джексона. Это позволяет Spring преобразовывать наш Java-объект в/из JSON:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.10.2</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId> 
    <version>2.10.2</version>
</dependency>

Если вы хотите получить новейшую версию вышеуказанных библиотек, поищите их на Maven Central.

3. Включите WebSocket в Spring

Первое, что нужно сделать, это включить возможности WebSocket. Для этого нам нужно добавить конфигурацию в наше приложение и аннотировать этот класс с помощью @EnableWebSocketMessageBroker.

Как следует из названия, он позволяет обрабатывать сообщения WebSocket при поддержке брокера сообщений:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
         registry.addEndpoint("/chat");
         registry.addEndpoint("/chat").withSockJS();
    }
}

Здесь мы видим, что метод configureMessageBroker используется для настройки брокера сообщений. Во-первых, мы разрешаем брокеру сообщений в памяти передавать сообщения клиенту по пунктам назначения с префиксом «/topic».

Мы завершаем нашу простую настройку, назначая префикс «/app» для фильтрации пунктов назначения, нацеленных на аннотированные методы приложения (через @MessageMapping).

Метод registerStompEndpoints регистрирует конечную точку «/chat», включая поддержку STOMP в Spring. Имейте в виду, что мы также добавляем здесь конечную точку, которая работает без SockJS для эластичности.

Эта конечная точка с префиксом «/app» является конечной точкой, на которую сопоставляется метод ChatController.send().

Он также включает резервные варианты SockJS, чтобы можно было использовать альтернативные варианты обмена сообщениями, если WebSockets недоступны. Это полезно, так как WebSocket еще не поддерживается во всех браузерах и может быть запрещен ограничивающими сетевыми прокси.

Резервные варианты позволяют приложениям использовать API WebSocket, но при необходимости во время выполнения изящно переходят на альтернативы, отличные от WebSocket.

4. Создайте модель сообщения

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

Конечная точка будет принимать сообщения, содержащие имя отправителя и текст сообщения STOMP, тело которого является объектом JSON.

Сообщение может выглядеть так:

{
    "from": "John",
    "text": "Hello!"
}

Чтобы смоделировать сообщение, содержащее текст, мы можем создать простой объект Java со свойствами from и text:

public class Message {

    private String from;
    private String text;

    // getters and setters
}

По умолчанию Spring будет использовать Библиотека Джексона для преобразования объекта нашей модели в JSON и обратно.

5. Создайте контроллер обработки сообщений

Как мы видели, подход Spring к работе с обменом сообщениями STOMP заключается в связывании метода контроллера с настроенной конечной точкой. Это стало возможным благодаря аннотации @MessageMapping.

Связь между конечной точкой и контроллером дает нам возможность обрабатывать сообщение, если это необходимо:

@MessageMapping("/chat")
@SendTo("/topic/messages")
public OutputMessage send(Message message) throws Exception {
    String time = new SimpleDateFormat("HH:mm").format(new Date());
    return new OutputMessage(message.getFrom(), message.getText(), time);
}

В целях нашего примера мы создадим еще один объект модели с именем OutputMessage для представления отправленного выходного сообщения. к настроенному месту назначения. Мы заполняем наш объект отправителем и текстом сообщения, взятым из входящего сообщения, и обогащаем его отметкой времени.

После обработки нашего сообщения мы отправляем его в соответствующий пункт назначения, указанный в аннотации @SendTo. Все подписчики адресата «/topic/messages» получат сообщение.

6. Создайте браузерный клиент

После настройки на стороне сервера мы воспользуемся библиотекой sockjs-client для создания простой HTML-страницы, взаимодействующей с нашей системой обмена сообщениями.

«Прежде всего, нам нужно импортировать клиентские библиотеки sockjs и stomp Javascript. Затем мы можем создать функцию connect() для открытия связи с нашей конечной точкой, функцию sendMessage() для отправки нашего сообщения STOMP и функцию разъединения() для закрытия связи:

<html>
    <head>
        <title>Chat WebSocket</title>
        <script src="resources/js/sockjs-0.3.4.js"></script>
        <script src="resources/js/stomp.js"></script>
        <script type="text/javascript">
            var stompClient = null;
            
            function setConnected(connected) {
                document.getElementById('connect').disabled = connected;
                document.getElementById('disconnect').disabled = !connected;
                document.getElementById('conversationDiv').style.visibility 
                  = connected ? 'visible' : 'hidden';
                document.getElementById('response').innerHTML = '';
            }
            
            function connect() {
                var socket = new SockJS('/chat');
                stompClient = Stomp.over(socket);  
                stompClient.connect({}, function(frame) {
                    setConnected(true);
                    console.log('Connected: ' + frame);
                    stompClient.subscribe('/topic/messages', function(messageOutput) {
                        showMessageOutput(JSON.parse(messageOutput.body));
                    });
                });
            }
            
            function disconnect() {
                if(stompClient != null) {
                    stompClient.disconnect();
                }
                setConnected(false);
                console.log("Disconnected");
            }
            
            function sendMessage() {
                var from = document.getElementById('from').value;
                var text = document.getElementById('text').value;
                stompClient.send("/app/chat", {}, 
                  JSON.stringify({'from':from, 'text':text}));
            }
            
            function showMessageOutput(messageOutput) {
                var response = document.getElementById('response');
                var p = document.createElement('p');
                p.style.wordWrap = 'break-word';
                p.appendChild(document.createTextNode(messageOutput.from + ": " 
                  + messageOutput.text + " (" + messageOutput.time + ")"));
                response.appendChild(p);
            }
        </script>
    </head>
    <body onload="disconnect()">
        <div>
            <div>
                <input type="text" id="from" placeholder="Choose a nickname"/>
            </div>
            <br />
            <div>
                <button id="connect" onclick="connect();">Connect</button>
                <button id="disconnect" disabled="disabled" onclick="disconnect();">
                    Disconnect
                </button>
            </div>
            <br />
            <div id="conversationDiv">
                <input type="text" id="text" placeholder="Write a message..."/>
                <button id="sendMessage" onclick="sendMessage();">Send</button>
                <p id="response"></p>
            </div>
        </div>

    </body>
</html>

7. Тестирование примера

Чтобы проверить наш пример, мы можем открыть пару окон браузера и получить доступ к странице чата по адресу:

http://localhost:8080

После этого мы можем присоединиться к чату, введя псевдоним и нажав кнопку подключения. Если мы создадим и отправим сообщение, мы увидим его во всех сеансах браузера, присоединившихся к чату.

Взгляните на скриншот, чтобы увидеть пример:

8. Заключение

В этом руководстве мы рассмотрели поддержку WebSocket в Spring. Мы видели его конфигурацию на стороне сервера и создали простой аналог на стороне клиента с использованием библиотек sockjs и stomp Javascript.

Пример кода можно найти в проекте GitHub.