我们在使用Spring Websocket的时候需要添加下面的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Security WebSocket 支持 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
问题开始于想要开发一个聊天应用程序,需要保存已经通过websocket建立连接的session,当向某个客户端发送消息的时候,检查对方是否在线。如果不在线,则保存离线消息。首先从配置websocket开始:
配置websocket
使用websocket的时候首先需要配置好websocket的端点信息,用于客户端建立连接。一般会使用实现WebSocketMessageBrokerConfigurer
接口的方式,通过里面的registerStompEndpoints
方法进行注册,如果是与spring security进行集成的话,可以继承AbstractSecurityWebSocketMessageBrokerConfigurer
类
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*");
}
}
通过上面的方式我们定义了一个WebSocketMessageBrokerConfigurer类型的配置类。在项目运行的时候,这个类会被注入到DelegatingWebSocketMessageBrokerConfiguration
类中,在这个类中会自动注入容器内所有的WebSocketMessageBrokerConfigurer
配置:
private final List<WebSocketMessageBrokerConfigurer> configurers = new ArrayList<>();
@Autowired(required = false)
public void setConfigurers(List<WebSocketMessageBrokerConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addAll(configurers);
}
}
这也是一个配置类,也会被自动加载到spring容器中。同时在这个类中声明了多个@Bean,我们主要来看stompWebSocketHandlerMapping
和subProtocolWebSocketHandler
。
将websocket路径转换为HandlerMapping
@Bean
public HandlerMapping stompWebSocketHandlerMapping(
WebSocketHandler subProtocolWebSocketHandler, TaskScheduler messageBrokerTaskScheduler) {
WebSocketHandler handler = decorateWebSocketHandler(subProtocolWebSocketHandler);
WebMvcStompEndpointRegistry registry =
new WebMvcStompEndpointRegistry(handler, getTransportRegistration(), messageBrokerTaskScheduler);
ApplicationContext applicationContext = getApplicationContext();
if (applicationContext != null) {
registry.setApplicationContext(applicationContext);
}
registerStompEndpoints(registry);
return registry.getHandlerMapping();
}
@Bean
public WebSocketHandler subProtocolWebSocketHandler(
AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel) {
return new SubProtocolWebSocketHandler(clientInboundChannel, clientOutboundChannel);
}
我们可以看到,在这个类中会创建WebMvcStompEndpointRegistry
对象,里面由两部分组成:
– WebSocketHandler
:处理WebSocket消息和生命周期事件;
– StompSubProtocolHandler
:子协议处理器,可以处理STOMP的1.0, 1.1, and 1.2版本;
webSocketHandler
是由下面的SubProtocolWebSocketHandler
bean自动注入的。
我们进一步说明一下WebSocketHandler
接口,在这个接口中定义了websocket的生命周期事件,在这里需要提到的是我们可以监听连接和断开的两个事件:
– SessionConnectedEvent
:服务器接收到建立stomp的连接请求之后,处理完成会响应一个CONNECTED命令,此时会发布SessionConnectedEvent
事件。
– SessionDisconnectEvent
:在WebSocketHandler的afterConnectionClosed方法中或者在响应客户端消息的时候如果出现了异常,则会清理websocket session,此时会发布SessionDisconnectEvent事件。
需要注意的是WebSocketHandler 是 Spring WebSocket 中的基础组件,afterConnectionEstablished 方法是在 WebSocket 连接建立后被调用的,这时候还没有进行 STOMP 协议的握手和处理,只是底层的 WebSocket 连接建立了。而 SessionConnectedEvent 是在 STOMP 协议层面的会话建立后触发的事件。
这个@Bean返回的是HandlerMapping类型的bean,这里实际上就是将websocket也统一成了我们熟悉的http的handlermapping,这里最终返回WebSocketHandlerMapping实例对象。

类的序列图如下:

接下来我们来看是如何返回handlermapping的,我们可以看到上面的实现是通过调用registry的getHandlerMapping方法返回的。在方法中,会遍历List<WebMvcStompWebSocketEndpointRegistration> registrations
从而组装成MultiValueMap<HttpRequestHandler, String>
映射最终返回。
registrations
是在我们调用addEndpoint
方法的时候添加进去的,在这个方法中会创建WebMvcStompWebSocketEndpointRegistration
并添加到registrations
集合中。
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket")
.setAllowedOriginPatterns("*");
}
在不使用socketjs的场景下,HttpRequestHandler就是WebSocketHttpRequestHandler。在WebSocketHttpRequestHandler中有两个重要的处理器:WebSocketHandler和HandshakeHandler。
HandshakeHandler
处理websocket连接的时候的握手逻辑。
WebSocketHandler
处理WebSocket消息和生命周期事件。
拓展方式
我们观察在创建WebSocketHttpRequestHandler的时候,在构造器内会对传入的WebSocketHandler进行装饰,目前内置的装饰有两种:ExceptionWebSocketHandlerDecorator和LoggingWebSocketHandlerDecorator。
protected WebSocketHandler decorate(WebSocketHandler handler) {
return new ExceptionWebSocketHandlerDecorator(new LoggingWebSocketHandlerDecorator(handler));
}
这个方法是protected类型的,说明我们可以继承该类并且重写decorate方法来增加自己的装饰器。但是在系统创建HttpRequestHandler的时候使用的是new WebSocketHttpRequestHandler,因此我们需要修改这些地方。最终会向上追溯到addEndpoint方法。
完整路径是:WebMvcStompEndpointRegistry.addEndpoint -> WebMvcStompWebSocketEndpointRegistration.getMappings方法都需要重写,而这几个都是配置websocket的关键组件,不推荐这样来拓展。
另一种方式:
上面我们提到了@Bean的时候的stompWebSocketHandlerMapping方法,我们来看看,在创建WebMvcStompEndpointRegistry之前会调用decorateWebSocketHandler
方法,来对自动注入的WebSocketHandler进行装饰,这个装饰发生在创建WebSocket的HandlerMapping之前。
protected WebSocketHandler decorateWebSocketHandler(WebSocketHandler handler) {
for (WebSocketHandlerDecoratorFactory factory : getTransportRegistration().getDecoratorFactories()) {
handler = factory.decorate(handler);
}
return handler;
}
可以看到是通过装饰器工厂的方式来创建WebSocketHandlerDecorator
装饰器。装饰器工厂类是通过getTransportRegistration
进行配置,对应到WebSocketMessageBrokerConfigurer
则是configureWebSocketTransport
方法,我们可以使用WebSocketTransportRegistration
的addDecoratorFactory
方法添加自己的装饰器并且不影响系统已有的其他逻辑。
示例:
1. 创建装饰器工厂
public class OnlineWebSocketHandlerDecoratorFactory implements WebSocketHandlerDecoratorFactory {
@Override
public WebSocketHandler decorate(WebSocketHandler handler) {
return new OnlineWebSocketHandlerDecorator(handler);
}
}
2. 创建装饰器
public class OnlineWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
private final Map<String, WebSocketSession> onlineSessions = new ConcurrentHashMap<>();
public OnlineWebSocketHandlerDecorator(WebSocketHandler delegate) {
super(delegate);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 保存session信息
Principal principal = session.getPrincipal();
String username = principal.getName();
onlineSessions.put(username, session);
System.out.println("onlineSessions = " + onlineSessions);
super.afterConnectionEstablished(session);
}
public boolean isUserOnline(String username) {
return onlineSessions.containsKey(username) && onlineSessions.get(username).isOpen();
}
}
3. 配置websocket
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new OnlineWebSocketHandlerDecoratorFactory());
}
}