Учебные материалы

Использование WebSocket для создания интерактивных web-приложений

В этом уроке освещается процесс создания "hello world" приложения, которое отправляет сообщения назад и вперед, между сервером и браузером. WebSocket является очень тонким и легковесным слоем над TCP. Это делает его очень подходящим при использования "подпротоколов" для вставки сообщений. В этом уроке мы изучим и используем обмен сообщений через STOMP c использованием Spring для создания интерактивного web приложения.

Что вы создадите

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

Что вам потребуется

  • Примерно 15 минут свободного времени
  • Любимый текстовый редактор или IDE
  • JDK 6 и выше
  • Gradle 1.11+ или Maven 3.0+
  • Вы также можете импортировать код этого урока, а также просматривать web-страницы прямо из Spring Tool Suite (STS), собственно как и работать дальше из него.

Как проходить этот урок

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

Чтобы начать с нуля, перейдите в Настройка проекта.

Чтобы пропустить базовые шаги, выполните следующее:

Когда вы закончите, можете сравнить получившийся результат с образцом в gs-messaging-stomp-websocket/complete.

Настройка проекта

Для начала вам необходимо настроить базовый скрипт сборки. Вы можете использовать любую систему сборки, которая вам нравится для сборки проетов Spring, но в этом уроке рассмотрим код для работы с Gradle и Maven. Если вы не знакомы ни с одним из них, ознакомьтесь с соответсвующими уроками Сборка Java-проекта с использованием Gradle или Сборка Java-проекта с использованием Maven.

Создание структуры каталогов

В выбранном вами каталоге проекта создайте следующую структуру каталогов; к примеру, командой mkdir -p src/main/java/hello для *nix систем:

└── src
    └── main
        └── java
            └── hello

Создание файла сборки Gradle

Ниже представлен начальный файл сборки Gradle. Файл pom.xml находится здесь. Если вы используете Spring Tool Suite (STS), то можете импортировать урок прямо из него.

Если вы посмотрите на pom.xml, вы найдете, что указана версия для maven-compiler-plugin. В общем, это не рекомендуется делать. В данном случае он предназначен для решения проблем с нашей CI системы, которая по умолчанию имеет старую(до Java 5) версию этого плагина.

build.gradle

buildscript {
    repositories {
        maven { url "http://repo.spring.io/libs-release" }
        mavenLocal()
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.8.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'gs-messaging-stomp-websocket'
    version =  '0.1.0'
}
sourceCompatibility = 1.7
targetCompatibility = 1.7

repositories {
    mavenLocal()
    mavenCentral()
    maven { url "http://repo.spring.io/libs-release" }
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-websocket")
    compile("org.springframework:spring-messaging")
    testCompile("junit:junit")
}

task wrapper(type: Wrapper) {
    gradleVersion = '1.11'
}

Spring Boot gradle plugin предоставляет множество удобных возможностей:

  • Он собирает все jar'ы в classpath и собирает единое, исполняемое "über-jar", что делает более удобным выполнение и доставку вашего сервиса
  • Он ищет public static void main() метод, как признак исполняемого класса
  • Он предоставляет встроенное разрешение зависимостей, с определенными номерами версий для соответсвующих Spring Boot зависимостей. Вы можете переопределить на любые версии, какие захотите, но он будет по умолчанию для Boot выбранным набором версий

Создание класса представления ресурса

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

Начнем с процесса взаимодействия с сервисом.

Сервис будет принимать содержащие имя STOMP сообщения, тела которых представляют собой в JSON объекты:

{ "name": "Fred" }

Чтобы смоделировать сообщение, содержащее имя, вы можете создать POJO с полем name и соответствующим методом getName():

src/main/java/hello/HelloMessage.java

package hello;

public class HelloMessage {

    private String name;

    public String getName() {
        return name;
    }

}

После получения сообщения и извлечении имени, сервис создаст на его основе приветствие и опубликует его в отдельной очереди, на которую подписан клиент. Приветствие также будет в форме JSON объекта, который выглядит примерно так:

{ "content": "Hello, Fred!" }

Чтобы смоделировать представление приветствия, вы добавляете другой POJO с полем content и соответствующим методом getContent():

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

}

