Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您创建一个“Hello, world”应用程序,该应用程序在浏览器和服务器之间来回发送消息。WebSocket 是 TCP 之上的一个轻量级薄层。这使得它适合使用“子协议”来嵌入消息。在本指南中,我们使用 STOMP 消息协议与 Spring 一起创建一个交互式 Web 应用程序。STOMP 是运行在较低级别 WebSocket 之上的子协议。

您将构建的内容

您将构建一个服务器,该服务器接收携带用户姓名的消息。作为响应,服务器将向客户端订阅的队列中推送一条问候信息。

所需条件

如何完成本指南

与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,或者可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都会得到可以运行的代码。

从头开始,请继续查看使用 Spring Initializr 开始

跳过基础知识,请执行以下操作:

完成后,您可以将您的结果与 gs-messaging-stomp-websocket/complete 中的代码进行对比。

从 Spring Initializr 开始

您可以使用这个预初始化项目,然后点击生成以下载一个ZIP文件。该项目已配置为符合本教程中的示例。

要手动初始化项目:

  1. 访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 Websocket

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置好的 Web 应用程序存档。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此过程。

您也可以从 Github 上 fork 该项目,并在您的 IDE 或其他编辑器中打开它。

创建一个资源表示类

现在您已经设置了项目和构建系统,您可以创建您的 STOMP 消息服务了。

首先,从思考服务交互开始这个过程。

该服务将接受包含名称的 STOMP 消息,消息体是一个 JSON 对象。如果名称是 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。

接下来,您将创建一个控制器来接收 hello 消息并发送问候消息。

创建一个消息处理控制器

在 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 消息传输 {#_configure_spring_for_stomp_messaging}

现在服务的基本组件已经创建完毕,您可以配置 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");
  }

}

WebSocketConfig 使用 @Configuration 注解来标识它是一个 Spring 配置类。它还使用了 @EnableWebSocketMessageBroker 注解。顾名思义,@EnableWebSocketMessageBroker 启用了由消息代理支持的 WebSocket 消息处理。

configureMessageBroker() 方法实现了 WebSocketMessageBrokerConfigurer 中的默认方法,用于配置消息代理。它首先调用 enableSimpleBroker() 来启用一个基于内存的简单消息代理,将问候消息传递回客户端,目标地址以 /topic 为前缀。它还指定了 /app 前缀,用于绑定到带有 @MessageMapping 注解的方法。这个前缀将用于定义所有的消息映射。例如,/app/helloGreetingController.greeting() 方法映射处理的端点。

registerStompEndpoints() 方法注册了 /gs-guide-websocket 端点用于 websocket 连接。

创建浏览器客户端

在服务器端组件就位后,您可以将注意力转向 JavaScript 客户端,该客户端将向服务器端发送消息并接收来自服务器端的消息。

创建一个类似于以下代码清单的 index.html 文件(位于 src/main/resources/static/index.html):

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <link href="/main.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.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 文件导入了 StompJS JavaScript 库,该库将用于通过 WebSocket 上的 STOMP 协议与我们的服务器进行通信。我们还导入了 app.js,其中包含了我们客户端应用程序的逻辑。以下代码清单(来自 src/main/resources/static/app.js)展示了该文件的内容:

const stompClient = new StompJs.Client({
    brokerURL: 'ws://localhost:8080/gs-guide-websocket'
});

stompClient.onConnect = (frame) => {
    setConnected(true);
    console.log('Connected: ' + frame);
    stompClient.subscribe('/topic/greetings', (greeting) => {
        showGreeting(JSON.parse(greeting.body).content);
    });
};

stompClient.onWebSocketError = (error) => {
    console.error('Error with websocket', error);
};

stompClient.onStompError = (frame) => {
    console.error('Broker reported error: ' + frame.headers['message']);
    console.error('Additional details: ' + frame.body);
};

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

function connect() {
    stompClient.activate();
}

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

function sendName() {
    stompClient.publish({
        destination: "/app/hello",
        body: JSON.stringify({'name': $("#name").val()})
    });
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

$(function () {
    $("form").on('submit', (e) => e.preventDefault());
    $( "#connect" ).click(() => connect());
    $( "#disconnect" ).click(() => disconnect());
    $( "#send" ).click(() => sendName());
});

理解这个 JavaScript 文件的主要内容是 stompClient.onConnectsendName 函数。

stompClient 使用 brokerURL 初始化,指向路径 /gs-guide-websocket,这是我们的 WebSocket 服务器等待连接的地方。在成功连接后,客户端会订阅 /topic/greetings 目的地,服务器将在此发布问候消息。当在该目的地接收到问候消息时,它会向 DOM 添加一个段落元素来显示问候消息。

sendName() 函数会获取用户输入的名称,并使用 STOMP 客户端将其发送到 /app/hello 目的地(GreetingController.greeting() 将在此接收它)。

如果您愿意,可以省略 main.css,或者创建一个空文件,只要确保 <link> 能够解析即可。

使应用程序可执行

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。例如,如果类路径上有spring-webmvc,此注解会将应用程序标记为Web应用程序,并激活关键行为,例如设置DispatcherServlet

  • @ComponentScan: 告诉Spring在com/example包中查找其他组件、配置和服务,以便它能找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用程序。您是否注意到没有一行 XML 代码?也没有 web.xml 文件。这个 Web 应用程序是 100% 纯 Java 的,您无需处理任何配置管道或基础设施的问题。

构建可执行的 JAR

您可以使用 Gradle 或 Maven 从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的单个可执行 JAR 文件,然后运行该文件。构建可执行 JAR 文件可以方便地在整个开发生命周期、跨不同环境等场景中打包、版本控制和部署服务。

如果您使用 Gradle,可以通过 ./gradlew bootRun 来运行应用程序。或者,您可以使用 ./gradlew build 构建 JAR 文件,然后运行该 JAR 文件,如下所示:

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

如果您使用 Maven,可以通过 ./mvnw spring-boot:run 来运行应用程序。或者,您也可以使用 ./mvnw clean package 构建 JAR 文件,然后按如下方式运行该 JAR 文件:

java -jar target/gs-messaging-stomp-websocket-0.1.0.jar

这里描述的步骤创建了一个可运行的 JAR 文件。您也可以构建一个经典的 WAR 文件

日志输出已显示。服务应在几秒钟内启动并运行。

测试服务

现在服务已经运行,请在浏览器中访问 http://localhost:8080 并点击 Connect 按钮。

当连接打开后,系统会提示您输入姓名。输入您的姓名并点击 Send。您的姓名将通过 STOMP 以 JSON 消息的形式发送到服务器。经过一秒钟的模拟延迟后,服务器会发送一条带有“Hello”问候语的消息,并显示在页面上。此时,您可以发送另一个姓名,或者点击 Disconnect 按钮来关闭连接。

总结

恭喜!您刚刚使用 Spring 开发了一个基于 STOMP 的消息服务。

另请参阅

以下指南可能也会有所帮助:

本页目录