Spring Web(第二部分)

二、REST Clients

他的部分描述了客户端访问REST端点的选项。

2.1。创建RestTemplate
RestTemplate是执行HTTP请求的同步客户机。它是原始的Spring REST客户机,并在底层HTTP客户机库上公开一个简单的模板方法API。

注意:从5.0开始,非阻塞的、反应性的WebClient提供了RestTemplate的现代替代方案,对同步和异步以及流场景都提供了有效的支持。RestTemplate将在未来的版本中被弃用,将来也不会添加主要的新特性。

2.2。WebClient
WebClient是执行HTTP请求的非阻塞的反应性客户机。它是在5.0中引入的,并提供了RestTemplate的现代替代方案,对同步和异步以及流场景都提供了有效的支持。

与RestTemplate不同,WebClient支持以下特性:

  • 非阻塞I / O。
  • 反应性流体返回压力。
  • 具有较少硬件资源的高并发性。
  • 利用Java 8 lambdas的函数式、流畅的API。
  • 同步和异步交互。
  • 从服务器向上或向下串流。

三、Testing

本节总结Spring -test中为Spring MVC应用程序提供的选项。

  • Servlet API Mocks:Servlet API契约的模拟实现,用于单元测试控制器、过滤器和其他web组件。有关更多细节,请参见 Servlet API 模拟对象。
  • TestContext Framework:支持在JUnit和TestNG测试中加载Spring配置,包括跨测试方法高效缓存加载的配置,以及支持用MockServletContext加载WebApplicationContext。有关更多细节,请参见TestContext Framework
  • Spring MVC Test:一个框架,也称为MockMvc,用于通过DispatcherServlet(即支持注释)测试带注释的控制器,该框架具有Spring MVC基础结构,但没有HTTP服务器。有关更多细节,请参见 Spring MVC Test
  • Client-side REST: spring-test提供了一个MockRestServiceServer,您可以将其用作模拟服务器,用于测试内部使用RestTemplate的客户端代码。有关更多细节,请参见 Client REST Tests
  • WebTestClient:用于测试WebFlux应用程序,但它也可以用于通过HTTP连接到任何服务器的端到端集成测试。它是非阻塞的、反应性的客户机,非常适合测试异步和流式场景。

四、WebSockets

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

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

WebSocket交互从一个HTTP请求开始,该请求使用HTTP升级头进行升级,或者在本例中切换到WebSocket协议。下面的例子展示了这种交互:

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

与通常的200状态代码不同,支持WebSocket的服务器返回的输出类似如下:

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

成功握手之后,HTTP升级请求的TCP套接字将保持打开状态,以便客户机和服务器继续发送和接收消息。
关于WebSockets如何工作的完整介绍超出了本文的范围。请参阅RFC 6455, HTML5的WebSocket章节,或Web上的任何介绍和教程。

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

4.4.1。HTTP Versus WebSocket
尽管WebSocket被设计为与HTTP兼容,并从HTTP请求开始,但重要的是要理解这两个协议导致了非常不同的体系结构和应用程序编程模型。

在HTTP和REST中,应用程序被建模为许多url。为了与应用程序交互,客户端访问这些url,请求-响应样式。服务器根据HTTP URL、方法和标头将请求路由到适当的处理程序。
相反,在WebSockets中,初始连接通常只有一个URL。随后,所有应用程序消息都在同一TCP连接上流动。这指向一个完全不同的异步、事件驱动的消息传递体系结构。

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

WebSocket客户端和服务器可以通过HTTP握手请求上的secc -WebSocket- protocol头协商使用更高级别的消息传递协议(例如,STOMP)。在这种情况下,他们需要制定自己的惯例。

4.1.2。何时使用WebSockets
WebSockets可以使web页面变得动态和交互式。然而,在许多情况下,Ajax和HTTP流媒体或长轮询的组合可以提供简单而有效的解决方案。

例如,新闻、邮件和社交提要需要动态更新,但是每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低(例如,监视网络故障),HTTP流媒体或轮询可以提供有效的解决方案。低延迟、高频率和高容量的组合是使用WebSocket的最佳选择。

还要记住,在Internet上,超出您控制范围的限制性代理可能会阻止WebSocket交互,这可能是因为它们没有配置为传递升级报头,也可能是因为它们关闭了看似空闲的长寿命连接。这意味着将WebSocket用于防火墙内的内部应用程序要比面向公共应用程序更加简单。

4.2。WebSocket API

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

4.2.1。准备WebSocketHandler

创建WebSocket服务器与实现WebSocketHandler一样简单,更有可能的是扩展TextWebSocketHandler或BinaryWebSocketHandler。下面的例子使用TextWebSocketHandler:

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

public class MyHandler extends TextWebSocketHandler {

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

}

有专门的WebSocket Java配置和XML命名空间支持映射前面的WebSocket处理程序到一个特定的URL,如下例所示:

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@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 id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

前面的示例用于Spring MVC应用程序,应该包含在DispatcherServlet的配置中。然而,Spring的WebSocket支持并不依赖于Spring MVC。在WebSocketHttpRequestHandler的帮助下,将WebSocketHandler集成到其他http服务环境中是比较简单的。

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

4.2.2。WebSocket握手

定制初始HTTP WebSocket握手请求的最简单方法是通过HandshakeInterceptor,它公开了握手之前和之后的方法。您可以使用这样的拦截器来阻止握手,或者使WebSocketSession可以使用任何属性。下面的示例使用内置的拦截器将HTTP会话属性传递给WebSocket会话:

@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 id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

一个更高级的选项是扩展DefaultHandshakeHandler,它执行WebSocket握手的步骤,包括验证客户端来源、协商子协议和其他细节。如果应用程序需要配置定制的RequestUpgradeStrategy以适应WebSocket服务器引擎和不支持的版本,那么它可能也需要使用这个选项(有关这个主题的更多信息,请参阅部署)。Java配置和XML名称空间都使配置自定义HandshakeHandler成为可能。

注意:Spring提供了一个WebSocketHandlerDecorator基类,您可以使用这个基类用其他行为装饰WebSocketHandler。在使用WebSocket Java配置或XML名称空间时,将默认提供并添加日志记录和异常处理实现。ExceptionWebSocketHandlerDecorator捕获来自任何WebSocketHandler方法的所有未捕获异常,并以状态1011关闭WebSocket会话,这表示服务器出错。

