基于 Servlet API 并部署到 Servlet 容器(四)

时间:2022-11-18 19:02:48

基于 Servlet API 并部署到 Servlet 容器(四)

2. REST 客户端

本节介绍客户端访问 REST 终结点的选项。

2.1. ​​RestTemplate​

​RestTemplate​​是执行 HTTP 请求的同步客户端。它是原始的 Spring REST 客户端,并在底层 HTTP 客户端上公开一个简单的模板方法 API 图书馆。

2.2. ​​WebClient​

​WebClient​​是执行 HTTP 请求的非阻塞、反应式客户端。它是 在 5.0 中引入,并提供了一种现代替代方案,具有高效的 支持同步和异步以及流式处理方案。​​RestTemplate​

相反,支持以下内容:​​RestTemplate​​​​WebClient​

  • 非阻塞 I/O。
  • 反应流背压。
  • 高并发性,更少的硬件资源。
  • 函数式、流畅的 API,利用了 Java 8 lambda。
  • 同步和异步交互。
  • 从服务器流式传输或向式传输。

有关更多详细信息,请参阅网络客户端。

2.3. HTTP接口

Spring 框架允许您将 HTTP 服务定义为带有 HTTP 的 Java 接口 交换方法。然后,您可以生成实现此接口的代理,并且 执行交换。这有助于简化 HTTP 远程访问并提供额外的 灵活选择 API 样式,例如同步或反应式。

有关详细信息,请参阅REST 终结点。

3. 测试

在Spring WebFlux中相同

本节总结了Spring MVC应用程序中可用的选项。​​spring-test​

  • Servlet API Mocks:单元测试控制器的 Servlet API 合约的模拟实现, 筛选器和其他 Web 组件。有关更多详细信息,请参阅Servlet API模拟对象。
  • TestContext Framework:支持在 JUnit 和 TestNG 测试中加载 Spring 配置, 包括跨测试方法对加载的配置进行高效缓存,并支持 加载 awith a. 有关更多详细信息,请参阅TestContext 框架。WebApplicationContextMockServletContext
  • Spring MVC 测试:用于测试带注释的控制器的框架,也称为 通过(即支持注释),完成与 Spring MVC 基础架构,但没有 HTTP 服务器。 有关更多详细信息,请参阅Spring MVC 测试。MockMvcDispatcherServlet
  • 客户端 REST:提供可以用作 用于测试内部使用的客户端代码的模拟服务器。 有关更多详细信息,请参阅客户端 REST 测试。spring-testMockRestServiceServerRestTemplate
  • ​WebTestClient​​:专为测试 WebFlux 应用程序而构建,但也可用于 通过 HTTP 连接对任何服务器进行端到端集成测试。这是一个 非阻塞、反应式客户端,非常适合测试异步和流式处理 场景。

4. 网络套接字

网络通量

参考文档的这一部分涵盖了对Servlet堆栈,WebSocket的支持。 消息传递,包括原始 WebSocket 交互、通过 SockJS 的 WebSocket 仿真,以及 通过 STOMP 作为 WebSocket 上的子协议发布-订阅消息传递。

= WebSocket 简介

WebSocket协议RFC 6455提供了一个标准化的 在客户端和服务器之间建立全双工双向通信通道的方法 通过单个 TCP 连接。它是与HTTP不同的TCP协议,但旨在 通过 HTTP 工作,使用端口 80 和 443,并允许重用现有防火墙规则。

WebSocket 交互以使用 HTTPheader 的 HTTP 请求开始 以升级,或者在这种情况下,切换到 WebSocket 协议。以下示例 显示了这样的交互:​​Upgrade​

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

标题。​​Upgrade​

使用连接。​​Upgrade​

支持 WebSocket 的服务器返回输出,而不是通常的 200 状态代码 类似于以下内容:

HTTP/1.1 101 Switching Protocols 
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

协议切换

握手成功后,HTTP 升级请求的 TCP 套接字将保留 为客户端和服务器打开以继续发送和接收消息。

有关 WebSocket 工作原理的完整介绍超出了本文档的范围。 请参阅 RFC 6455、HTML5 的 WebSocket 章节,或许多介绍中的任何一个和 网络上的教程。

请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx)后面运行,则 可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,请检查 与 WebSocket 支持相关的云提供商的说明。

== HTTP 与 WebSocket

即使 WebSocket 被设计为与 HTTP 兼容并且以 HTTP 请求开始, 重要的是要了解这两种协议导致非常不同的 体系结构和应用程序编程模型。

在 HTTP 和 REST 中,应用程序被建模为多个 URL。要与应用程序交互, 客户端访问这些 URL,请求-响应样式。服务器将请求路由到 基于 HTTP URL、方法和标头的适当处理程序。

相比之下,在 WebSocket 中,初始连接通常只有一个 URL。 随后,所有应用程序消息都在同一 TCP 连接上流动。这指向 一种完全不同的异步、事件驱动的消息传递体系结构。

WebSocket也是一种低级传输协议,与HTTP不同,它没有规定 消息内容的任何语义。这意味着无法路由或处理 消息,除非客户端和服务器在消息语义上达成一致。

WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议 (例如,STOMP),通过 HTTP 握手请求上的标头。 如果没有这一点,他们需要提出自己的公约。​​Sec-WebSocket-Protocol​

== 何时使用 WebSockets

WebSockets可以使网页具有动态性和交互性。但是,在许多情况下, Ajax 和 HTTP 流或长轮询的组合可以提供简单和 有效的解决方案。

例如,新闻、邮件和社交源需要动态更新,但可能是 完全可以每隔几分钟这样做一次。协作、游戏和金融应用, 另一方面,需要更接近实时。

延迟本身并不是决定性因素。如果消息量相对较低(例如, 监控网络故障)HTTP 流或轮询可以提供有效的解决方案。 低延迟、高频和高容量的组合使最佳 使用 WebSocket 的案例。

另请记住,在互联网上,您无法控制的限制性代理 可能会排除 WebSocket 交互,因为它们未配置为传递标头,或者因为它们关闭了显示为空闲的长期连接。这 意味着将WebSocket用于防火墙内的内部应用程序是一个更多的 比面向公众的应用程序更直接的决定。​​Upgrade​

4.1. 网络套接字接口

网络通量

Spring 框架提供了一个 WebSocket API,您可以使用它来编写客户端和 处理 WebSocket 消息的服务器端应用程序。

4.1.1. ​​WebSocketHandler​

网络通量

创建 WebSocket 服务器就像实现器一样简单,更多 很可能,扩展任一。以下 示例用途:​​WebSocketHandler​​​​TextWebSocketHandler​​​​BinaryWebSocketHandler​​​​TextWebSocketHandler​

public class MyHandler extends TextWebSocketHandler {

@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}

}

有专用的 WebSocket Java 配置和 XML 命名空间支持,用于映射前面的内容 特定 URL 的 WebSocket 处理程序,如以下示例所示:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>

<bean class="org.springframework.samples.MyHandler"/>

</beans>

前面的示例用于Spring MVC应用程序,应包含在内 在DispatcherServlet 的配置中。然而,春天的 WebSocket 支持不依赖于 Spring MVC。相对简单的是 在WebSocketHttpRequestHandler的帮助下集成到其他HTTP服务环境中。​​WebSocketHandler​

当直接使用API与间接使用API时,例如通过STOMP消息传递,应用程序必须同步消息的发送 因为底层标准 WebSocket 会话 (JSR-356) 不允许并发 发送。一种选择是使用ConcurrentWebSocketSessionDecorator 包装。​​WebSocketHandler​​​​WebSocketSession​

4.1.2. 网络套接字握手

网络通量

自定义初始 HTTP WebSocket 握手请求的最简单方法是通过 a,它公开了握手之前和“之后”的方法。 您可以使用此类拦截器来排除握手或创建任何属性 可用到。以下示例使用内置侦听器 要将 HTTP 会话属性传递给 WebSocket 会话,请执行以下操作:​​HandshakeInterceptor​​​​WebSocketSession​

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>

<bean class="org.springframework.samples.MyHandler"/>

</beans>

更高级的选项是扩展该执行 WebSocket 握手的步骤,包括验证客户端源, 谈判子议定书和其他细节。应用程序可能还需要使用它 选项,如果它需要配置自定义以便 适应尚不支持的 WebSocket 服务器引擎和版本 (有关此主题的详细信息,请参阅部署)。 Java 配置和 XML 命名空间都使配置自定义成为可能。​​DefaultHandshakeHandler​​​​RequestUpgradeStrategy​​​​HandshakeHandler​

4.1.3. 部署

Spring WebSocket API很容易集成到Spring MVC应用程序中,其中 同时提供 HTTP WebSocket 握手和其他 HTTP 请求。它也很容易集成到其他 HTTP 处理场景中 通过调用。这既方便又容易 理解。但是,JSR-356 运行时需要特别注意。​​DispatcherServlet​​​​WebSocketHttpRequestHandler​

Java WebSocket API(JSR-356)提供了两种部署机制。第一个 涉及在启动时进行 Servlet 容器类路径扫描(Servlet 3 功能)。 另一个是在 Servlet 容器初始化时使用的注册 API。 这两种机制都无法使用单个“前端控制器” 用于所有 HTTP 处理 — 包括 WebSocket 握手和所有其他 HTTP 请求 — 例如 Spring MVC 的请求。​​DispatcherServlet​

