开篇词
该指南将引导你完成创建 “Hello, world” 应用,该应用在浏览器和服务器之间来回发送消息。WebSocket 是 TCP 之上的轻量层。这使其适合使用 “子协议” 来嵌入消息。在该指南汇总,我们将 STOMP 消息与 Spring 结合使用来创建一个交互式 Web 应用。
你将创建的应用
我们将构建一个服务器,该服务器接受带有用户名的消息。作为响应,服务器将问候语推送到客户端已订阅的队列中。
你将需要的工具
- 大概 15 分钟左右;
- 你最喜欢的文本编辑器或集成开发环境(IDE)
- JDK 1.8 或更高版本;
- Gradle 4+ 或 Maven 3.2+
- 你还可以将代码直接导入到 IDE 中:
如何完成这个指南
像大多数的 Spring 入门指南一样,你可以从头开始并完成每个步骤,也可以绕过你已经熟悉的基本设置步骤。如论哪种方式,你最终都有可以工作的代码。
- 要从头开始,移步至从 Spring Initializr 开始;
- 要跳过基础,执行以下操作:
待一切就绪后,可以检查一下 gs-messaging-stomp-websocket/complete
目录中的代码。
从 Spring Initializr 开始
对于所有的 Spring 应用来说,你应该从 Spring Initializr 开始。Initializr 提供了一种快速的方法来提取应用程序所需的依赖,并为你完成许多设置。该示例仅需要 Websocket 依赖。下图显示了此示例项目的 Initializr 设置:
上图显示了选择 Maven 作为构建工具的 Initializr。你也可以使用 Gradle。它还将
com.example
和messaging-stomp-websocket
的值分别显示为 Group 和 Artifact。在本示例的其余部分,将用到这些值。
以下清单显示了选择 Maven 时创建的 pom.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>messaging-stomp-websocket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>messaging-stomp-websocket</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
以下清单显示了在选择 Gradle 时创建的 build.gradle
文件:
plugins {
id 'org.springframework.boot' version '2.2.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
添加依赖
在这种情况下,Spring Initializr 无法提供所需的一切。对于 Maven,我们需要添加以下依赖:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.1.1-1</version>
</dependency>
以下清单显示了最终的 pom.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>messaging-stomp-websocket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>messaging-stomp-websocket</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.1.1-1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
如果我们用的是 Gradle,则需要添加以下依赖:
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:sockjs-client:1.0.2'
implementation 'org.webjars:stomp-websocket:2.3.3'
implementation 'org.webjars:bootstrap:3.3.7'
implementation 'org.webjars:jquery:3.1.1-1'
以下清单显示了最终的 build.gradle
文件:
plugins {
id 'org.springframework.boot' version '2.2.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:sockjs-client:1.0.2'
implementation 'org.webjars:stomp-websocket:2.3.3'
implementation 'org.webjars:bootstrap:3.3.7'
implementation 'org.webjars:jquery:3.1.1-1'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
创建资源展示类
现在我们已经搭建了项目和构建系统,我们可以创建 STOMP 消息服务了。
通过考虑服务交互来开始该过程。
该服务将接受正文为 JSON 对象的 STOMP 中包含名称的消息。如果名称为 Fred,则消息可能类似于以下内容:
{
"name": "Fred"
}
要对带有名称的消息进行建模,可以创建一个带有 name
属性和对应的 getName()
方法的普通 Java 对象,如下所示(来自 src/main/java/com/example/messagingstompwebsocket/HelloMessage.java
):
package com.example.messagingstompwebsocket;
public class HelloMessage {
private String name;
public HelloMessage() {
}
public HelloMessage(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
收到消息并提取名称后,服务将通过创建问候语并将该问候语发不到客户端已订阅的单独队列上来对其进行处理。问候语还将是一个 JSON 对象,如下清单所示:
{
"content": "Hello, Fred!"
}
要对问候展示进行建模,请添加另一个具有 content
属性和对应的 getContent()
方法的普通 Java 对象,如下面的清单(来自 src/main/java/com/example/messagingstompwebsocket/Greeting.java
)所示:
package com.example.messagingstompwebsocket;
public class Greeting {
private String content;
public Greeting() {
}
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
Spring 将使用 Jackson JSON 库自动将类型 Greeting
的实例封装为 JSON。
接下来,我们将创建一个控制器来接收问候消息并发送问候消息。
创建消息处理控制器
在 Spring 处理 STOMP 消息的方法中,可以将 STOMP 消息路由到 @Controller
类。例如,GreetingController
(来自 src/main/java/com/example/messagingstompwebsocket/GreetingController.java
)被映射为处理发送给 /hello
目标的消息,如以下清单所示:
package com.example.messagingstompwebsocket;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
}
该控制器简洁明了,但后续仍有很多东西要添加。我们将其逐步分解。
@MessageMapping
注解可确保如果将消息发送到 /hello
目标地址,则将调用 greeting()
方法。
消息的有效负载绑定到一个 HelloMessage
对象,该对象传递到 greeting()
中。
在内部,该方法的实现通过使线程休眠一秒钟来模拟处理延迟。这表明客户端发送消息后,服务器可以花费其异步处理消息所需的时间。客户无需等待响应即可继续进行所需的任何工作。
一秒钟的延迟后,greeting()
方法创建一个 Greeting
对象并返回它。返回值将被广播给 /topic/greetings
的所有订阅者,如 @SendTo
注解中所指定的。请注意,输入消息中的名称已被清除,因为在这种情况下,它将在客户端的浏览器 DOM 中回显。
为 Spring 配置 STOMP 消息收发
现在已经创建了服务的基本组件,我们可以配置 Spring 以启用 WebSocket 和 STOMP 消息传递。
创建一个名为 WebSocketConfig
的 Java 类,该类类似于以下清单(来自 src/main/java/com/example/messagingstompwebsocket/WebSocketConfig.java
):
package com.example.messagingstompwebsocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@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("/gs-guide-websocket").withSockJS();
}
}
WebSocketConfig
带有 @Configuration
注解,以指示它是 Spring 配置类。它也用 @EnableWebSocketMessageBroker
注解。顾名思义,@EnableWebSocketMessageBroker
启用由消息代理支持的 WebSocket 消息处理。
configureMessageBroker()
方法实现 WebSocketMessageBrokerConfigurer
中的默认方法以配置消息代理。首先,通过调用 enableSimpleBroker()
来启用基于内存的简单消息代理,以将问候消息在以 /topic
为前缀的目标地址上携带回客户端。它还为与 @MessageMapping
注解的方法绑定的消息指定 /app
前缀。该前缀将用于定义所有消息映射。例如 /app/hello
是由 GreetingController.greeting()
方法映射来处理的端点。
registerStompEndpoints()
方法注册 /gs-guide-websocket
端点,启用 SockJS 后备选项,以便在 WebSocket 不可用时可以使用备用传输。SockJS 客户端将尝试连接到 /gs-guide-websocket
并使用最佳的可用传输方式(websocket、xhr-streaming、xhr-polling 等等)。
创建浏览器客户端
在服务器端组建就绪后,我们可以将注意力转移到 JavaScript 客户端上,该客户端将向服务端发送消息并从服务器端接收消息。
创建类似于以下清单的 index.html
文件(来自 src/main/resources/static/index.html
):
<!DOCTYPE html>
<html>
<head>
<title>Hello WebSocket</title>
<link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/main.css" rel="stylesheet">
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/app.js"></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 id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="connect">WebSocket connection:</label>
<button id="connect" class="btn btn-default" type="submit">Connect</button>
<button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
</button>
</div>
</form>
</div>
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="name">What is your name?</label>
<input type="text" id="name" class="form-control" placeholder="Your name here...">
</div>
<button id="send" class="btn btn-default" type="submit">Send</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table table-striped">
<thead>
<tr>
<th>Greetings</th>
</tr>
</thead>
<tbody id="greetings">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
该 HTML 文件导入 SockJS
和 STOMP
这两个 JS 库,这些库将用于通过 websocket 使用 STOMP 与我们的服务器进行通信。我们还导入 app.js,其中包含客户端应用的逻辑。以下清单(来自 src/main/resources/static/app.js
)显示了该文件:
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#greetings").html("");
}
function connect() {
var socket = new SockJS('/gs-guide-websocket');
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() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
function showGreeting(message) {
$("#greetings").append("<tr><td>" + message + "</td></tr>");
}
$(function () {
$("form").on('submit', function (e) {
e.preventDefault();
});
$( "#connect" ).click(function() { connect(); });
$( "#disconnect" ).click(function() { disconnect(); });
$( "#send" ).click(function() { sendName(); });
});
该 JS 文件的主要部分是 connect()
和 sendName()
函数。
connect()
函数使用 SockJS 和 stomp.js 来打开通向 /gs-guide-websocket
的连接,这是我们的 SockJS 服务器等待连接的地方。连接成功后,客户端将预定 /topic/greetings
目标,服务器将在该目标中发布问候消息。在该目标地址上收到问候语时,它将在 DOM 后面添加一个段落元素以显示问候语消息。
sendName()
函数检索用户输入的名称,并使用 STOMP 客户端将其发送到 /app/hello
目标地址(GreetingController.greeting()
将对其进行接收操作)。
使应用可执行
Spring Boot 为我们创建了一个应用类。在这种情况下,无需进一步修改。我们可以使用它来运行该应用。以下清单(来自 src/main/java/com/example/messagingstompwebsocket/MessagingStompWebsocketApplication.java
)显示了应用类:
package com.example.messagingstompwebsocket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MessagingStompWebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(MessagingStompWebsocketApplication.class, args);
}
}
@SpringBootApplication
是一个便利的注解,它添加了以下所有内容:
@Configuration
:将类标记为应用上下文 Bean 定义的源;@EnableAutoConfiguration
:告诉 Spring Boot 根据类路径配置、其他 bean 以及各种属性的配置来添加 bean。@ComponentScan
:告知 Spring 在com/example
包中寻找他组件、配置以及服务。
main()
方法使用 Spring Boot 的 SpringApplication.run()
方法启动应用。
构建可执行 JAR
我们可以结合 Gradle 或 Maven 来从命令行运行该应用。我们还可以构建一个包含所有必须依赖项、类以及资源的可执行 JAR 文件,然后运行该文件。在整个开发生命周期中,跨环境等等情况下,构建可执行 JAR 可以轻松地将服务作为应用进行发布、版本化以及部署。
如果使用 Gradle,则可以借助 ./gradlew bootRun
来运行应用。或通过借助 ./gradlew build
来构建 JAR 文件,然后运行 JAR 文件,如下所示:
java -jar build/libs/gs-messaging-stomp-websocket-0.1.0.jar
由官网提供的以上这条命令的执行结果与我本地的不一样,我需要这样才能运行:
java -jar build/libs/messaging-stomp-websocket-0.0.1-SNAPSHOT.jar
。
如果使用 Maven,则可以借助 ./mvnw spring-boot:run
来运行该用。或可以借助 ./mvnw clean package
来构建 JAR 文件,然后运行 JAR 文件,如下所示:
java -jar target/gs-messaging-stomp-websocket-0.1.0.jar
由官网提供的以上这条命令的执行结果与我本地的不一样,我需要这样才能运行:
java -jar target/messaging-stomp-websocket-0.0.1-SNAPSHOT.jar
。
我们还可以将 JAR 应用转换成 WAR 应用。
显示日志记录输出。该服务应在几秒内启动并运行。
测试服务
现在该服务正在运行中,将浏览器指向 http://localhost:8080 并单击 “Connect” 按钮。
打开连接后,系统会要求我们输入名称。输入后单击 Send。我们的姓名将通过 STOMP 作为 JSON 消息发送到服务器。经过一秒的模拟延迟后,服务器将向页面发送回带有 “Hello” 问候语的消息,该问候语显示在页面上。此时,我们可以发送其他名称,也可以单击 Disconnect 按钮以关闭连接。
概述
恭喜你!我们刚刚使用 Spring 开发了基于 STOMP 的消息服务。
参见
以下指南也可能会有所帮助:
想看指南的其他内容?请访问该指南的所属专栏:《Spring 官方指南》