4.2.3。部署
Spring WebSocket API很容易集成到Spring MVC应用程序中,其中DispatcherServlet服务于HTTP WebSocket握手和其他HTTP请求。通过调用WebSocketHttpRequestHandler,也很容易集成到其他HTTP处理场景中。这既方便又容易理解。但是,对于JSR-356运行时有一些特殊的考虑。

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

这是JSR-356的一个重要限制,即Spring的WebSocket支持使用特定于服务器的RequestUpgradeStrategy实现来寻址,即使在JSR-356运行时中也是如此。这样的策略目前存在于Tomcat、Jetty、GlassFish、WebLogic、WebSphere和Undertow(以及WildFly)。

注意:已经创建了一个请求来克服Java WebSocket API中的上述限制,可以在eclipse-ee4j/ WebSocket - API #211中找到它。Tomcat、Undertow和WebSphere提供了它们自己的API替代方案,使得这成为可能,而Jetty也可以做到这一点。我们希望更多的服务器也能这样做。

第二个需要考虑的是,带有JSR-356支持的Servlet容器将执行Servlet containerinitializer (SCI)扫描,这可能会减慢应用程序的启动速度——在某些情况下,速度会非常快。如果在升级到支持JSR-356的Servlet容器版本后观察到重大影响,应该可以通过在web中使用<absolute-order />元素有选择地启用或禁用web片段(和SCI扫描)。xml,如下例所示:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering/>

</web-app>

然后可以根据名称有选择地启用web片段,比如Spring自己的SpringServletContainerInitializer,它提供了对Servlet 3 Java初始化API的支持。下面的例子演示了如何做到这一点:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

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

</web-app>

4.2.4。服务器配置

每个底层WebSocket引擎都公开控制运行时特征的配置属性,例如消息缓冲区大小、空闲超时等。
对于Tomcat、WildFly和GlassFish,您可以将ServletServerContainerFactoryBean添加到您的WebSocket Java配置中,如下面的示例所示:

@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配置,您应该使用WebSocketContainerFactoryBean (XML)或ContainerProvider.getWebSocketContainer() (Java配置)。

对于Jetty,您需要提供预先配置好的Jetty WebSocketServerFactory,并通过WebSocket Java配置将其插入Spring的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 id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
        <constructor-arg ref="upgradeStrategy"/>
    </bean>

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

    <bean id="serverFactory" 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.2.5。允许的起源
在Spring Framework 4.1.5中,WebSocket和SockJS的默认行为是只接受同源请求。也可以允许所有或指定的源列表。这个检查主要是为浏览器客户端设计的。没有任何东西可以阻止其他类型的客户端修改源标头的值(有关更多细节,请参阅RFC 6454: Web Origin概念)。

三种可能的行为是:

  • 只允许同源请求(默认):在此模式下,当启用SockJS时,Iframe HTTP响应头X-Frame-Options被设置为SAMEORIGIN, JSONP传输被禁用,因为它不允许检查请求的起源。因此,在启用此模式时不支持IE6和IE7。
  • 允许指定的起源列表:每个允许的起源必须以http://或https://开头。在此模式下,当启用SockJS时,将禁用IFrame传输。因此,在启用此模式时不支持IE6到IE9。
  • 允许所有原点:要启用此模式,您应该提供*作为允许的原点值。在此模式下,所有传输都是可用的。

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

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@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 id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

4.3。SockJS回退
在公共Internet上,您控制之外的限制性代理可能会阻止WebSocket交互,这可能是因为它们没有配置为传递升级报头,也可能是因为它们关闭了看似空闲的长寿命连接。

这个问题的解决方案是WebSocket模拟——也就是说,首先尝试使用WebSocket,然后退回到基于http的技术,模拟WebSocket交互并公开相同的应用程序级API。
在Servlet堆栈上,Spring框架为SockJS协议提供了服务器(以及客户端)支持。

4.3.1。概述
SockJS的目标是让应用程序使用WebSocket API,但在运行时,如果需要,则返回到非WebSocket替代方案,而不需要更改应用程序代码。
SockJS包括:

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

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

SockJS客户端首先发送GET /info从服务器获取基本信息。在那之后,它必须决定使用什么交通工具。如果可能,使用WebSocket。如果没有,在大多数浏览器中,至少有一个HTTP流选项。如果没有,则使用HTTP (long)轮询。
所有的传输请求都有以下的URL结构:

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

地点:

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

WebSocket传输只需要一个HTTP请求就可以完成WebSocket握手。此后所有消息都在该套接字上交换。
HTTP传输需要更多的请求。例如,Ajax/XHR流依赖于服务器到客户端消息的一个长时间运行的请求和客户端到服务器消息的附加HTTP POST请求。长轮询与此类似,不同之处在于它在每次服务器到客户端发送后结束当前请求。