这是JSR-356的一个重大限制,Spring的WebSocket支持解决了 特定于服务器的实现,即使在 JSR-356 运行时也是如此。 Tomcat,Jetty,GlassFish,WebLogic,WebSphere和 暗流(和野蝇)。​​RequestUpgradeStrategy​

第二个考虑因素是支持 JSR-356 的 Servlet 容器是预期的。 执行可能会减慢应用程序速度的 (SCI) 扫描 创业 - 在某些情况下,戏剧性。如果在 升级到支持 JSR-356 的 Servlet 容器版本,它应该 可以有选择地启用或禁用 Web 片段(和 SCI 扫描) 通过使用元素,如以下示例所示:​​ServletContainerInitializer​​​​<absolute-ordering />​​​​web.xml​

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">

<absolute-ordering/>

</web-app>

然后,您可以有选择地按名称启用Web片段,例如Spring自己的为Servlet 3提供支持。 Java 初始化 API。以下示例演示如何执行此操作:​​SpringServletContainerInitializer​

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">

<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>

</web-app>

4.1.4. 服务器配置

网络通量

每个基础 WebSocket 引擎都公开控制 运行时特征,例如消息缓冲区大小、空闲超时、 等。

对于Tomcat,WildFly和GlassFish,您可以将a添加到您的 WebSocket Java 配置,如以下示例所示:​​ServletServerContainerFactoryBean​

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>

</beans>

对于码头,您需要提供预配置的码头插头 通过你的WebSocket Java配置进入Spring。 以下示例演示如何执行此操作:​​WebSocketServerFactory​​​​DefaultHandshakeHandler​

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}

@Bean
public DefaultHandshakeHandler handshakeHandler() {

WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);

return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/echo" handler="echoHandler"/>
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>

<bean class="org.springframework...DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>

<bean class="org.springframework...JettyRequestUpgradeStrategy">
<constructor-arg ref="serverFactory"/>
</bean>

<bean class="org.eclipse.jetty...WebSocketServerFactory">
<constructor-arg>
<bean class="org.eclipse.jetty...WebSocketPolicy">
<constructor-arg value="SERVER"/>
<property name="inputBufferSize" value="8092"/>
<property name="idleTimeout" value="600000"/>
</bean>
</constructor-arg>
</bean>

</beans>

4.1.5. 允许的来源

网络通量

从 Spring Framework 4.1.5 开始,WebSocket 和 SockJS 的默认行为是接受 仅同源请求。也可以允许所有或指定的源列表。 此检查主要针对浏览器客户端设计。没有什么能阻止其他类型 的客户端修改标头值(有关更多详细信息,请参阅RFC 6454:Web 起源概念)。​​Origin​

三种可能的行为是:

  • 仅允许同源请求(默认):在此模式下,启用 SockJS 时, Iframe HTTP 响应标头设置为 JSONP 传输被禁用,因为它不允许检查请求的来源。 因此,启用此模式时不支持 IE6 和 IE7。X-Frame-OptionsSAMEORIGIN
  • 允许指定的源列表:每个允许的源必须以 or 开头。在此模式下,启用 SockJS 时,将禁用 IFrame 传输。 因此,在以下情况下不支持 IE6 到 IE9 模式已启用。http://https://
  • 允许所有源:要启用此模式,应提供允许的源 价值。在此模式下,所有传输都可用。*

您可以配置 WebSocket 和 SockJS 允许的源,如以下示例所示:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>

<bean class="org.springframework.samples.MyHandler"/>

</beans>

4.2. SockJS 回退

在公共互联网上,您无法控制的限制性代理可能会排除 WebSocket 交互,因为它们未配置为传递标头或 因为它们关闭了看似空闲的长期连接。​​Upgrade​

此问题的解决方案是 WebSocket 仿真,即尝试使用 WebSocket。 首先,然后回退到模拟WebSocket的基于HTTP的技术 交互并公开相同的应用程序级 API。

在Servlet堆栈上,Spring 框架同时提供服务器(和客户端)支持 对于 SockJS 协议。

4.2.1. 概述

SockJS的目标是让应用程序使用WebSocket API,但回退到。 在运行时需要时,非 WebSocket 替代方案,无需 更改应用程序代码。

SockJS包括:

  • 以可执行叙述测试的形式定义的SockJS 协议。
  • SockJS JavaScript 客户端 — 用于浏览器的客户端库。
  • SockJS服务器实现,包括Spring Frameworkmodule中的一个。spring-websocket
  • 模块中的SockJS Java客户端(从4.1版本开始)。spring-websocket

SockJS是为在浏览器中使用而设计的。它使用多种技术 支持各种浏览器版本。 有关 SockJS 传输类型和浏览器的完整列表,请参阅SockJS 客户端页面。运输 分为三大类:WebSocket、HTTP 流和 HTTP 长轮询。 有关这些类别的概述,请参阅此博客文章。

SockJS 客户端首先发送至 从服务器获取基本信息。之后,它必须决定什么运输方式 来使用。如果可能,将使用 WebSocket。如果没有,在大多数浏览器中, 至少有一个 HTTP 流式处理选项。如果不是,则 HTTP(长) 使用轮询。​​GET /info​

所有传输请求都具有以下 URL 结构:

https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

哪里:

  • ​{server-id}​​可用于在集群中路由请求,但不用于其他用途。
  • ​{session-id}​​关联属于 SockJS 会话的 HTTP 请求。
  • ​{transport}​​指示传输类型(例如,,,等)。websocketxhr-streaming

WebSocket 传输只需要一个 HTTP 请求即可执行 WebSocket 握手。 此后的所有消息都在该套接字上交换。

HTTP 传输需要更多请求。例如,Ajax/XHR 流依赖于 一个长时间运行的请求,用于服务器到客户端消息和其他 HTTP POST 客户端到服务器消息的请求。长轮询类似,只是它 在每次服务器到客户端发送后结束当前请求。

SockJS添加了最少的消息框架。例如,服务器最初发送字母(“打开”帧),消息作为(JSON编码数组)发送,如果没有消息流,则发送字母(“心跳”帧) 25 秒(默认情况下),以及字母(“关闭”框架)关闭会话。​​o​​​​a["message1","message2"]​​​​h​​​​c​

若要了解详细信息,请在浏览器中运行示例并观察 HTTP 请求。 SockJS客户端允许修复传输列表,因此可以 一次查看一个传输。SockJS 客户端还提供了一个调试标志, 这会在浏览器控制台中启用有用的消息。在服务器端,您可以启用日志记录。 有关更多详细信息,请参阅 SockJS 协议叙述测试。​​TRACE​​​​org.springframework.web.socket​

4.2.2. 启用 SockJS

您可以通过 Java 配置启用 SockJS,如以下示例所示:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>

<bean class="org.springframework.samples.MyHandler"/>

</beans>

前面的示例用于Spring MVC应用程序,应包含在 DispatcherServlet 的配置。然而,Spring的WebSocket SockJS支持不依赖于Spring MVC。相对简单的是 在SockJsHttpRequestHandler 的帮助下集成到其他 HTTP 服务环境中。

在浏览器端,应用程序可以使用sockjs-client(版本 1.0.x)。它 模拟 W3C WebSocket API 并与服务器通信以选择最佳 传输选项,具体取决于它运行的浏览器。请参阅sockjs-client页面和 浏览器支持的传输类型。客户端还提供了几个 配置选项 — 例如,指定要包括的传输。

4.2.3. IE 8 和 9

Internet Explorer 8 和 9 仍在使用中。他们是 拥有SockJS的一个关键原因。本节介绍重要的 有关在这些浏览器中运行的注意事项。

SockJS客户端通过使用Microsoft的XDomainRequest在IE 8和9中支持Ajax/XHR流。 这适用于跨域,但不支持发送 Cookie。 Cookie 对于 Java 应用程序通常是必不可少的。 但是,由于SockJS客户端可以与许多服务器一起使用 类型(不仅仅是Java类型),它需要知道cookie是否重要。 如果是这样,SockJS 客户端更喜欢 Ajax/XHR 进行流式传输。否则,它 依赖于基于 iframe 的技术。

来自 SockJS 客户端的第一个请求是 可能影响客户端选择传输的信息。 其中一个细节是服务器应用程序是否依赖于 cookie (例如,用于身份验证目的或使用粘性会话进行群集)。 Spring 的 SockJS 支持包括一个名为的属性。 默认情况下启用它,因为大多数Java应用程序都依赖于cookie。如果您的应用程序不需要它,您可以关闭此选项, 然后,SockJS客户端应该在IE 8和9中选择。​​/info​​​​sessionCookieNeeded​​​​JSESSIONID​​​​xdr-streaming​

如果您确实使用基于 iframe 的传输,请记住 可以通过以下方式指示浏览器阻止在给定页面上使用 IFrames 将 HTTP 响应标头设置为 、、 或。这用于防止点击劫持。​​X-Frame-Options​​​​DENY​​​​SAMEORIGIN​​​​ALLOW-FROM <origin>​

如果您的应用程序添加了响应标头(应该如此! 并且依赖于基于 iframe 的传输,您需要将标头值设置为 toor。春袜子 支持还需要知道 SockJS 客户端的位置,因为它已加载 从 iframe 。默认情况下,iframe 设置为下载 SockJS 客户端 从 CDN 位置。最好将此选项配置为使用 与应用程序来自同一源的 URL。​​X-Frame-Options​​​​SAMEORIGIN​​​​ALLOW-FROM <origin>​

以下示例显示了如何在 Java 配置中执行此操作:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}