Spring будет использовать библиотеку Jackson JSON для автоматической упаковки экземпляров типа Greeting в JSON.

Далее вы создадите контроллер для приема hello сообщения и отправки сообщения приветствия.

Создание контроллера обработки сообщения

В подходе Spring при работе с STOMP сообщениями, STOMP сообщения могут быть перенаправлены к @Controller классам. К примеру, GreetingController обрабатывает сообщения для /hello.

src/main/java/hello/GreetingController.java

package hello;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class GreetingController {


    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(3000); // simulated delay
        return new Greeting("Hello, " + message.getName() + "!");
    }

}

контроллер краток и прост, но там много чего происходит. Давайте подробнее рассмотрим шаг за шагом.

@RequestMapping аннотация гарантирует, что если сообщение отправляется на /hello, то будет вызван greeting() метод.

Полезная часть сообщения привязана к HelloMessage объекту, который передается в greeting().

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

После трехсекундной задержки, метод greeting() создает объект Greeting и возвращает его. Возвращаемое значение рассылается всем подписчикам на /topic/greetings, как это определено в аннотации @sendTo.

Настройка Spring для STOMP обмена сообщениями

Теперь, когда основные компоненты сервиса созданы, вы можете настроить Spring, чтобы включить WEbSocket и обмен сообщениями по STOMP.

Создайте Java класс WebSocketConfig как показано ниже:

src/main/java/hello/WebSocketConfig.java

package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@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("/hello").withSockJS();
	}

}

WebSocketConfig аннотирован @Configuration, означая, что это конфигурационный Spring класс. Он также аннотирован @EnableWebSocketMessageBroker. Как и предполагает его название @EnableWebSocketMessageBroker включает обработке сообщений по WebSocket, возвращаемый брокером сообщений.

Метод configureMessageBroker() переопределяет поведение по умолчанию в WebSocketMessageBrokerConfigurer для настройки брокера сообщений. Он вызывает enableSimpleBroker() для включения простого брокера сообщений в памятичтобы возвращать обратно сообщения клиенту по направлениям с префиксом /topic. Он также объявляет префикс /app для сообщений, привязанных к методам, аннотированными @MessageMapping.

Метод registerStompEndpoints() регистрирует /hello, включая дополнительно SockJS как альтернативный вариант обмена сообщениями, когда WebSocket не доступен.

Создание браузерного клиента

Серверная часть на месте, теперь давайте обратим наше внимание на Javascript клиент, который будет отправлять сообщения к и получать от сервера. Создайте index.html, как показано ниже:

src/main/resources/static/index.html

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <script src="sockjs-0.3.4.js"></script>
    <script src="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('/hello');
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function(frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                stompClient.subscribe('/topic/greetings', function(greeting){
                    showGreeting(JSON.parse(greeting.body).content);
                });
            });
        }

        function disconnect() {
            stompClient.disconnect();
            setConnected(false);
            console.log("Disconnected");
        }

        function sendName() {
            var name = document.getElementById('name').value;
            stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
        }

        function showGreeting(message) {
            var response = document.getElementById('response');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            response.appendChild(p);
        }
    </script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect();">Connect</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
    </div>
    <div id="conversationDiv">
        <label>What is your name?</label><input type="text" id="name" />
        <button id="sendName" onclick="sendName();">Send</button>
        <p id="response"></p>
    </div>
</div>
</body>
</html>

Главную часть HTML файла играет JavaScript код в функциях connect() и sendName().

Функция connect() использует SockJS и stomp.js для открытия соединения к /gs-messaging-stomp-websocket/hello, где GreetingController ожидает соединений. После успешного соединения, она подписывается на /topic/greetings, где сервер публикует сообщения приветствия. Когда сообщение будет получено, функция добавит элемент параграфа в DOM для отображения сообщения приветствия.