SockJS增加了最小的消息帧。例如,服务器最初发送字母o(“打开”帧),消息以[“message1”、“message2”](json编码的数组)、字母h(“心跳”帧)(如果25秒内没有消息流(默认),以及字母c(“关闭”帧)来关闭会话。

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

4.3.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 id="myHandler" 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.3.3。IE8和9
ie8和ie9仍在使用。他们是拥有SockJS的关键原因。本节讨论在这些浏览器中运行的重要注意事项。

SockJS客户端通过使用微软的XDomainRequest在ie8和ie9中支持Ajax/XHR流。它可以跨域工作,但不支持发送cookie。cookie通常是Java应用程序所必需的。但是,由于SockJS客户机可以与许多服务器类型(不仅仅是Java类型)一起使用,所以它需要知道cookie是否重要。如果是这样,SockJS客户端更喜欢使用Ajax/XHR进行流处理。否则,它依赖于基于iframe的技术。

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

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

注意:Spring Security 3.2+支持在每个响应上设置X-Frame-Options。默认情况下,Spring Security Java配置将其设置为DENY。在3.2中,Spring Security XML名称空间在默认情况下不设置那个头,但是可以配置成这样。将来,它可能会默认设置它。
有关如何配置X-Frame-Options头的设置的详细信息,请参阅Spring安全文档的默认安全头。您还可以查看sec2501了解更多的背景信息。

如果您的应用程序添加了X-Frame-Options响应标头(它应该这样做!)并依赖于基于iframe的传输,那么您需要将标头值设置为SAMEORIGIN或ALLOW-FROM <origin>。Spring SockJS支持还需要知道SockJS客户机的位置,因为它是从iframe加载的。默认情况下,iframe被设置为从CDN位置下载SockJS客户机。将此选项配置为使用来自与应用程序相同来源的URL是一个好主意。

@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>元素提供了类似的选项。

注意:在初始开发期间,一定要启用SockJS客户端devel模式,以防止浏览器缓存SockJS请求(如iframe),否则将缓存这些请求。有关如何启用它的详细信息,请参阅SockJS客户端页面。

4.3.4。心跳
SockJS协议要求服务器发送心跳消息,以防止代理推断连接被挂起。Spring SockJS配置有一个名为heartbeatTime的属性,您可以使用它来定制频率。默认情况下,心跳在25秒后发送,假设该连接上没有发送其他消息。这个25秒的值符合以下IETF对公共Internet应用程序的推荐。

注意:当在WebSocket和SockJS上使用STOMP时,如果STOMP客户机和服务器协商交换心跳,SockJS心跳将被禁用。

Spring SockJS支持还允许您配置TaskScheduler来调度心跳任务。任务调度器由线程池支持,默认设置基于可用处理器的数量。您应该考虑根据您的具体需要定制设置。

4.3.5。客户端断开连接
HTTP流媒体和HTTP长轮询SockJS传输需要一个连接保持比平时更长的开放时间。有关这些技术的概述,请参阅本文。

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

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

注意:因此,网络I/O故障可能会因为客户端断开连接而发生,这会用不必要的堆栈跟踪填充日志。Spring尽最大努力识别代表客户端断开(特定于每个服务器)的此类网络故障,并使用专用日志类别DISCONNECTED_CLIENT_LOG_CATEGORY(在AbstractSockJsSession中定义)记录最小的消息。如果需要查看堆栈跟踪,可以将日志类别设置为TRACE。

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

还可以通过在Spring的SockJsService中设置suppressCors属性来禁用添加这些CORS标头。
SockJS期望以下头和值:

  • Access-Control-Allow-Origin: Initialized from the value of the Origin request header.

  • Access-Control-Allow-Credentials: Always set to true.

  • Access-Control-Request-Headers: Initialized from values from the equivalent request header.

  • Access-Control-Allow-Methods: The HTTP methods a transport supports (see TransportType enum).

  • Access-Control-Max-Age: Set to 31536000 (1 year).

有关确切的实现,请参见AbstractSockJsService中的addCorsHeaders和源代码中的TransportType enum。
或者,如果CORS配置允许,考虑排除带有SockJS端点前缀的url,从而让Spring的SockJsService处理它。

4.3.7。SockJsClient
Spring提供了一个SockJS Java客户机来连接到远程SockJS端点,而无需使用浏览器。当需要在公共网络上的两个服务器之间进行双向通信时(也就是说,网络代理可以阻止使用WebSocket协议),这一点特别有用。SockJS Java客户机对于测试目的(例如,模拟大量并发用户)也非常有用。

SockJS Java客户端支持websocket、xhr-streaming和xhr-polling传输。剩下的只有在浏览器中使用才有意义。
你可以配置WebSocketTransport与:

  • JSR-356运行时中的StandardWebSocketClient。
  • JettyWebSocketClient通过使用Jetty 9+原生WebSocket API。
  • Spring的WebSocketClient的任何实现。

根据定义,XhrTransport同时支持xhr流和xhr轮询,因为从客户端角度看,除了用于连接到服务器的URL外,没有其他区别。目前有两种实现方式:

  • RestTemplateXhrTransport使用Spring的RestTemplate处理HTTP请求。
  • JettyXhrTransport使用Jetty的HttpClient来处理HTTP请求。

下面的例子展示了如何创建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");

SockJS对消息使用JSON格式的数组。默认情况下,Jackson 2被使用,并且需要在类路径上。或者,您可以配置SockJsMessageCodec的自定义实现,并在SockJsClient上配置它。

要使用SockJsClient来模拟大量并发用户,您需要配置底层HTTP客户机(用于XHR传输)以允许足够数量的连接和线程。下面的例子展示了如何与Jetty:

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) //1
            .setHttpMessageCacheSize(1000) //2
            .setDisconnectDelay(30 * 1000); //3
    }

    // ...
}
  1. 将streamBytesLimit属性设置为512KB(默认为128KB - 128 * 1024)。
  2. 将httpMessageCacheSize属性设置为1,000(缺省值为100)。
  3. 将disconnectDelay属性设置为30个属性秒(默认为5秒- 5 * 1000)。

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

4.1.1。概述
STOMP(简单的面向文本的消息传递协议)最初是为脚本语言(如Ruby、Python和Perl)创建的,用于连接到企业消息代理。它的设计目的是解决常用消息传递模式的一个最小子集。STOMP可用于任何可靠的双向流网络协议,如TCP和WebSocket。虽然STOMP是一种面向文本的协议,但是消息有效负载可以是文本的,也可以是二进制的。

STOMP是一种基于框架的协议,框架是基于HTTP建模的。下面的清单显示了STOMP框架的结构:

COMMAND
header1:value1
header2:value2

Body^@

客户端可以使用SEND或SUBSCRIBE命令发送或订阅消息,以及描述消息内容和谁应该接收消息的目的地标头。这启用了一个简单的发布-订阅机制,您可以使用该机制通过代理将消息发送到其他连接的客户机,或者将消息发送到服务器以请求执行某些工作。

当您使用Spring的STOMP支持时,Spring WebSocket应用程序充当客户端的STOMP代理。消息被路由到@Controller消息处理方法或简单的内存代理,该代理跟踪订阅并向订阅用户广播消息。您还可以将Spring配置为使用专用的STOMP代理(如RabbitMQ、ActiveMQ等)来实际广播消息。在这种情况下,Spring维护到代理的TCP连接,将消息传递给代理,并将消息从代理向下传递到连接的WebSocket客户机。因此,Spring web应用程序可以依赖于统一的基于http的安全性、公共验证和熟悉的消息处理编程模型。

下面的示例显示了一个客户端订阅接收股票报价,服务器可能会定期发出股票报价(例如,通过一个预定的任务,通过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}^@

服务器不能发送未经请求的消息。来自服务器的所有消息必须响应特定的客户机订阅,并且服务器消息的订阅id头必须与客户机订阅的id头匹配。

前面的概述旨在提供对STOMP协议的最基本的理解。我们建议全面审查协议规范。