// ...

}

XML 命名空间通过元素提供类似的选项。​​<websocket:sockjs>​

4.2.4. 心跳

SockJS 协议要求服务器发送心跳消息以排除代理 从断定连接挂起。Spring SockJS 配置具有一个属性 调用可用于自定义频率。默认情况下,a 检测信号在 25 秒后发送,假设没有发送其他消息 连接。此 25 秒值符合以下针对公共互联网应用程序的IETF 建议。​​heartbeatTime​

Spring SockJS支持还允许您配置以下 计划检测信号任务。任务调度程序由线程池支持, 使用基于可用处理器数量的默认设置。你 应考虑根据您的特定需求自定义设置。​​TaskScheduler​

4.2.5. 客户端断开连接

HTTP 流和 HTTP 长轮询 SockJS 传输需要保持连接 开放时间比平时长。有关这些技术的概述,请参阅此博客文章。

在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的 允许退出 Servlet 容器线程,处理请求并继续 写入来自另一个线程的响应。

一个特定的问题是 Servlet API 不为客户端提供通知 这已经消失了。请参阅eclipse-ee4j/servlet-api#44。 但是,Servlet 容器在后续尝试写入时引发异常 到响应。由于 Spring 的 SockJS 服务支持服务器发送的心跳(每个 默认为 25 秒),这意味着通常会在该范围内检测到客户端断开连接 时间段(或更早,如果消息发送更频繁)。

4.2.6. SockJS 和 CORS

如果您允许跨源请求(请参阅允许的来源),SockJS 协议 使用 CORS 在 XHR 流式处理和轮询传输中提供跨域支持。因此 将自动添加 CORS 标头,除非响应中存在 CORS 标头 被检测到。因此,如果应用程序已配置为提供 CORS 支持(例如, 通过 Servlet 过滤器),Spring 跳过了这部分。​​SockJsService​

也可以通过在 Spring 的 SockJsService 中设置属性来禁用这些 CORS 标头的添加。​​suppressCors​

SockJS 需要以下标头和值:

  • ​Access-Control-Allow-Origin​​:从请求标头的值初始化。Origin
  • ​Access-Control-Allow-Credentials​​:始终设置为。true
  • ​Access-Control-Request-Headers​​:从等效请求标头中的值初始化。
  • ​Access-Control-Allow-Methods​​:传输支持的 HTTP 方法(参见)。TransportType
  • ​Access-Control-Max-Age​​:设置为 31536000(1 年)。

对于确切的实现,请参阅和 源代码中的 theenum。​​addCorsHeaders​​​​AbstractSockJsService​​​​TransportType​

或者,如果 CORS 配置允许,请考虑排除具有 SockJS端点前缀,从而让Spring处理它。​​SockJsService​

4.2.7. ​​SockJsClient​

Spring 提供了一个 SockJS Java 客户端来连接到远程 SockJS 端点,而无需 使用浏览器。当需要双向时,这尤其有用 两台服务器之间通过公共网络进行通信(即,网络代理可以 排除使用 WebSocket 协议)。SockJS Java客户端也非常有用 用于测试目的(例如,模拟大量并发用户)。

SockJS Java 客户端支持 、 和 transports。其余的仅在浏览器中使用有意义。​​websocket​​​​xhr-streaming​​​​xhr-polling​

您可以配置:​​WebSocketTransport​

  • ​StandardWebSocketClient​​在 JSR-356 运行时中。
  • ​JettyWebSocketClient​​通过使用 Jetty 9+ 本机 WebSocket API。
  • 春天的任何实现。WebSocketClient

根据定义,An支持两者,因为, 从客户端的角度来看,除了用于连接的 URL 之外,没有其他区别 到服务器。目前有两种实现:​​XhrTransport​​​​xhr-streaming​​​​xhr-polling​

  • ​RestTemplateXhrTransport​​使用 Spring 的 HTTP 请求。RestTemplate
  • ​JettyXhrTransport​​使用 Jetty's for HTTP 请求。HttpClient

以下示例演示如何创建 SockJS 客户端并连接到 SockJS 终结点:

List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");

若要用于模拟大量并发用户,请 需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够的 连接数和线程数。以下示例演示如何使用 Jetty 执行此操作:​​SockJsClient​

HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));

以下示例显示了与服务器端 SockJS 相关的属性(有关详细信息,请参阅 javadoc) 您还应该考虑自定义:

@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024)
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000);
}

// ...
}

将属性设置为 512KB(默认值为 128KB —)。​​streamBytesLimit​​​​128 * 1024​

将属性设置为 1,000(默认值为)。​​httpMessageCacheSize​​​​100​

将属性设置为 30 属性秒(默认值为 5 秒 —)。​​disconnectDelay​​​​5 * 1000​

4.3. 跺脚

WebSocket 协议定义了两种类型的消息(文本和二进制),但它们 内容未定义。该协议定义了客户端和服务器协商 子协议(即更高级别的消息传递协议),用于 WebSocket 之上 定义每个人可以发送的消息类型,格式是什么,每个消息的内容 消息,等等。子协议的使用是可选的,但无论哪种方式,客户端和 服务器需要就定义消息内容的某个协议达成一致。

4.3.1. 概述

跺脚(简单) 面向文本的消息传递协议)最初是为脚本语言创建的 (如 Ruby、Python 和 Perl)连接到企业消息代理。是的 旨在解决常用消息传递模式的最小子集。跺脚可以是 通过任何可靠的双向流网络协议(如 TCP 和 WebSocket)使用。 尽管 STOMP 是一种面向文本的协议,但消息有效负载可以是 文本或二进制。

STOMP 是一种基于帧的协议,其帧基于 HTTP 建模。以下清单显示了结构 的踩踏帧:

COMMAND
header1:value1
header2:value2

Body^@

客户端可以使用 theorcommand 发送或订阅 消息,以及描述什么的标头 消息是关于以及谁应该接收它。这使得 可用于通过代理发送消息的发布-订阅机制 到其他连接的客户端或向服务器发送消息以请求 执行一些工作。​​SEND​​​​SUBSCRIBE​​​​destination​

当您使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序会起作用。 作为客户的 STOMP 代理。消息路由到消息处理 方法或跟踪订阅和 向订阅用户广播消息。您还可以将 Spring 配置为工作 使用专用的STOMP代理(例如RabbitMQ,ActiveMQ等)进行实际操作 消息广播。在这种情况下,弹簧保持 与代理的 TCP 连接,向其中继消息,并传递消息 从它向下到连接的 WebSocket 客户端。因此,Spring Web应用程序可以 依靠基于 HTTP 的统一安全性、通用验证和熟悉的编程 消息处理模型。​​@Controller​

以下示例显示订阅以接收股票的客户端,该 服务器可能会定期发出(例如,通过发送消息的计划任务) 通过ATO经纪人):​​SimpMessagingTemplate​

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

以下示例显示发送交易请求的客户端,服务器 可以通过方法处理:​​@MessageMapping​

SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

执行后,服务器可以 向客户广播交易确认消息和详细信息。

目的地的含义在 STOMP 规范中故意保持不透明。它可以 是任何字符串,完全由 STOMP 服务器来定义语义和 它们支持的目标的语法。然而,这是很常见的 目标为类似路径的字符串,其中暗示发布-订阅 (一对多)并暗示点对点(一对一)消息 交流。​​/topic/..​​​​/queue/​

STOMP 服务器可以使用该命令向所有订阅者广播消息。 以下示例显示服务器向订阅的客户端发送:​​MESSAGE​

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@

服务器无法发送未经请求的消息。所有消息 来自服务器的标头必须响应特定的客户端订阅,并且服务器消息的标头必须与 客户端订阅。​​subscription​​​​id​

前面的概述旨在提供对 踩踏协议。我们建议完整查看协议规范。

4.3.2. 好处

使用STOMP作为子协议可以让Spring Framework和Spring Security。 与使用原始 WebSocket 相比,提供更丰富的编程模型。相同的点可以是 关于HTTP与原始TCP以及它如何让Spring MVC和其他Web框架 提供丰富的功能。以下是好处列表:

  • 无需发明自定义消息传递协议和消息格式。
  • STOMP 客户端,包括 Spring Framework 中的Java 客户端,是可用的。
  • 您可以(可选)使用消息代理(例如 RabbitMQ、ActiveMQ 等)来 管理订阅和广播消息。
  • 应用程序逻辑可以组织在任意数量的实例中,消息可以是 根据 STOMP 目标标头路由到它们与处理原始 WebSocket 消息 与单用于给定连接。@ControllerWebSocketHandler
  • 您可以使用 Spring 安全性根据 STOMP 目标和消息类型来保护消息。

4.3.3. 启用踩踏

在 andmodules 中提供了 Stomp over WebSocket 支持。拥有这些依赖项后,可以公开 STOMP 端点,通过 WebSocket 与SockJS 回退,如以下示例所示:​​spring-messaging​​​​spring-websocket​

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

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

​/portfolio​​是 WebSocket(或 SockJS)到的端点的 HTTP URL 客户端需要连接以进行 WebSocket 握手。

