Spring Websocket配置流程分析

By ref-nobody 创建时间 2025年7月31日 | 本文最后更新于 2025年8月16日 #websocket

我们在使用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,我们主要来看stompWebSocketHandlerMappingsubProtocolWebSocketHandler

将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方法,我们可以使用WebSocketTransportRegistrationaddDecoratorFactory方法添加自己的装饰器并且不影响系统已有的其他逻辑。

示例:

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

Leave a Reply

Your email address will not be published. Required fields are marked *

目录