4.4.2 Benefits
使用STOMP作为子协议使得Spring框架和Spring安全性提供了比使用原始WebSockets更丰富的编程模型。同样的观点也适用于HTTP和原始TCP,以及它如何让Spring MVC和其他web框架提供丰富的功能。以下是一些好处:

  • 不需要发明自定义消息传递协议和消息格式。
  • STOMP客户机(包括Spring框架中的Java客户机)是可用的。
  • 您可以(可选地)使用消息代理(如RabbitMQ、ActiveMQ和其他)来管理订阅和广播消息。
  • 可以在任意数量的@Controller实例中组织应用程序逻辑,并且可以根据STOMP目的地标头将消息路由到它们,而不是针对给定连接使用单个WebSocketHandler处理原始WebSocket消息。
  • 您可以使用Spring Security基于STOMP目的地和消息类型保护消息。

4.4.3。Enable STOMP
在spring-messaging和spring-websocket模块中提供了对WebSocket的支持。一旦有了这些依赖项,就可以通过WebSocket使用SockJS回退来公开STOMP端点,如下面的示例所示:

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app"); //2
        config.enableSimpleBroker("/topic", "/queue"); //3
    }
}
  1. /portfolio是WebSocket(或SockJS)客户端需要连接的用于WebSocket握手的端点的HTTP URL。
  2. 目的地标头以/app开头的STOMP消息被路由到@Controller类中的@MessageMapping方法。
  3. 使用内置的消息代理进行订阅和广播,并将目的地标头以/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>

提示:对于内置的简单代理,/topic和/queue前缀没有任何特殊意义。它们只是一种区分发布-订阅和点到点消息传递(即许多订阅者和一个消费者)的惯例。使用外部代理时,请检查代理的STOMP页面,以了解它支持哪种类型的STOMP目的地和前缀。

要从浏览器连接,对于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) {
}

注意,在前面的示例中,stompClient不需要指定登录和密码 headers。即使这样,它们也会在服务器端被忽略(或者被覆盖)。有关身份验证的更多信息,请参见连接到代理和身份验证。

4.4.4。WebSocket服务器
要配置底层WebSocket服务器,可以应用服务器配置中的信息。但是对于Jetty,你需要通过StompEndpointRegistry设置HandshakeHandler和WebSocketPolicy:

@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.4.5。流的消息
一旦STOMP端点被公开,Spring应用程序就成为连接客户端的STOMP代理。本节描述服务器端上的消息流。

Spring消息传递模块包含对消息传递应用程序的基本支持,这些消息传递应用程序起源于Spring Integration,后来被提取并合并到Spring框架中,以便在许多Spring项目和应用程序场景中广泛使用。下面的列表简要描述了一些可用的消息传递抽象:

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

message flow simple broker

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

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

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

message flow broker relay

前面两个图之间的主要区别是使用“代理中继”,通过TCP将消息向上传递给外部STOMP代理,并将消息向下传递给订阅的客户端。

当从WebSocket连接接收到消息时,它们被解码为STOMP帧,转换为Spring消息表示,并发送到clientInboundChannel进行进一步处理。例如,目的地标头以/app开头的STOMP消息可能被路由到带注释的控制器中的@MessageMapping方法,而/topic和/queue消息可能被直接路由到消息代理。

处理来自客户机的STOMP消息的带注释的@Controller可以通过brokerChannel向消息代理发送消息,代理通过clientOutboundChannel将消息广播给匹配的订阅者。相同的控制器也可以对HTTP请求做出相同的响应,因此客户端可以执行HTTP POST,然后@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. 客户机连接到http://localhost:8080/portfolio,一旦建立了WebSocket连接,STOMP帧就开始在上面流动。
  2. 客户端发送一个带有目标标题/主题/问候语的SUBSCRIBE框架。接收和解码后,消息被发送到clientInboundChannel,然后被路由到存储客户端订阅的message broker。
  3. 客户端向/app/greeting发送一个aSEND帧。/app前缀有助于将其路由到带注释的控制器。去掉/app前缀后,目的地中剩下的/greeting部分被映射到GreetingController中的@MessageMapping方法。
  4. 从GreetingController返回的值被转换成一个Spring消息,该消息的有效负载基于返回值和/topic/greeting的缺省目的地标头(源自输入目的地,/app被/topic替换)。结果消息被发送到brokerChannel并由message broker处理。
  5. message broker找到所有匹配的订阅者,并通过clientOutboundChannel向每个订阅者发送一个消息框架,从那里将消息编码为STOMP框架并通过WebSocket连接发送。

下一节将详细介绍带注释的方法,包括支持的参数类型和返回值。

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

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

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

支持方法参数
下表描述了方法参数:

Method argument Description

Message

For access to the complete message.

MessageHeaders

For access to the headers within the Message.

MessageHeaderAccessorSimpMessageHeaderAccessor, and StompHeaderAccessor

For access to the headers through typed accessor methods.

@Payload

For access to the payload of the message, converted (for example, from JSON) by a configured MessageConverter.

The presence of this annotation is not required since it is, by default, assumed if no other argument is matched.

You can annotate payload arguments with @javax.validation.Valid or Spring’s @Validated, to have the payload arguments be automatically validated.

@Header

For access to a specific header value — along with type conversion using an org.springframework.core.convert.converter.Converter, if necessary.

@Headers

For access to all headers in the message. This argument must be assignable to java.util.Map.

@DestinationVariable

For access to template variables extracted from the message destination. Values are converted to the declared method argument type as necessary.

java.security.Principal

Reflects the user logged in at the time of the WebSocket HTTP handshake.

返回值
默认情况下,@MessageMapping方法的返回值通过一个匹配的MessageConverter序列化到一个有效负载,并作为消息发送到brokerChannel,从brokerChannel广播到订阅者。出站消息的目的地与入站消息的目的地相同,但以/topic作为前缀。

您可以使用@SendTo和@SendToUser注释来定制输出消息的目的地。@SendTo用于自定义目标目的地或指定多个目的地。@SendToUser用于将输出消息仅定向到与输入消息关联的用户。看到用户目的地。

您可以在同一方法上同时使用@SendTo和@SendToUser,它们在类级别都受到支持,在这种情况下,它们充当类中方法的默认值。但是,请记住,任何方法级别的@SendTo或@SendToUser注释都会覆盖类级别的任何此类注释。
消息可以异步处理,@MessageMapping方法可以返回ListenableFuture、CompletableFuture或CompletionStage。