目标标头以 开头的 STOMP 消息路由到类中的方法。​​/app​​​​@MessageMapping​​​​@Controller​

使用内置的消息代理进行订阅和广播,以及 将目标标头以 开头的消息路由到代理。​​/topic `or `/queue​

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio">
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic, /queue"/>
</websocket:message-broker>

</beans>

要从浏览器连接,对于 SockJS,您可以使用sockjs-client​。对于 STOMP,许多应用程序都有 使用JMESNIL/STOMP-WebSocket​库 (也称为 stomp.js),功能完整,已在生产中用于 年,但不再维护。目前JSteunou/webstomp-client是最多的 积极维护和发展该图书馆的继任者。以下示例代码 基于它:

var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

stompClient.connect({}, function(frame) {
}

或者,如果您通过 WebSocket 连接(不带 SockJS),则可以使用以下代码:

var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
}

请注意,在前面的示例中不需要指定标头。即使有,它们也会被忽略(或者更确切地说, 覆盖)在服务器端。有关身份验证的更多信息,请参阅连接到代理和身份验证。stompClientloginpasscode

有关更多示例代码,请参阅:

  • 使用 WebSocket 构建 交互式 Web 应用程序 — 入门指南。
  • 股票投资组合 — 样本 应用。

4.3.4. 网络套接字服务器

若要配置基础 WebSocket 服务器,请应用服务器配置中的信息。对于码头,但是您需要设置 通过:​​HandshakeHandler​​​​WebSocketPolicy​​​​StompEndpointRegistry​

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}

@Bean
public DefaultHandshakeHandler handshakeHandler() {

WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);

return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}

4.3.5. 消息流

一旦 STOMP 端点暴露,Spring 应用程序就会成为 STOMP 代理 连接的客户端。本节介绍服务器端的消息流。

该模块包含对消息传递应用程序的基础支持 起源于春季集成,是 后来提取并合并到 Spring 框架中,以便在许多Spring 项目和应用程序场景中更广泛地使用。 以下列表简要介绍了一些可用的消息传递抽象:spring-messaging

  • 留言: 消息的简单表示形式,包括标头和有效负载。
  • 消息处理程序: 用于处理消息的协定。
  • 消息通道: 用于发送消息的协定,该消息允许在生产者和使用者之间实现松散耦合。
  • 订阅频道:与订阅者。MessageChannelMessageHandler
  • ExecutorSubscribableChannel:使用anfor传递消息。SubscribableChannelExecutor

Java 配置(即)和 XML 命名空间配置 (即)使用上述组件组装消息 工作流。下图显示了简单内置消息时使用的组件 代理已启用:​​@EnableWebSocketMessageBroker​​​​<websocket:message-broker>​

基于 Servlet API 并部署到 Servlet 容器(四)

上图显示了三个消息通道:

  • ​clientInboundChannel​​:用于传递从 WebSocket 客户端接收的消息。
  • ​clientOutboundChannel​​:用于将服务器消息发送到 WebSocket 客户端。
  • ​brokerChannel​​:用于从内部向消息代理发送消息 服务器端应用程序代码。

下图显示了外部代理(如 RabbitMQ)时使用的组件 配置为管理订阅和广播消息:

基于 Servlet API 并部署到 Servlet 容器(四)

前面两个图的主要区别在于使用“代理中继”进行传递 消息通过 TCP 发送到外部 STOMP 代理,以及用于从 代理到订阅客户。

当从 WebSocket 连接接收消息时,它们被解码为 STOMP 帧, 变成了一个弹簧表示,并发送到进一步处理。例如,STOMP 消息,其 目标标头开头为 可以路由到方法 带注释的控制器,而消息可以直接路由 到消息代理。​​Message​​​​clientInboundChannel​​​​/app​​​​@MessageMapping​​​​/topic​​​​/queue​

处理来自客户端的 STOMP 消息的注释可能会将消息发送到 消息代理通过,代理广播 通过消息发送给匹配的订阅者。一样 控制器也可以执行相同的操作来响应 HTTP 请求,因此客户端可以执行 HTTP POST,然后 amethod 可以向消息代理发送消息 向订阅的客户端广播。​​@Controller​​​​brokerChannel​​​​clientOutboundChannel​​​​@PostMapping​

我们可以通过一个简单的例子来跟踪流程。请考虑以下示例,该示例设置服务器:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

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

@Controller
public class GreetingController {

@MessageMapping("/greeting")
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}

前面的示例支持以:

  1. 客户端连接到 WebSocket 连接,一旦 WebSocket 连接 建立后,STOMP框架开始在其上流动。http://localhost:8080/portfolio
  2. 客户端发送目标标头为 的 SUBSCRIBE 帧。收到后 并解码后,消息被发送到 然后路由到 消息代理,用于存储客户端订阅。/topic/greetingclientInboundChannel
  3. 客户端将 SEND 帧发送到。前缀有助于将其路由到 带注释的控制器。去除前缀后,目标的剩余部分将映射到方法。/app/greeting/app/app/greeting@MessageMappingGreetingController
  4. 返回的值 is 变成了一个 Springwith 基于返回值的有效负载和默认目标标头 (派生自输入目标替换为)。生成的消息被发送到并处理 通过消息代理。GreetingControllerMessage/topic/greeting/app/topicbrokerChannel
  5. 消息代理查找所有匹配的订阅者,并为每个订阅者发送一个 MESSAGE 帧 通过,从中将消息编码为 STOMP 帧 并在 WebSocket 连接上发送。clientOutboundChannel

下一节提供有关带注释的方法的更多详细信息,包括 支持的参数和返回值的类型。

4.3.6. 带注释的控制器

应用程序可以使用带注释的类来处理来自客户端的消息。 此类类可以声明 、 和方法,如以下主题中所述:​​@Controller​​​​@MessageMapping​​​​@SubscribeMapping​​​​@ExceptionHandler​

  • @MessageMapping
  • @SubscribeMapping
  • @MessageExceptionHandler
​@MessageMapping​

您可以使用注释基于消息路由消息的方法 目的地。它在方法级别和类型级别都受支持。在类型 level,用于表示跨所有方法的共享映射 控制器。​​@MessageMapping​​​​@MessageMapping​