Функция sendName() получает имя, введенное пользователем и используется STOMP клиентом для отправки к /app/helloGreetingController.greeting() получит его).

Создание приложения исполняемым

Несмотря на то, что пакет этого сервиса может быть в составе web-приложения и WAR файлов, более простой подход, продемонстрированный ниже создает отдельное самостоятельное приложение. Вы упаковываете все в единый, исполняемый JAR-файл, который запускается через хорошо знакомый старый main() Java-метод. Попутно, вы используете поддержку Spring для встроенного Tomcat контейнера сервлетов как HTTP среду выполнения вместо развертывания на сторонний экземпляр.

src/main/java/hello/Application.java

package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Метод main() передает управление вспомогательному классу SpringApplication, предоставляя Application.class как аргумент его run() методу. Это говорит Spring о том, чтобы прочитать аннотацию метаданных из Application и управлять им как компонентом в Spring Application Context.

Аннотация @ComponentScan сообщает Spring о запуске рекурсивного поиска в пакете hello и потомках классов, отмеченных прямо или косвенно Spring аннотацией @Component. При этом гарантируется, что Spring найдет и зарегистрирует GreetingController, потому что он отмечен @Controller, что в свою очередь является своего рода @Component аннотацией.

@EnableAutoConfiguration аннотация переключает на доступные по умолчанию настройки, основанные на содержимом вашего classpath. К примеру, т.к. приложение зависит от встраиваемой версии Tomcat(tomcat-embed-core.jar), то Tomcat сервер установлен и настроен по умолчанию от вашего имени. И также, т.к. приложение зависит от Spring MVC (spring-webmvc.jar), Spring MVC DispatcherServlet настроен и зарегистрирован за вас - web.xml не нужен! Поэтому здесь MultipartConfigElement, он настроен DispatcherServlet с функциональностью загрузки файлов. Автонастройка является мощным и гибким механизмом. Более подробно вы можете ознакомиться в API документации.

Сборка исполняемого JAR

Вы можете собрать единый исполняемый JAR-файл, который содержит все необходимые зависимости, классы и ресурсы. Это делает его легким в загрузке, версионировании и развертывании сервиса как приложения на протяжении всего периода разработки, на различных средах и так далее.

./gradlew build

Затем вы можете запустить JAR-файл:

java -jar build/libs/gs-messaging-stomp-websocket-0.1.0.jar

Если вы используете Maven, вы можете запустить приложение, используя mvn spring-boot:run, либо вы можете собрать приложение с mvn clean package и запустить JAR примерно так:

java -jar target/gs-messaging-stomp-websocket-0.1.0.jar
Процедура, описанная выше, создает исполняемый JAR. Вы также можете вместо него собрать классический WAR-файл.

Если вы используете Gradle, вы можете запустить ваш сервис из командной строки:

./gradlew clean build && java -jar build/libs/gs-messaging-stomp-websocket-0.1.0.jar
Если вы используете Maven, то можете запустить ваш сервис таким образом: mvn clean package && java -jar target/gs-messaging-stomp-websocket-0.1.0.jar.

Как вариант, вы можете запустить ваш сервис напрямую из Gradle примерно так:

./gradlew bootRun
С mvn - mvn spring-boot:run.

Сервис должен быть поднят и запущен через несколько секунд.

Тестирование сервиса

Теперь, когда сервис поднят и запущен, зайдите по адресу http://localhost:8080 и нажмите кнопку "Connect".

после открытия соединения вас попросят ввести свое имя. Введите его и нажмите кнопку "Send". Ваше имя отправится серверу как JSON сообщение поверх STOMP. После 3-х секундной задержки сервер отправит обратно сообщение с "Hello" приветствием, которое отобразится на странице. На данном этапе вы можете отправить другое имя или нажать на кнопку "Disconnect" для закрытия соединения.

Итог

Поздравляем! Вы только что разработали STOMP-сервис обмена сообщениями, используя Spring.

С оригинальным текстом урока вы можете ознакомиться на spring.io.

comments powered by Disqus