注意,@SendTo和@SendToUser只是一种便利,相当于使用SimpMessagingTemplate发送消息。如果需要,对于更高级的场景,@MessageMapping方法可以直接使用SimpMessagingTemplate。可以这样做,而不是返回一个值,或者可能是附加返回一个值。看到发送消息。

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

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

什么时候这没有用?不要试图将代理和控制器映射到相同的目的地前缀,除非出于某种原因希望它们都独立地处理消息(包括订阅)。入站消息是并行处理的。不能保证代理或控制器首先处理给定的消息。如果目标是在存储订阅并准备进行广播时得到通知,那么如果服务器支持,客户机应该请求接收(simple broker不支持)。例如,使用Java STOMP客户端,您可以执行以下操作来添加收据:

@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(() -> {
    // Subscription ready...
});

服务器端选项是在brokerChannel上注册一个ExecutorChannelInterceptor,并实现在消息(包括订阅)处理后调用的aftermessagemanaged方法。

@MessageExceptionHandler
应用程序可以使用@MessageExceptionHandler方法来处理来自@MessageMapping方法的异常。如果希望访问异常实例,可以在注释本身中声明异常,也可以通过方法参数声明异常。下面的例子通过一个方法参数声明了一个异常:

@Controller
public class MyController {

    // ...

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

@MessageExceptionHandler方法支持灵活的方法签名,并且支持与@MessageMapping方法相同的方法参数类型和返回值。
通常,@MessageExceptionHandler方法应用于声明它们的@Controller类(或类层次结构)中。如果您想要这些方法更全局地(跨控制器)应用,您可以在一个标记为@ControllerAdvice的类中声明它们。这与Spring MVC中提供的类似支持相当。

4.4.7。发送消息
如果希望从应用程序的任何部分向连接的客户机发送消息,该怎么办?任何应用程序组件都可以将消息发送到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.4.8。Simple Broker(简单的代理)
内置的简单消息代理处理来自客户机的订阅请求,将它们存储在内存中,并将消息广播给具有匹配目的地的连接客户机。代理支持类似路径的目的地,包括订阅ant样式的目的地模式。

提示;应用程序还可以使用点分隔(而不是斜杠分隔)的目的地。把点看作分隔符。

如果配置了任务调度程序,则简单代理支持STOMP心跳。为此,您可以声明自己的调度器,或者使用自动声明并在内部使用的调度器。下面的例子展示了如何声明自己的调度程序:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private TaskScheduler messageBrokerTaskScheduler;

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

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/queue/", "/topic/")
                .setHeartbeatValue(new long[] {10000, 20000})
                .setTaskScheduler(this.messageBrokerTaskScheduler);

        // ...
    }
}

4.4.9。外部Broker
简单代理非常适合于入门,但是它只支持STOMP命令的一个子集(它不支持ack、收据和其他一些特性),依赖于一个简单的消息发送循环,不适合集群。作为一种替代方法,您可以升级您的应用程序以使用全功能的消息Broker。

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

下面的配置示例启用了功能齐全的Broker:

@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 MessageHandler,它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的TCP连接,将所有消息转发给代理,然后将从代理接收到的所有消息通过它们的WebSocket会话转发给客户机。本质上,它充当一个“中继”,向两个方向转发消息。

提示:将io.projectreactor.netty: reator -netty和io.netty:netty—TCP连接管理项目的所有依赖项。

此外,应用程序组件(如HTTP请求处理方法、业务服务和其他组件)还可以将消息发送到代理中继(如发送消息中所述),以便将消息广播到订阅的WebSocket客户端。

实际上,代理中继支持健壮的、可伸缩的消息广播。

4.4.10。连接到Broker(代理)
STOMP代理中继维护到代理的单个“系统”TCP连接。此连接仅用于来自服务器端应用程序的消息,而不用于接收消息。您可以为此连接配置STOMP凭据(即STOMP框架登录和密码头)。这在XML名称空间和Java配置中都公开为systemLogin和systemPasscode属性,默认值为guest和guest。

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

提示:STOMP代理中继总是在代表客户端转发给代理的每个连接帧上设置登录和密码头。因此,WebSocket客户端不需要设置这些头。他们将被忽略。正如认证部分所解释的,WebSocket客户端应该依赖HTTP认证来保护WebSocket端点并建立客户端身份。

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

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

4.4.11。点作为分隔符
当消息被路由到@MessageMapping方法时,它们与AntPathMatcher匹配。默认情况下,模式应该使用斜杠(/)作为分隔符。这是web应用程序中的一个很好的约定,与HTTP url类似。但是,如果您更习惯于消息传递约定,则可以切换到使用点(.)作为分隔符。
下面的示例展示了如何在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 id="pathMatcher" 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.4.12。身份验证
每次跨WebSocket消息传递会话都以HTTP请求开始。这可以是一个升级到WebSockets的请求(即一个WebSocket握手),或者,在SockJS回退的情况下,是一系列的SockJS HTTP传输请求。

许多web应用程序已经具备了身份验证和授权来保护HTTP请求。通常,用户通过Spring Security通过某种机制(如登录页面、HTTP基本身份验证或其他方式)进行身份验证。经过身份验证的用户的安全上下文保存在HTTP会话中,并与同一基于cookie的会话中的后续请求相关联。

因此,对于WebSocket握手或SockJS HTTP传输请求,通常已经有一个通过HttpServletRequest#getUserPrincipal()访问的经过身份验证的用户。Spring自动将该用户与为其创建的WebSocket或SockJS会话关联起来,然后,通过用户标头将所有STOMP消息传输到该会话。

简而言之,一个典型的web应用程序不需要做任何事情,除了它已经做的安全方面的事情。用户在HTTP请求级别通过安全上下文进行身份验证,该安全上下文通过基于cookie的HTTP会话(然后该会话与为该用户创建的WebSocket或SockJS会话相关联)进行维护,并导致在通过应用程序的每个消息流上都有一个用户标头。

注意,STOMP协议在CONNECT帧上确实有登录和密码头。它们最初是为TCP上的STOMP而设计的,现在仍然需要。但是,对于STOMP over WebSocket,在默认情况下,Spring忽略STOMP协议级别的授权头,假设用户已经在HTTP传输级别进行了身份验证,并期望WebSocket或SockJS会话包含经过身份验证的用户。