默认情况下,映射值是 Ant 样式的路径模式(例如, 包括对模板变量的支持(例如,)。这些值可以是 通过方法参数引用。应用程序也可以切换到 映射的点分隔目标约定,如点作为分隔符中所述。​​/thing*​​​​/thing/**​​​​/thing/{id}​​​​@DestinationVariable​

支持的方法参数

下表描述了方法参数:

方法参数

描述

​Message​

以访问完整的消息。

​MessageHeaders​

用于访问其中的标头。​​Message​

​MessageHeaderAccessor​​​和​​SimpMessageHeaderAccessor​​​​StompHeaderAccessor​

用于通过类型化访问器方法访问标头。

​@Payload​

对于访问消息的有效负载,由配置的转换(例如,从 JSON)。​​MessageConverter​

不需要存在此注释,因为默认情况下,如果没有,则假定存在此注释 其他参数匹配。

您可以使用 Spring 的参数注释有效负载参数, 以自动验证有效负载参数。​​@jakarta.validation.Valid​​​​@Validated​

​@Header​

用于访问特定的标头值 — 如有必要,使用 an 进行类型转换。​​org.springframework.core.convert.converter.Converter​

​@Headers​

用于访问邮件中的所有标头。此参数必须可分配给。​​java.util.Map​

​@DestinationVariable​

用于访问从消息目标中提取的模板变量。 根据需要,值将转换为声明的方法参数类型。

​java.security.Principal​

反映在 WebSocket HTTP 握手时登录的用户。

返回值

默认情况下,来自 amethod的返回值被序列化为有效负载 通过匹配并作为 ato 发送, 从那里向订阅者广播。出站消息的目的地是 与入站消息相同,但前缀为 。​​@MessageMapping​​​​MessageConverter​​​​Message​​​​brokerChannel​​​​/topic​

您可以使用注释来自定义目标 输出 message.is 用于自定义目标或 指定用于定向输出消息的多个 destinations.is 仅与输入消息关联的用户。请参阅用户目标。​​@SendTo​​​​@SendToUser​​​​@SendTo​​​​@SendToUser​

您可以在同一方法上同时使用两者,并且两者兼而有之 在类级别受支持,在这种情况下,它们充当 .class。但是,请记住,任何方法级注释 在类级别覆盖任何此类注释。​​@SendTo​​​​@SendToUser​​​​@SendTo​​​​@SendToUser​

消息可以异步处理,amethod可以返回,或。​​@MessageMapping​​​​ListenableFuture​​​​CompletableFuture​​​​CompletionStage​

请注意,and只是一种便利,相当于使用the来发送消息。如有必要,对于更高级的场景,方法可以直接回退使用。 这可以代替返回值,或者可以作为返回值的补充。 请参阅发送消息。​​@SendTo​​​​@SendToUser​​​​SimpMessagingTemplate​​​​@MessageMapping​​​​SimpMessagingTemplate​

​@SubscribeMapping​

​@SubscribeMapping​​类似于但将映射范围缩小到 仅限订阅消息。它支持与 相同的方法参数。然而 对于返回值,默认情况下,消息直接发送到客户端(通过,以响应订阅),而不是发送到代理(通过,作为对匹配订阅的广播)。添加将覆盖此行为并改为发送到代理。​​@MessageMapping​​​​@MessageMapping​​​​clientOutboundChannel​​​​brokerChannel​​​​@SendTo​​​​@SendToUser​

这什么时候有用?假设代理映射到 and,而 应用程序控制器映射到。在此设置中,代理存储所有 订阅用于重复广播,以及 应用程序无需参与。客户端也可以订阅 某个目标,控制器可以返回一个值来响应 订阅,无需经纪人参与,无需再次存储或使用订阅 (实际上是一次性请求-答复交换)。一个用例是填充 UI 带有启动时的初始数据。​​/topic​​​​/queue​​​​/app​​​​/topic​​​​/queue​​​​/app​

什么时候这没有用?不要尝试将代理和控制器映射到同一目标 前缀,除非您希望两者都独立处理消息,包括订阅, 出于某种原因。入站消息并行处理。无法保证是否 代理或控制器首先处理给定的消息。如果目标是通知 当订阅已存储并准备好进行广播时,客户端应要求 收据,如果服务器支持它(简单代理不支持)。例如,使用 JavaSTOMP 客户机,您可以执行以下操作来添加收据:

@Autowired
private TaskScheduler messageBrokerTaskScheduler;

// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);

// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(receiptHeaders -> {
// Subscription ready...
});

服务器端选项是注册anon 并实现在处理完消息(包括订阅)后调用的方法。​​ExecutorChannelInterceptor​​​​brokerChannel​​​​afterMessageHandled​

​@MessageExceptionHandler​

应用程序可以使用方法来处理来自方法的异常。您可以在注释中声明异常 本身或通过方法参数(如果要访问异常实例)。 下面的示例通过方法参数声明异常:​​@MessageExceptionHandler​​​​@MessageMapping​

@Controller
public class MyController {

// ...

@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}

​@MessageExceptionHandler​​方法支持灵活的方法签名和支持 方法参数类型和返回值与方法相同@MessageMapping方法。

通常,方法适用于类 (或类层次结构)在其中声明它们。如果您希望应用此类方法 更全局(跨控制器),可以在标记为 with 的类中声明它们。这与Spring MVC中提供的类似支持相当。​​@MessageExceptionHandler​​​​@Controller​​​​@ControllerAdvice​

4.3.7. 发送消息

如果要从 应用?任何应用程序组件都可以向 发送消息。 最简单的方法是注入 aand 使用它来发送消息。通常,您将通过以下方式注入它 类型,如以下示例所示:​​brokerChannel​​​​SimpMessagingTemplate​

@Controller
public class GreetingController {

private SimpMessagingTemplate template;

@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}

@RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}

}

但是,您也可以通过其名称 () 来限定它,如果另一个 存在相同类型的 Bean。​​brokerMessagingTemplate​

4.3.8. 简单代理

内置的简单消息代理处理来自客户端的订阅请求, 将它们存储在内存中,并将消息广播到具有匹配的已连接客户端 目的地。代理支持类似路径的目标,包括订阅 到 Ant 样式的目标模式。

如果配置了任务计划程序,则简单代理支持STOMP 检测信号​。 要配置调度程序,您可以声明自己的 bean 并将其设置为 这。或者,您可以使用自动的那个 但是,在内置 WebSocket 配置中声明,您需要避免 内置 WebSocket 配置和您的配置之间的循环。例如:​​TaskScheduler​​​​MessageBrokerRegistry​​​​@Lazy​​​​WebSocketMessageBrokerConfigurer​

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private TaskScheduler messageBrokerTaskScheduler;

@Autowired
public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);

// ...
}
}

4.3.9. 外部经纪商

简单代理非常适合入门,但仅支持 STOMP命令(它不支持确认,收据和其他一些功能), 依赖于简单的消息发送循环,不适合群集。 作为替代方案,您可以升级应用程序以使用功能齐全的应用程序 消息代理。

请参阅所选消息代理(如RabbitMQ、ActiveMQ 等)的 STOMP 文档,安装代理, 并在启用 STOMP 支持的情况下运行它。然后,您可以启用 STOMP 代理中继 (而不是简单的代理)在 Spring 配置中。

以下示例配置启用功能齐全的代理:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>

</beans>

上述配置中的 STOMP 代理中继是一个 Spring 消息处理程序,它通过将消息转发到外部消息代理来处理消息。 为此,它与代理建立 TCP 连接,将所有消息转发给代理, 然后将从代理接收的所有消息转发给客户端,通过他们的 网络套接字会话。本质上,它充当转发消息的“中继” 在两个方向上。

此外,应用程序组件(例如 HTTP 请求处理方法、 业务服务等)也可以向代理中继发送消息,如前所述 在“发送消息”中,将消息广播到订阅的 WebSocket 客户端。

实际上,代理中继支持可靠且可扩展的消息广播。

4.3.10. 连接到经纪商

STOMP 代理中继维护与代理的单个“系统”TCP 连接。 此连接用于源自服务器端应用程序的消息 仅用于接收消息。您可以配置 STOMP 凭据(即 STOMP帧和标头)用于此连接。这是暴露的 在 XML 命名空间和 Java 配置中作为默认值为 and 的 and属性。​​login​​​​passcode​​​​systemLogin​​​​systemPasscode​​​​guest​​​​guest​

STOMP 代理中继还为每个连接的 TCP 连接创建单独的 TCP 连接 WebSocket 客户端。您可以配置用于所有 TCP 的 STOMP 凭据 代表客户端创建的连接。这在两个 XML 命名空间中都公开 和 Java 配置作为默认属性 的价值观和。​​clientLogin​​​​clientPasscode​​​​guest​​​​guest​

STOMP 代理中继还向消息发送和接收检测信号 代理通过“系统”TCP 连接。您可以配置发送间隔 和接收检测信号(默认情况下每个 10 秒)。如果连接到代理 丢失,代理中继继续尝试重新连接,每 5 秒, 直到它成功。

任何 Spring Bean 都可以实现在与代理的“系统”连接丢失时接收通知,并且 重新建立。例如,广播股票报价的股票报价服务可以 在没有活动的“系统”连接时停止尝试发送消息。​​ApplicationListener<BrokerAvailabilityEvent>​

默认情况下,STOMP 代理中继始终连接,并在以下情况下根据需要重新连接 与同一主机和端口的连接丢失。如果您希望提供多个地址, 在每次尝试连接时,您可以配置地址供应商,而不是 固定主机和端口。以下示例演示如何执行此操作:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

// ...

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
registry.setApplicationDestinationPrefixes("/app");
}

private ReactorNettyTcpClient<byte[]> createTcpClient() {
return new ReactorNettyTcpClient<>(
client -> client.addressSupplier(() -> ... ),
new StompReactorNettyCodec());
}
}

您还可以使用 aproperty配置 STOMP 代理中继。 此属性的值设置为每个帧的标头 并且可能很有用(例如,在云环境中,其中实际主机 建立的 TCP 连接不同于提供 基于云的 STOMP 服务)。​​virtualHost​​​​host​​​​CONNECT​

4.3.11. 点作为分隔符

当消息路由到方法时,它们将与方法匹配。默认情况下,模式应使用斜杠 () 作为分隔符。 这在 Web 应用程序中是一个很好的约定,类似于 HTTP URL。但是,如果 您更习惯于消息传递约定,可以切换到使用点 () 作为分隔符。​​@MessageMapping​​​​AntPathMatcher​​​​/​​​​.​

以下示例显示了如何在 Java 配置中执行此操作:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

// ...

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
<websocket:stomp-endpoint path="/stomp"/>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>

<bean class="org.springframework.util.AntPathMatcher">
<constructor-arg index="0" value="."/>
</bean>

</beans>

之后,控制器可以使用点 () 作为分隔符方法, 如以下示例所示:​​.​​​​@MessageMapping​

@Controller
@MessageMapping("red")
public class RedController {

@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}

客户端现在可以向其发送消息。​​/app/red.blue.green123​

在前面的示例中,我们没有更改“代理中继”上的前缀,因为这些 完全依赖于外部消息代理。请参阅 STOMP 文档页面 用于查看它支持目标标头的约定的代理。

另一方面,“简单代理”确实依赖于配置的,因此,如果 您切换分隔符,该更改也适用于代理以及代理匹配的方式 从消息到订阅模式的目标。​​PathMatcher​

4.3.12. 身份验证

每个 Stomp over WebSocket 消息传递会话都以 HTTP 请求开头。 这可以是升级到 WebSocket 的请求(即 WebSocket 握手) 或者,在 SockJS 回退的情况下,一系列 SockJS HTTP 传输请求。

许多 Web 应用程序已经具有身份验证和授权,以 安全的 HTTP 请求。通常,用户通过 Spring 安全性进行身份验证 通过使用某种机制,例如登录页、HTTP 基本身份验证或其他方式。 经过身份验证的用户的安全上下文保存在 HTTP 会话中 并与同一基于 Cookie 的会话中的后续请求相关联。

因此,对于 WebSocket 握手或 SockJS HTTP 传输请求, 通常,已经有一个经过身份验证的用户可通过 访问。Spring 会自动关联该用户 使用为他们创建的 WebSocket 或 SockJS 会话,以及随后的所有 通过用户标头通过该会话传输的 STOMP 消息。​​HttpServletRequest#getUserPrincipal()​

简而言之,典型的 Web 应用程序不需要执行任何操作 超越它已经为安全所做的工作。用户在以下位置进行身份验证 具有通过基于 Cookie 的安全上下文维护的 HTTP 请求级别 HTTP 会话(然后与创建的 WebSocket 或 SockJS 会话相关联 对于该用户),并导致用户标头被标记在每次流动 通过应用程序。​​Message​

STOMP协议在帧上确实有标头。 这些最初是为 STOMP over TCP 设计的,并且是必需的。但是,对于跺脚 通过 WebSocket,默认情况下,Spring 会忽略 STOMP 协议上的身份验证标头 ,并假定用户已在 HTTP 传输级别进行身份验证。 预期是 WebSocket 或 SockJS 会话包含经过身份验证的用户。​​login​​​​passcode​​​​CONNECT​

4.3.13. 令牌身份验证

Spring Security OAuth支持基于令牌的安全性,包括 JSON Web Token (JWT)。 您可以将其用作 Web 应用程序中的身份验证机制, 包括基于 WebSocket 交互的 STOMP,如前面所述 部分(即,通过基于 Cookie 的会话维护标识)。

同时,基于 Cookie 的会话并不总是最适合的(例如, 在不维护服务器端会话的应用程序中或在 通常使用标头进行身份验证的移动应用程序)。

WebSocket协议RFC 6455“没有规定服务器可以在以下期间对客户端进行身份验证的任何特定方式 WebSocket 握手。但是,在实践中,浏览器客户端只能使用标准 身份验证标头(即基本 HTTP 身份验证)或 Cookie,但不能(例如) 提供自定义标头。同样,SockJS JavaScript 客户端不提供 一种使用 SockJS 传输请求发送 HTTP 标头的方法。请参阅sockjs-client 问题 196。 相反,它确实允许发送可用于发送令牌的查询参数, 但这有其自身的缺点(例如,令牌可能是无意的 使用服务器日志中的 URL 记录)。

因此,希望避免使用 cookie 的应用程序可能没有任何好处 HTTP 协议级别身份验证的替代方法。而不是使用饼干, 他们可能更喜欢使用 STOMP 消息传递协议级别的标头进行身份验证。 这样做需要两个简单的步骤:

  1. 使用 STOMP 客户端在连接时传递身份验证标头。
  2. 使用 a 处理身份验证标头。ChannelInterceptor

下一个示例使用服务器端配置来注册自定义身份验证 拦截 器。请注意,拦截器只需要进行身份验证和设置 连接上的用户标头。春天笔记并保存经过身份验证的 用户并将其与同一会话上的后续 STOMP 消息相关联。以下 示例演示如何注册自定义身份验证侦听器:​​Message​

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ... ; // access authentication header(s)
accessor.setUser(user);
}
return message;
}
});
}
}

另外,请注意,当您使用 Spring 安全性对消息的授权时,目前, 您需要确保身份验证配置是有序的 在春季安全之前。最好通过在 它自己的实现是标记的。​​ChannelInterceptor​​​​WebSocketMessageBrokerConfigurer​​​​@Order(Ordered.HIGHEST_PRECEDENCE + 99)​

4.3.14. 授权

Spring 安全性提供WebSocket 子协议授权,该授权使用 ato 根据消息中的用户标头对消息进行授权。 此外,Spring 会话提供 WebSocket 集成,确保用户的 HTTP 会话不会过期,而WebSocket会话仍处于活动状态。​​ChannelInterceptor​

4.3.15. 用户目的地

应用程序可以发送针对特定用户的消息,并且Spring的STOMP支持 识别为此前缀的目标。 例如,客户端可能订阅 thedestination。处理此目标并将其转换为 用户会话唯一的目标(例如)。 这提供了订阅通用名称目标的便利,同时, 同时,确保与订阅相同的其他用户不发生冲突 目标,以便每个用户都可以接收唯一的头寸更新。​​/user/​​​​/user/queue/position-updates​​​​UserDestinationMessageHandler​​​​/queue/position-updates-user123​

在发送端,消息可以发送到目的地,例如,该目的地又被翻译 通过进入一个或多个目的地,每个目的地一个 与用户关联的会话。这允许应用程序中的任何组件 发送针对特定用户的消息,而不必知道更多内容 比他们的名称和通用目的地。这也通过 批注和消息传递模板。​​/user/{username}/queue/position-updates​​​​UserDestinationMessageHandler​

消息处理方法可以将消息发送给与 通过注释处理的消息(也支持 用于共享公共目标的类级别),如以下示例所示:​​@SendToUser​

@Controller
public class PortfolioController {

@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
public TradeResult executeTrade(Trade trade, Principal principal) {
// ...
return tradeResult;
}
}

如果用户有多个会话,则默认情况下,所有会话都已订阅 到给定的目的地是目标。但是,有时可能需要 仅定位发送正在处理的消息的会话。您可以通过以下方式执行此操作 将属性设置为 false,如以下示例所示:​​broadcast​

@Controller
public class MyController {

@MessageMapping("/action")
public void handleAction() throws Exception{
// raise MyBusinessException here
}

@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
public ApplicationError handleException(MyBusinessException exception) {
// ...
return appError;
}
}

您可以从任何应用程序向用户目标发送消息 组件,例如,注入由 Java 配置创建或 XML 命名空间。(如果需要,则提供 Bean 名称 用于资格。以下示例演示如何执行此操作:​​SimpMessagingTemplate​​​​brokerMessagingTemplate​​​​@Qualifier​

@Service
public class TradeServiceImpl implements TradeService {

private final SimpMessagingTemplate messagingTemplate;

@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}

// ...

public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}

在多应用程序服务器方案中,用户目标可能仍未解析,因为 用户连接到其他服务器。在这种情况下,您可以配置 广播未解析消息的目标,以便其他服务器有机会尝试。 这可以通过 在 Java 配置和属性中完成 的元素。​​userDestinationBroadcast​​​​MessageBrokerRegistry​​​​user-destination-broadcast​​​​message-broker​

4.3.16. 消息顺序

来自代理的消息发布到它们所在的位置 写入 WebSocket 会话。由于通道由 a 支持,因此消息 在不同的线程中处理,客户端接收的结果序列可以 与确切的发布顺序不匹配。​​clientOutboundChannel​​​​ThreadPoolExecutor​

如果这是一个问题,请启用标志,如以下示例所示:​​setPreservePublishOrder​

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker preserve-publish-order="true">
<!-- ... -->
</websocket:message-broker>

</beans>

设置该标志后,同一客户端会话中的消息将一次发布到该会话,以便保证发布顺序。 请注意,这会产生较小的性能开销,因此应仅在需要时启用它。​​clientOutboundChannel​

4.3.17. 事件

已发布多个事件,可以是 通过实现 Spring 的接口接收:​​ApplicationContext​​​​ApplicationListener​

  • ​BrokerAvailabilityEvent​​:指示代理何时可用或不可用。 虽然“简单”代理在启动时立即可用,并且仍然如此 应用程序正在运行,STOMP“代理中继”可能会失去连接 到全功能代理(例如,如果代理重新启动)。代理中继 具有重新连接逻辑并重新建立与代理的“系统”连接 当它回来时。因此,每当状态从已连接更改时,都会发布此事件 断开连接,反之亦然。使用应 订阅此事件并避免在代理不在时发送消息 可用。无论如何,他们应该准备好在发送消息时进行处理。SimpMessagingTemplateMessageDeliveryException
  • ​SessionConnectEvent​​:在收到新的 STOMP 连接时发布到 指示新客户端会话的启动。该事件包含表示 连接,包括会话 ID、用户信息(如果有)和客户端的任何自定义标头 送。这对于跟踪客户端会话非常有用。订阅的组件 到此事件可以将包含的消息包装为 withor。SimpMessageHeaderAccessorStompMessageHeaderAccessor
  • ​SessionConnectedEvent​​:不久后出版 代理已发送一个 STOMP CONNECTED 帧以响应 CONNECT。此时, STOMP会话可以认为是完全建立的。SessionConnectEvent
  • ​SessionSubscribeEvent​​:在收到新的 STOMP 订阅时发布。
  • ​SessionUnsubscribeEvent​​:在收到新的 STOMP 取消订阅时发布。
  • ​SessionDisconnectEvent​​:在 STOMP 会话结束时发布。断开连接可能会 已从客户端发送,或者可能在 WebSocket 会话已关闭。在某些情况下,此事件会多次发布 每个会话。组件对于多个断开连接事件应该是幂等的。

4.3.18. 拦截

​事件​​为生命周期提供通知 的 STOMP 连接,但并非针对每个客户端消息。应用程序还可以注册 aa 以拦截处理链的任何部分中的任何消息。 以下示例演示如何截获来自客户端的入站消息:​​ChannelInterceptor​

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new MyChannelInterceptor());
}
}

自定义用途访问有关消息的信息,如以下示例所示:​​ChannelInterceptor​​​​StompHeaderAccessor​​​​SimpMessageHeaderAccessor​

public class MyChannelInterceptor implements ChannelInterceptor {

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getStompCommand();
// ...
return message;
}
}

应用程序也可以实现,这是一个子接口 在处理消息的线程中使用回调。 虽然 ais 为发送到通道的每条消息调用一次,但每个订阅来自通道的消息的线程中的 提供了钩子。​​ExecutorChannelInterceptor​​​​ChannelInterceptor​​​​ChannelInterceptor​​​​ExecutorChannelInterceptor​​​​MessageHandler​

请注意,与前面描述的一样,断开连接消息 可以来自客户端,也可以在以下情况下自动生成 WebSocket 会话已关闭。在某些情况下,拦截器可能会拦截此 为每个会话发送多次消息。组件在以下方面应该是幂等的 多个断开连接事件。​​SessionDisconnectEvent​

4.3.19. 踩踏客户端

Spring 提供了一个 STOMP over WebSocket 客户端和一个 STOMP over TCP 客户端。

首先,您可以创建和配置,如以下示例所示:​​WebSocketStompClient​

WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

在前面的示例中,您可以替换为 因为这也是一种实现。泰康 使用 WebSocket 或基于 HTTP 的传输作为回退。有关更多详细信息,请参阅SockJsClient。​​StandardWebSocketClient​​​​SockJsClient​​​​WebSocketClient​​​​SockJsClient​

接下来,您可以建立连接并为 STOMP 会话提供处理程序, 如以下示例所示:

String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

当会话可供使用时,将通知处理程序,如以下示例所示:

public class MyStompSessionHandler extends StompSessionHandlerAdapter {

@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
// ...
}
}

建立会话后,可以发送任何有效负载,并且 使用配置进行序列化,如以下示例所示:​​MessageConverter​

session.send("/topic/something", "payload");

您还可以订阅目的地。方法需要处理程序 对于订阅上的消息,您可以返回句柄 用于取消订阅。对于收到的每条消息,处理程序可以指定有效负载应反序列化为的目标类型,如以下示例所示:​​subscribe​​​​Subscription​​​​Object​

session.subscribe("/topic/something", new StompFrameHandler() {

@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}

@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}

});

要启用 STOMP 检测信号,您可以配置并选择性地自定义检测信号间隔(写入不活动为 10 秒, 这会导致发送检测信号,读取不活动需要 10 秒,这 关闭连接)。​​WebSocketStompClient​​​​TaskScheduler​

​WebSocketStompClient​​仅在不活动的情况下发送检测信号,即当没有 发送其他消息。使用外部代理时,这可能会带来挑战 由于具有非代理目标的消息表示活动,但实际上不是 转发给经纪人。在这种情况下,您可以在初始化外部代理时进行配置,以确保 当只有带有非代理的消息时,检测信号也会转发到代理 发送目标。​​TaskScheduler​

STOMP协议还支持收据,其中客户端必须添加一个标头,服务器在发送或发送或 订阅已处理。为了支持这一点,theoffer这会导致标头 在每个后续发送或订阅事件中添加。 或者,您也可以手动将收据标头添加到。 发送和订阅都返回一个实例,可用于注册接收成功和失败回调。 对于此功能,您必须为客户端配置收据过期前的时间量(默认为 15 秒)。​​receipt​​​​StompSession​​​​setAutoReceipt(boolean)​​​​receipt​​​​StompHeaders​​​​Receiptable​​​​TaskScheduler​

请注意,它本身是一个,它允许 除了回调之外,它还处理错误帧 处理消息和 for 的例外情况 传输级错误,包括。​​StompSessionHandler​​​​StompFrameHandler​​​​handleException​​​​handleTransportError​​​​ConnectionLostException​

4.3.20. 网络套接字范围

每个 WebSocket 会话都有一个属性映射。地图作为标题附加到 入站客户端消息,可以从控制器方法访问,如以下示例所示:

@Controller
public class MyController {

@MessageMapping("/action")
public void handle(SimpMessageHeaderAccessor headerAccessor) {
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
// ...
}
}

您可以在示波器中声明一个 Spring 管理的 bean。 您可以将 WebSocket 范围的 bean 注入控制器和任何通道拦截器 注册在。这些通常是单例和活的 比任何单个 WebSocket 会话都长。因此,您需要使用 WebSocket 作用域 Bean 的作用域代理模式,如以下示例所示:​​websocket​​​​clientInboundChannel​

@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {

@PostConstruct
public void init() {
// Invoked after dependencies injected
}

// ...

@PreDestroy
public void destroy() {
// Invoked when the WebSocket session ends
}
}

@Controller
public class MyController {

private final MyBean myBean;

@Autowired
public MyController(MyBean myBean) {
this.myBean = myBean;
}

@MessageMapping("/action")
public void handle() {
// this.myBean from the current WebSocket session
}
}

与任何自定义作用域一样,Spring 在第一个初始化一个新实例 从控制器访问并将实例存储在 WebSocket 中的时间 会话属性。随后返回相同的实例,直到会话 结束。WebSocket 范围的 bean 调用了所有 Spring 生命周期方法,如 如前面的示例所示。​​MyBean​

4.3.21. 性能

在性能方面没有灵丹妙药。许多因素 影响它,包括消息的大小和数量,是否应用程序 方法执行需要阻塞和外部因素的工作 (如网速等问题)。本节的目标是提供 可用配置选项的概述以及一些想法 关于如何推理缩放。

在消息传递应用程序中,消息通过通道进行异步传递 由线程池支持的执行。配置此类应用程序需要 对渠道和消息流有很好的了解。因此,它是 建议查看消息流。

显而易见的起点是配置支持和的线程池。默认情况下,两者 配置为可用处理器数量的两倍。​​clientInboundChannel​​​​clientOutboundChannel​

如果批注方法中的消息处理主要受 CPU 限制,则 的线程数应保持接近 处理器数量。如果他们所做的工作更受 IO 限制并且需要阻止 或等待数据库或其他外部系统,线程池大小 可能需要增加。​​clientInboundChannel​

另一方面,这一切都是关于向WebSocket发送消息 客户。如果客户端位于快速网络上,则线程数应 保持接近可用处理器的数量。如果它们很慢或打开 低带宽,它们需要更长的时间来消耗消息并给 线程池。因此,必须增加线程池大小。​​clientOutboundChannel​

虽然可以预测的工作负载 - 毕竟,它基于应用程序的功能 - 如何配置 “clientOutboundChannel”更难,因为它基于其他因素 应用程序的控制。出于这个原因,另外两个 属性与消息的发送相关:和。您可以使用这些方法来配置 允许发送以及发送时可以缓冲多少数据 发送到客户端的消息。​​clientInboundChannel​​​​sendTimeLimit​​​​sendBufferSizeLimit​

一般的想法是,在任何给定时间,只能使用单个线程 以发送到客户端。同时,所有其他消息都会被缓冲,而您 可以使用这些属性来决定允许发送消息的时间 采取以及在此期间可以缓冲多少数据。请参阅 javadoc 和 有关其他重要详细信息的 XML 架构的文档。

以下示例显示了可能的配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}

// ...

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker>
<websocket:transport send-timeout="15000" send-buffer-size="524288" />
<!-- ... -->
</websocket:message-broker>

</beans>

您还可以使用前面显示的 WebSocket 传输配置来配置 传入 STOMP 消息的最大允许大小。理论上,WebSocket 消息的大小几乎是无限的。在实践中,WebSocket 服务器强加 限制 - 例如,Tomcat 上的 8K 和 Jetty 上的 64K。因此,STOMP 客户端 (例如JavaScriptwebstomp-client等)在16K边界处拆分较大的STOMP消息,并将它们作为多个消息发送。 WebSocket 消息,这需要服务器缓冲和重新组装。

Spring的STOMP-over-WebSocket支持可以做到这一点,因此应用程序可以配置。 STOMP 消息的最大大小,与特定于 WebSocket 服务器的消息无关 大小。请记住,WebSocket 消息大小是自动的 如有必要,进行了调整,以确保它们可以在 最低。

以下示例显示了一种可能的配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}

// ...

}

以下示例显示了与上述示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker>
<websocket:transport message-size="131072" />
<!-- ... -->
</websocket:message-broker>

</beans>

有关扩展的一个重要点涉及使用多个应用程序实例。 目前,您无法使用简单代理执行此操作。 但是,当您使用功能齐全的代理(如 RabbitMQ)时,每个应用程序 实例连接到代理,消息从一个应用程序广播 实例可以通过代理广播到连接的 WebSocket 客户端 通过任何其他应用程序实例。

4.3.22. 监控

当你使用时,键 基础结构组件自动收集统计信息和计数器,这些统计信息和计数器提供 对应用程序内部状态的重要见解。配置情况 还声明了一个收集所有 可用信息在一个地方,默认情况下将其记录在级别一次 每30分钟一班。这个bean可以通过Spring导出到JMX,以便在运行时查看(例如,通过JDK)。 以下列表汇总了可用信息:​​@EnableWebSocketMessageBroker​​​​<websocket:message-broker>​​​​WebSocketMessageBrokerStats​​​​INFO​​​​MBeanExporter​​​​jconsole​

客户端 WebSocket 会话

当前

指示有多少个客户端会话 目前,计数进一步按WebSocket与HTTP细分 流式传输和轮询 SockJS 会话。

指示已建立的总会话数。

异常关闭

连接失败

已建立但 在 60 秒内未收到任何消息后关闭。这是 通常表示代理或网络问题。

超出发送限制

会话在超过配置的发送后关闭 超时或发送缓冲区限制,慢速客户端可能会发生这种情况 (请参阅上一节)。

传输错误

传输错误后关闭的会话,例如 无法读取或写入 WebSocket 连接,或 HTTP 请求或响应。

踩踏框架

“连接”、“已连接”和“断开连接”帧的总数 已处理,指示在 STOMP 级别连接的客户端数。请注意, 当会话异常关闭或当 客户端关闭而不发送断开连接帧。

踩踏代理继电器

TCP 连接

指示代表客户端的 TCP 连接数 为代理建立 WebSocket 会话。这应该等于 客户端 WebSocket 会话数 + 1 个额外的共享“系统”连接 用于从应用程序内发送消息。

踩踏框架

“连接”、“已连接”和“断开连接”帧的总数 代表客户转发给经纪人或从经纪人那里接收。请注意,a 断开连接帧被发送到代理,而不管客户端 WebSocket 如何 会议已关闭。因此,较低的断开连接帧数表明 代理正在主动关闭连接(可能是因为 未及时到达的检测信号、无效的输入帧或其他问题)。

客户端入站通道

来自支持线程池的统计信息,用于深入了解传入消息处理的运行状况。任务排队 此处指示应用程序可能太慢而无法处理消息。 如果有 I/O 绑定任务(例如,慢速数据库查询、对第三方的 HTTP 请求) REST API 等),请考虑增加线程池大小。​​clientInboundChannel​

客户端出站通道

来自支持线程池的统计信息,可以深入了解向客户端广播消息的运行状况。任务 在此处排队表示客户端使用消息的速度太慢。 解决此问题的一种方法是增加线程池大小以适应 预期的并发慢速客户端数。另一种选择是减少 发送超时和发送缓冲区大小限制(请参阅上一节)。​​clientOutboundChannel​

SockJS 任务计划程序

来自 SockJS 任务调度程序线程池的统计信息 用于发送检测信号。请注意,当在 跺脚级别,SockJS 检测信号被禁用。

4.3.23. 测试

当你使用Spring的STOMP-over-WebSocket时,有两种主要方法来测试应用程序。 支持。第一种是编写服务器端测试来验证功能 控制器及其带注释的消息处理方法。二是写作 涉及运行客户端和服务器的完整端到端测试。

这两种方法并不相互排斥。相反,每个人都有自己的位置 在整体测试策略中。服务器端测试更集中,更易于编写 和维护。另一方面,端到端集成测试更完整, 测试更多,但它们也更多地涉及编写和维护。

服务器端测试的最简单形式是编写控制器单元测试。然而 这还不够有用,因为控制器的大部分功能取决于其 附注。纯粹的单元测试根本无法测试这一点。

理想情况下,受测控制器应该像在运行时一样调用,就像 使用 Spring MVC 测试测试处理 HTTP 请求的控制器的方法 框架 — 也就是说,不运行 Servlet 容器,而是依赖于 Spring 框架 以调用带批注的控制器。与Spring MVC测试一样,您有两个 此处可能的替代方案,要么使用“基于上下文”,要么使用“独立”设置:

  • 在 Spring TestContext 框架,注入测试字段,以及 使用它来发送要由控制器方法处理的消息。clientInboundChannel
  • 手动设置调用所需的最低 Spring 框架基础结构 控制器(即)和传递消息 控制器直接到它。SimpAnnotationMethodMessageHandler

这两种设置方案在股票投资组合示例应用程序的测试中进行了演示。

第二种方法是创建端到端集成测试。为此,您需要 以嵌入式模式运行 WebSocket 服务器,并作为 WebSocket 客户端连接到该服务器 发送包含 STOMP 帧的 WebSocket 消息。 股票投资组合示例应用程序的测试还通过使用 Tomcat 作为嵌入式来演示此方法 WebSocket 服务器和用于测试目的的简单 STOMP 客户端。

5. 其他网络框架

本章详细介绍了 Spring 与第三方 Web 框架的集成。

Spring 框架的核心价值主张之一是支持选择。从一般意义上讲,Spring不会强迫您使用或购买任何 特定的架构、技术或方法(尽管它肯定建议 有些超过其他)。这种*选择架构、技术或 与开发人员及其开发团队最相关的方法是 可以说在Web领域最为明显,Spring提供了自己的Web框架。 (Spring MVC和Spring WebFlux) 同时, 支持与许多流行的第三方 Web 框架集成。

5.1. 常用配置

在深入研究每个受支持的 Web 框架的集成细节之前,让我们 首先看一下不特定于任何一个Web的常见Spring配置。 框架。(本节同样适用于 Spring 自己的 Web 框架变体。

Spring轻量级所支持的概念之一(因为没有更好的词) 应用程序模型是分层体系结构的模型。请记住,在“经典”中 分层架构,Web 层只是众多层之一。它作为 服务器端应用程序的入口点,它委托给服务对象 (立面),在服务层中定义以满足特定于业务(和 演示技术不可知)用例。在 Spring 中,这些服务对象,任何其他 特定于业务的对象、数据访问对象和其他对象存在于不同的“业务”中 上下文“,不包含 Web 或表示层对象(表示对象, 如Spring MVC控制器,通常配置在不同的“演示”中 上下文“)。本节详细介绍了如何配置包含应用程序中所有“业务 Bean”的 Spring 容器 (a)。​​WebApplicationContext​

继续细节,您需要做的就是在Web应用程序的标准Jakarta EE servletfile中声明一个ContextLoaderListener,并添加一个<context-param/>部分(在同一文件中)来定义 要加载的 Spring XML 配置文件集。​​web.xml​​​​contextConfigLocation​

请考虑以下配置:​​<listener/>​

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

进一步考虑以下配置:​​<context-param/>​

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

如果不指定上下文参数,则查找调用的文件 负荷。加载上下文文件后,Spring 会根据 bean 定义创建一个WebApplicationContext对象,并将其存储在 Web 中。 应用。​​contextConfigLocation​​​​ContextLoaderListener​​​​/WEB-INF/applicationContext.xml​​​​ServletContext​

所有Java Web框架都建立在Servlet API之上,因此您可以使用 以下代码片段来访问此“业务上下文”由创建。​​ApplicationContext​​​​ContextLoaderListener​

以下示例演示如何获取:​​WebApplicationContext​

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils类是为了方便起见,因此您无需记住属性的名称。它的方法返回如果一个对象 在密钥下不存在。与其冒险进入您的应用程序,不如 使用该方法。此方法引发异常 当不见了。​​ServletContext​​​​getWebApplicationContext()​​​​null​​​​WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE​​​​NullPointerExceptions​​​​getRequiredWebApplicationContext()​​​​ApplicationContext​

一旦你有了对 的引用,你就可以通过他们的 名称或类型。大多数开发人员按名称检索 bean,然后将它们强制转换为他们的一个 实现的接口。​​WebApplicationContext​

幸运的是,本节中的大多数框架都有更简单的查找 bean 的方法。 它们不仅使从 Spring 容器中获取豆子变得容易,而且还可以让您 在其控制器上使用依赖关系注入。每个 Web 框架部分都有更多详细信息 关于其具体的整合战略。

5.2. JSF

JavaServer Faces(JSF)是JCP的标准基于组件,事件驱动的Web 用户界面框架。它是雅加达EE伞的官方部分,但也 可单独使用,例如通过在Tomcat中嵌入Mojarra或MyFaces。

请注意,最新版本的 JSF 与 CDI 基础结构紧密相连 在应用程序服务器中,一些新的 JSF 功能仅在这样的 环境。Spring 的 JSF 支持不再积极发展,主要是 存在是为了在对基于 JSF 的旧应用程序进行现代化改造时进行迁移。

Spring 的 JSF 集成的关键元素是 JSF 机制。​​ELResolver​

5.2.1. 弹簧豆解析器

​SpringBeanFacesELResolver​​是一个符合 JSF 的实现, 与 JSF 和 JSP 使用的标准统一 EL 集成。它委托给 春天的“商业语境”先到后 基础 JSF 实现的缺省解析程序。​​ELResolver​​​​WebApplicationContext​

在配置方面,您可以定义 JSF文件,如以下示例所示:​​SpringBeanFacesELResolver​​​​faces-context.xml​

<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>

5.2.2. 使用​​FacesContextUtils​

自定义在将属性映射到 bean 时效果很好,但有时,您可能需要显式获取 bean。 FacesContextUtils类使这变得简单。它类似于,除了 它采用参数而不是参数。​​ELResolver​​​​faces-config.xml​​​​WebApplicationContextUtils​​​​FacesContext​​​​ServletContext​

以下示例演示如何使用:​​FacesContextUtils​

ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());

5.3. 阿帕奇支柱 2.x

由Craig McClanahan发明,Struts是一个开源项目 由 Apache 软件基金会主办。在当时,它大大简化了 JSP/Servlet编程范式,赢得了许多使用专有语言的开发人员的青睐。 框架。它简化了编程模型,它是开源的(因此是免费的,如 啤酒),它有一个庞大的社区,这让该项目在 Java Web 开发人员。

作为原始 Struts 1.x 的继任者,请查看 Struts 2.x 和 Struts 提供的Spring 插件 内置弹簧集成。

5.4. 阿帕奇挂毯 5.x

Tapestry是一个“”面向组件的框架,用于创建 Java中的动态,健壮,高度可扩展的Web应用程序。

虽然 Spring 有自己强大的 web 层,但有许多独特的 使用 Tapestry 组合构建企业 Java 应用程序的优势 用于 Web 用户界面,Spring 容器用于下层。

有关更多信息,请参阅 Tapestry 的Spring 专用集成模块。