提示:Spring Security提供了WebSocket子协议授权,该授权使用一个ChannelInterceptor对基于用户标头的消息进行授权。此外,Spring Session提供了一个WebSocket集成,确保用户的HTTP会话在WebSocket会话仍然处于活动状态时不会过期。

4.4.13 令牌验证

Spring Security OAuth支持基于令牌的安全性,包括JSON Web令牌(JWT)。您可以将其用作Web应用程序中的身份验证机制,包括前一节中描述的对WebSocket交互的践踏(即通过基于cookie的会话来维护身份)。

与此同时,基于cookie的会话并不总是最佳选择(例如,在不维护服务器端会话的应用程序中,或者在通常使用header进行身份验证的移动应用程序中)。

WebSocket协议RFC 6455“没有规定任何特定的方式,服务器可以在WebSocket握手期间对客户端进行身份验证。”然而,在实践中,浏览器客户机只能使用标准的身份验证头(即基本HTTP身份验证)或cookie,而不能(例如)提供自定义头。同样,SockJS JavaScript客户机也不提供使用SockJS传输请求发送HTTP头信息的方法。参见sockjs-客户端第196期。相反,它允许发送您可以用来发送令牌的查询参数,但是这有其自身的缺点(例如,令牌可能在不经意间与服务器日志中的URL一起被记录)。

提示:前面的限制是针对基于浏览器的客户机的,并不适用于基于Spring java的STOMP客户机,它支持使用WebSocket和SockJS请求发送报头。

因此,希望避免使用cookie的应用程序可能在HTTP协议级别没有任何好的身份验证替代方案。与使用cookie不同,他们可能更喜欢使用STOMP消息协议级别的消息头进行身份验证,这需要两个简单的步骤:

  1. 使用STOMP客户端在连接时传递身份验证头。
  2. 使用通道拦截器处理身份验证标头。

下一个示例使用服务器端配置来注册自定义身份验证拦截器。注意,拦截器只需要对CONNECT消息进行身份验证并设置用户头。Spring注释并保存经过身份验证的用户,并将其与同一会话上的后续STOMP消息关联起来。下面的例子展示了如何注册一个自定义认证拦截器:

@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 Security的消息授权时,目前您需要确保身份验证通道拦截器配置的顺序比Spring Security的早。最好的方法是在WebSocketMessageBrokerConfigurer的实现中声明自定义拦截器,并使用@Order(Ordered)进行标记。HIGHEST_PRECEDENCE + 99)。

4.4.14。用户目的地
应用程序可以发送针对特定用户的消息,Spring的STOMP支持可以识别带/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;
    }
}

如果用户有多个会话,默认情况下,所有订阅到给定目的地的会话都是目标。但是,有时可能需要只针对发送正在处理的消息的会话。可以将broadcast属性设置为false,如下面的示例所示:

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

提示:虽然用户目的地通常意味着经过身份验证的用户,但这并不是严格要求的。未与经过身份验证的用户关联的WebSocket会话可以订阅用户目的地。在这种情况下,@SendToUser注释的行为与broadcast=false完全相同(即仅针对发送正在处理的消息的会话)。

您可以从任何应用程序组件向用户目的地发送消息,例如,注入由Java配置或XML名称空间创建的SimpMessagingTemplate。(bean名是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());
    }
}

提示:当您将用户目的地与外部消息代理一起使用时,您应该检查代理文档,了解如何管理不活动的队列,以便在用户会话结束时删除所有惟一的用户队列。例如,RabbitMQ在使用/exchange/amq.direct/position-updates等目的地时创建自动删除队列。因此,在这种情况下,客户端可以订阅/user/exchange/amq.direct/position-updates。类似地,ActiveMQ有用于清除不活动目的地的配置选项。

在多应用程序服务器场景中,由于用户连接到不同的服务器,因此用户目的地可能无法解析。在这种情况下,可以将目标配置为广播未解析的消息,以便其他服务器有机会尝试。这可以通过Java配置中的MessageBrokerRegistry的userDestinationBroadcast属性和XML中的message-broker元素的user- destinationbroadcast属性来实现。

4.4.15。消息的有序性
来自代理的消息被发布到clientOutboundChannel,从那里它们被写到WebSocket会话。由于通道由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.4.16。事件
发布了几个ApplicationContext事件,可以通过实现Spring的ApplicationListener接口来接收:

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

提示:当您使用功能齐全的代理时,如果代理暂时不可用,STOMP“代理中继”将自动重新连接“系统”连接。但是,客户端连接不会自动重新连接。假设心跳被激活,客户端通常会注意到代理在10秒内没有响应。客户端需要实现自己的重新连接逻辑。

4.4.17。拦截
事件为STOMP连接的生命周期提供通知,但不是为每个客户端消息提供。应用程序还可以注册一个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;
    }
}

应用程序还可以实现ExecutorChannelInterceptor,它是ChannelInterceptor的子接口,在处理消息的线程中有回调。对于发送到通道的每个消息,调用一次ChannelInterceptor,而ExecutorChannelInterceptor在每个订阅了通道消息的MessageHandler的线程中提供挂钩。

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

4.4.18。STOMP客户
Spring提供了对WebSocket客户机的跳转和对TCP客户机的跳转。
首先,您可以创建和配置WebSocketStompClient,如下面的示例所示:

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

在前面的例子中,您可以用SockJsClient替换StandardWebSocketClient,因为它也是WebSocketClient的一个实现。SockJsClient可以使用WebSocket或基于http的传输作为后备。更多细节,见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");

您还可以订阅目的地。订阅方法需要订阅消息的处理程序,并返回可用于取消订阅的订阅句柄。对于每个接收到的消息,处理程序可以指定负载应该反序列化到的目标对象类型,如下面的示例所示:

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

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

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

});

要启用STOMP心跳,您可以使用TaskScheduler配置WebSocketStompClient并可选地自定义心跳间隔(10秒用于写不活动,这将导致发送心跳,10秒用于读不活动,这将关闭连接)。

提示:当您使用WebSocketStompClient进行性能测试来模拟来自同一台机器的数千个客户机时,请考虑关闭心跳,因为每个连接都安排了自己的心跳任务,而且这并没有针对运行在同一台机器上的大量客户机进行优化。

STOMP协议还支持收据,其中客户端必须添加一个收据头,服务器在处理发送或订阅之后用一个收据帧对其进行响应。为了支持这一点,StompSession提供了setAutoReceipt(布尔值),它导致在每个后续的发送或订阅事件上添加一个收据头。

或者,您也可以手动将一个收据头添加到StompHeaders。send和subscribe都返回一个Receiptable实例,您可以使用它来注册接收成功和失败回调。对于此功能,您必须使用TaskScheduler和在收据过期之前的时间(默认为15秒)配置客户端。

请注意,StompSessionHandler本身是一个StompFrameHandler,除了处理消息处理中的异常的handleException回调和处理包括ConnectionLostException在内的传输级别错误的handleTransportError之外,它还允许处理错误帧。

4.4.19。WebSocket范围
每个WebSocket会话都有一个属性映射。该映射作为头文件附加到入站客户端消息,可以通过控制器方法访问,如下面的示例所示:

@Controller
public class MyController {

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

您可以在websocket范围内声明一个spring管理的bean。您可以将websocket作用域的bean注入到控制器和注册在clientInboundChannel上的任何通道拦截器中。它们通常是单例的,比任何单独的WebSocket会话都要长。因此,您需要为websocket范围的bean使用范围代理模式,如下面的例子所示:

@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在第一次从控制器访问一个新的MyBean实例时初始化它,并将该实例存储在WebSocket会话属性中。随后返回相同的实例,直到会话结束。websocket范围的bean调用了所有Spring生命周期方法,如前面的示例所示。

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

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

最明显的起点是配置支持clientInboundChannel和clientOutboundChannel的线程池。默认情况下,两者的配置都是可用处理器数量的两倍。
如果注释方法中的消息处理主要是cpu绑定的,那么clientInboundChannel的线程数量应该与处理器数量保持接近。如果它们所做的工作有更多的io限制,并且需要阻塞或等待数据库或其他外部系统,那么线程池的大小可能需要增加。

提示:ThreadPoolExecutor有三个重要的属性:核心线程池大小、最大线程池大小,以及队列存储没有可用线程的任务的能力。

ThreadPoolExecutor有三个重要的属性:核心线程池大小、最大线程池大小,以及队列存储没有可用线程的任务的能力。
一个常见的混淆点是,配置核心池大小(例如,10)和最大池大小(例如,20)会导致线程池中有10到20个线程。实际上,如果容量保留默认值Integer。MAX_VALUE,线程池的大小不会超过核心池的大小,因为所有附加的任务都会排队。

请参阅ThreadPoolExecutor的javadoc,以了解这些属性是如何工作的,并了解各种队列策略。

在clientOutboundChannel端,主要是向WebSocket客户端发送消息。如果客户端在一个高速网络上,线程的数量应该与可用处理器的数量保持接近。如果它们运行缓慢或带宽较低,它们将花费更长的时间来消耗消息,并给线程池带来负担。因此,有必要增加线程池的大小。

尽管可以预测clientInboundChannel的工作负载(毕竟,它是基于应用程序所做的工作的),但是如何配置“clientOutboundChannel”比较困难,因为它是基于应用程序无法控制的因素。因此,与发送消息相关的另外两个属性是: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客户机(如JavaScript的webstomp-client和其他客户机)在16K边界处分割较大的STOMP消息,并将它们作为多个WebSocket消息发送,这需要服务器进行缓冲和重新组装。

Spring的STOMP-over-WebSocket支持做到了这一点,因此应用程序可以配置STOMP消息的最大大小,而不管WebSocket服务器特定的消息大小。请记住,WebSocket消息大小是自动调整的,如果需要的话,可以确保它们至少可以携带16K的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.4.21。监控
当您使用@EnableWebSocketMessageBroker或<websocket:message-broker>时,关键基础结构组件将自动收集统计信息和计数器,这些统计信息和计数器为了解应用程序的内部状态提供了重要信息。该配置还声明了WebSocketMessageBrokerStats类型的bean,它在一个地方收集所有可用信息,默认情况下每30分钟在INFO级别记录一次。这个bean可以通过Spring的mbean出口商导出到JMX,以便在运行时查看(例如,通过JDK的jconsole)。以下列表总结了可用的信息:

Client WebSocket Sessions

Current

指示当前有多少客户端会话,通过WebSocket和HTTP流和轮询SockJS会话进一步细分。

Total

指示已建立的总会话数。

Abnormally Closed

Connect Failures

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

Send Limit Exceeded

会话在超过配置的发送超时或发送缓冲区限制后关闭,对于速度较慢的客户机可能会出现这种情况(请参阅前面的部分)。

Transport Errors

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

STOMP Frames

处理的连接、已连接和断开连接的帧的总数,指示在STOMP级别上连接的客户端数量。请注意,当会话非正常关闭或客户端关闭而不发送断开连接的帧时,断开连接计数可能更低。

STOMP Broker Relay

TCP Connections

指示代表客户端WebSocket会话向代理建立了多少个TCP连接。这应该等于客户端WebSocket会话的数量加上一个额外的用于从应用程序内部发送消息的共享“系统”连接。

STOMP Frames

代表客户端转发给代理或从代理接收到的连接、已连接和断开连接的帧的总数。请注意,无论客户端WebSocket会话是如何关闭的,都会向代理发送一个断开连接的帧。因此,较低的断开帧计数表明代理正在主动关闭连接(可能是因为未及时到达的心跳、无效的输入帧或其他问题)。

Client Inbound Channel

来自支持clientInboundChannel的线程池的统计信息,该线程池提供对传入消息处理健康状况的深入了解。在这里排队的任务表明应用程序可能太慢,无法处理消息。如果存在I/O绑定的任务(例如,缓慢的数据库查询、对第三方REST API的HTTP请求等),请考虑增加线程池的大小。

Client Outbound Channel

来自支持clientOutboundChannel的线程池的统计数据,该通道提供了对向客户端广播消息的健康状况的深入了解。在这里排队的任务表明客户端太慢,无法使用消息。解决这个问题的一种方法是增加线程池的大小,以适应预期的并发慢客户机数量。另一个选项是减少发送超时和发送缓冲区大小限制(参见前一节)。

SockJS Task Scheduler

用于发送心跳的SockJS任务调度程序的线程池的统计信息。注意,当在STOMP级别协商心跳时,SockJS心跳将被禁用。

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

这两种方法并不相互排斥。相反,它们在整个测试策略中都占有一席之地。服务器端测试更集中,更容易编写和维护。另一方面,端到端集成测试更完整,测试更多,但是编写和维护它们也更复杂。

服务器端测试的最简单形式是编写控制器单元测试。但是,这还不够有用,因为控制器的很多工作都依赖于它的注释。纯粹的单元测试根本无法测试它。

理想情况下,应该在运行时调用被测试的控制器,就像使用Spring MVC测试框架来测试处理HTTP请求的控制器一样——即不运行Servlet容器,而是依赖Spring框架来调用带注释的控制器。与Spring MVC测试一样,你有两个可能的选择,要么使用“基于上下文”的设置,要么使用“独立”的设置:

  • 在Spring TestContext框架的帮助下加载实际的Spring配置,将clientInboundChannel作为测试字段注入,并使用它发送由控制器方法处理的消息。
  • 手动设置调用控制器(即SimpAnnotationMethodMessageHandler)所需的最小Spring框架基础结构,并将控制器的消息直接传递给它。

这两个设置场景都在股票投资组合示例应用程序的测试中得到了演示。
第二种方法是创建端到端集成测试。为此,您需要以嵌入式模式运行WebSocket服务器,并作为发送包含STOMP帧的WebSocket消息的WebSocket客户端连接到它。股票投资组合示例应用程序的测试也演示了这种方法,将Tomcat用作嵌入式WebSocket服务器,并使用一个简单的STOMP客户机进行测试。

 

5. 其他Web框架
本章详细介绍Spring与第三方web框架的集成。
Spring框架的核心价值主张之一是支持选择。一般来说,Spring并不强迫您使用或购买任何特定的体系结构、技术或方法(尽管它肯定会推荐一些优于其他的方法)。这种自由选择架构,技术,或方法最相关的开发人员和他们的开发团队可以说是在web领域表现得最为明显,在Spring提供了自己的web框架(Spring MVC和Spring WebFlux),与此同时,支持与许多流行的第三方web框架的集成。

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

Spring的轻量级应用程序模型所支持的概念之一(没有更好的说法)是分层体系结构。请记住,在“经典”的分层体系结构中,web层只是许多层中的一个。它充当服务器端应用程序的入口点之一,并将其委托给服务层中定义的服务对象(facade),以满足特定于业务的(与表示技术无关的)用例。在Spring中,这些服务对象、任何其他特定于业务的对象、数据访问对象和其他对象存在于不同的“业务上下文”中,而“业务上下文”不包含web或表示层对象(表示对象,如Spring MVC控制器,通常在不同的“表示上下文”中配置)。本节详细介绍如何配置Spring容器(WebApplicationContext),该容器包含应用程序中的所有“业务bean”。

继续细节,所有你需要做的就是声明一个ContextLoaderListener在标准的Java EE web应用程序的servlet的web . XML文件,并添加一个contextConfigLocation < context-param / >部分(在同一个文件中),它定义了它的Spring XML配置文件加载。
考虑以下<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>

如果没有指定contextConfigLocation上下文参数,ContextLoaderListener将查找要加载的文件/WEB-INF/applicationContext.xml。加载上下文文件后,Spring基于bean定义创建WebApplicationContext对象,并将其存储在web应用程序的ServletContext中。

所有Java web框架都构建在Servlet API之上,因此您可以使用以下代码片段来访问这个由ContextLoaderListener创建的“业务上下文”ApplicationContext。
下面的例子展示了如何获取WebApplicationContext:

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

webapplicationcontext类是为了方便起见,所以您不需要记住ServletContext属性的名称。如果一个对象在WebApplicationContext下不存在,那么它的getWebApplicationContext()方法返回null。ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE关键。与其冒着在应用程序中获得nullpointerexception的风险,不如使用getRequiredWebApplicationContext()方法。该方法在ApplicationContext丢失时抛出异常。

一旦有了对WebApplicationContext的引用,就可以根据bean的名称或类型检索它们。大多数开发人员根据名称检索bean,然后将它们转换为实现的接口之一。

幸运的是,本节中的大多数框架都有更简单的查找bean的方法。它们不仅使从Spring容器获取bean变得很容易,而且还允许您在它们的控制器上使用依赖项注入。每个web框架部分都有关于其特定集成策略的更多细节。

5.2。JSF
JavaServer Faces (JSF)是JCP标准的基于组件、事件驱动的web用户界面框架。它是Java EE保护伞的正式部分,但也可以单独使用,例如通过在Tomcat中嵌入Mojarra或MyFaces。

请注意,最新版本的JSF与应用服务器中的CDI基础设施紧密联系在一起,一些新的JSF功能只能在这样的环境中工作。Spring的JSF支持不再是主动演化的,主要是为了在更新较旧的基于JSF的应用程序时进行迁移而存在的。
Spring的JSF集成中的关键元素是JSF ELResolver机制。

5.2.1。Spring Bean解析器
SpringBeanFacesELResolver是一个与JSF兼容的ELResolver实现,与JSF和JSP使用的标准统一EL集成。它首先委托给Spring的“业务上下文”WebApplicationContext,然后委托给底层JSF实现的默认解析器。

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

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

5.2.2。使用FacesContextUtils
自定义ELResolver在将属性映射到faces-config中的bean时工作得很好。但是,有时可能需要显式地获取一个bean。facescontext类简化了这一过程。它类似于webapplicationcontext,除了它接受FacesContext参数而不是ServletContext参数。

下面的例子展示了如何使用facescontext:

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

5.3。Apache Struts 2.倍
Struts由Craig McClanahan发明,是Apache软件基金会主持的一个开源项目。当时,它极大地简化了JSP/Servlet编程范式,并赢得了许多使用专有框架的开发人员的支持。它简化了编程模型,它是开源的(因此就像beer一样是免费的),而且它有一个大型社区,这让项目得以发展并在Java web开发人员中流行起来。

作为原来Struts 1.x的继承者。检查struts2.x和struts提供的用于内置Spring集成的Spring插件。

5.4。Apache Tapestry 5.倍
Tapestry是一个面向组件的框架,用于在Java中创建动态、健壮、高度可伸缩的web应用程序。
虽然Spring有自己强大的web层,但是通过将Tapestry用于web用户界面和Spring容器用于较低层,构建企业Java应用程序有许多独特的优点。
有关更多信息,请参见Tapestry的Spring专用集成模块。

5.5。进一步的资源
下面的链接提供本章中描述的各种web框架的更多参考资料。

  • JSF的主页
  • Struts主页
  • Tapestry主页

 

发布了19 篇原创文章 · 获赞 0 · 访问量 179
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览