Spring Boot如何整合Spring WebSocket实现实时通信?

来源:IPIPP.com作者:BIT程序员头衔:程序员
导读:本期聚焦于BIT程序员创作的《Spring Boot如何整合Spring WebSocket实现实时通信?》,敬请观看详情。实时通信功能几乎是协作类应用绕不开的需求,基于HTTP的短轮询或长轮询在延迟、连接数和服务器资源上都有明显短板。WebSocket通过一次握手建立持久连接,支持服务端主动推送,是构建聊天、消息通知、协同编辑等场景的更优选择。Spring Boot对WebSocket提供了良好支持,既可以基于javax.websocket规范快速注册端点,也可以结合Spring Messaging和STOMP协议实现消息代理、订阅发布等高级能力。本文将从一个可运行的聊天室示例入手,演示依赖引入、配置类编写、端点注册、Session管理以及前端连接脚本,同时对比原生WebSocket与STOMP方案的差异,并补充心跳、跨域、路径拦截等生产环境常见处理。读者可以按步骤搭建出完整的实时通信服务,并根据业务特点选择合适的实现方式。

HTTP协议的工作模式是请求-响应,客户端不发起请求,服务器就无法主动推送数据。早期实现实时通知时,开发者常使用短轮询或长轮询,前者频繁发起HTTP请求,浪费带宽和服务器资源;后者虽然减少了请求次数,但连接挂起期间仍占用线程,并且延迟不稳定。WebSocket在TCP之上建立了一条全双工通道,客户端与服务端完成一次握手后,双方可以随时向对方发送消息,特别适合聊天、协同编辑、行情推送等场景。Spring Boot整合WebSocket并不复杂,只需要引入starter依赖,并通过注解或配置类注册端点即可快速搭建服务。

Spring Boot如何整合Spring WebSocket实现实时通信?

一、依赖引入与基础配置

要在Spring Boot项目中使用WebSocket,第一步是在pom.xml中引入spring-boot-starter-websocket依赖。该starter会自动引入必要的WebSocket API和Spring WebSocket模块,无需额外手动添加Tomcat或Jetty的WebSocket支持,因为Spring Boot内嵌容器已经包含了相应实现。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

引入依赖后,如果使用基于注解的@ServerEndpoint方式,还需要向Spring容器注册一个ServerEndpointExporter。这个Bean负责扫描所有带有@ServerEndpoint注解的类,并把它们注册为WebSocket端点。如果遗漏了这一步,客户端连接时会出现404错误。配置类写法非常简单:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

需要注意的是,如果项目采用外置Servlet容器部署,并且容器本身已经支持WebSocket,例如Tomcat 8以上版本,那么在某些情况下可以不显式配置ServerEndpointExporter,但为了保持开发环境与生产环境的一致性,建议始终显式声明。另外,如果同时引入了Spring Security,还需要对WebSocket握手路径进行放行,否则连接可能会被安全过滤器拦截。

二、使用@ServerEndpoint实现原生WebSocket端点

原生WebSocket方式最直接,适合消息格式简单、不需要复杂路由的场景。创建一个带有@ServerEndpoint注解的类,并为其指定连接路径。该类同时需要被Spring管理,通常加上@Component注解。在类内部使用@OnOpen@OnMessage@OnClose@OnError四个注解分别处理连接建立、消息到达、连接关闭和异常事件。

下面是一个简单的聊天室端点实现。所有在线会话被保存在一个线程安全的ConcurrentHashMap中,消息到达后广播给所有客户端。注意ConcurrentHashMap<String, Session>中的泛型在源码中需要正确书写,在HTML源码展示时需要转义尖括号。

import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/ws/chat")
public class ChatEndpoint {

    private static final ConcurrentHashMap<String, Session> SESSIONS = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        SESSIONS.put(session.getId(), session);
        broadcast("用户 " + session.getId() + " 加入聊天室,当前在线:" + SESSIONS.size());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        broadcast("用户 " + session.getId() + " 说:" + message);
    }

    @OnClose
    public void onClose(Session session) {
        SESSIONS.remove(session.getId());
        broadcast("用户 " + session.getId() + " 离开,当前在线:" + SESSIONS.size());
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    private void broadcast(String message) {
        for (Session session : SESSIONS.values()) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这种实现方式非常直观,但存在一些限制。例如消息的发送与接收直接耦合在同一个类中,当业务变得复杂时,多个端点之间很难统一管理。此外,原生WebSocket没有消息路由能力,无法根据消息头或目的地址将消息分发给特定用户或群组。如果只是做简单的控制台日志推送或者极小型聊天应用,它完全够用;一旦需要订阅、点对点私聊、消息确认等能力,就应当考虑更上层的方案。

Session管理是原生方案中最容易出错的地方。多个客户端同时连接时,对SESSIONS的读写必须保证线程安全。虽然ConcurrentHashMap本身支持并发操作,但遍历时进行删除仍然可能产生弱一致性问题。上面的广播方法在遍历时没有修改集合,因此安全;但如果在遍历过程中尝试移除已断开的Session,建议使用迭代器并调用remove(),或者将失效会话先收集再统一清理。

三、使用STOMP协议实现消息代理与订阅发布

如果业务要求消息按主题订阅、支持点对点私聊或者需要与消息代理集成,更适合使用STOMP协议。STOMP是一种简单的面向文本的消息协议,可以在WebSocket之上提供类似消息队列的语义。Spring Boot通过spring-messagingspring-websocket模块提供了STOMP支持,配置后可以在Controller中使用@MessageMapping@SendTo注解处理消息。

首先需要开启STOMP消息代理。创建一个实现WebSocketMessageBrokerConfigurer接口的配置类,并标注@EnableWebSocketMessageBroker。在registerStompEndpoints方法中注册WebSocket端点,并允许跨域访问。在configureMessageBroker方法中启用一个基于内存的简单消息代理,并指定应用前缀和代理目标前缀。

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS();
    }

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

上面的配置中,所有客户端发送消息到以/app开头的目标地址,这些消息会先进入Spring的Controller中处理;而服务端推送给客户端的消息则来自以/topic/queue开头的代理地址。启用SockJS后,即使浏览器不支持原生WebSocket,也可以自动降级为HTTP流或长轮询,从而提高兼容性。

接着可以编写一个处理聊天消息的Controller。使用@MessageMapping映射客户端发送的目标,使用@SendTo将返回值广播到指定主题。这种方式将消息路由和业务逻辑分离,代码结构更清晰。

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

    @MessageMapping("/chat.send")
    @SendTo("/topic/public")
    public String sendMessage(String message) {
        return message;
    }
}

当客户端向/app/chat.send发送消息时,该方法被调用,返回的字符串会被推送到所有订阅了/topic/public的客户端。如果需要点对点私聊,可以使用@SendToUser注解,消息会发送到指定用户自己的队列中,默认队列前缀为/user。这种方式天然支持更复杂的消息路由,也更容易与RabbitMQ、ActiveMQ等外部消息代理对接。

四、前端连接与心跳保活

对于原生WebSocket端点,前端使用JavaScript的WebSocket对象即可连接。连接地址与后端注册的路径保持一致,例如ws://localhost:8080/ws/chat。生产环境如果使用HTTPS,则需要使用wss://协议。前端代码需要处理连接建立、消息接收、连接关闭等事件,并提供发送消息的函数。

const ws = new WebSocket("ws://localhost:8080/ws/chat");

ws.onopen = function() {
    console.log("连接已建立");
    heartBeat();
};

ws.onmessage = function(event) {
    console.log("收到消息:", event.data);
};

ws.onclose = function() {
    console.log("连接已关闭");
};

function sendMessage() {
    const input = document.getElementById("msg");
    ws.send(input.value);
    input.value = "";
}

function heartBeat() {
    setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
            ws.send("ping");
        }
    }, 30000);
}

WebSocket连接虽然保持长连接,但中间可能经过代理、负载均衡等设备,这些设备往往会在一定空闲时间后断开连接。心跳保活是生产环境必不可少的一环。客户端定时发送一个简单的ping消息,服务端可以原样返回pong或者直接忽略,目的是维持链路活跃。上面的heartBeat函数每30秒发送一次ping,实际间隔可以根据网络环境和业务需求调整。

如果使用STOMP和SockJS,前端需要引入SockJS和STOMP客户端库,连接地址改为http://localhost:8080/ws-stomp,并且使用STOMP的订阅和发送方法。STOMP客户端会处理底层WebSocket细节,应用代码只需调用subscribesend,开发体验更接近消息队列。通常前端还需要处理连接断开后的自动重连,避免用户手动刷新页面。

五、常见问题与优化建议

在实际部署中,WebSocket服务会遇到一些问题。首先是跨域限制。浏览器要求WebSocket握手也遵循同源策略,如果前端页面与后端服务不在同一域名或端口,需要在服务端配置允许跨域。原生@ServerEndpoint可以通过实现ServerEndpointConfig.Configurator并重写checkOrigin方法;STOMP方式则使用setAllowedOriginPatterns配置允许的来源模式,生产环境应尽量指定具体域名,而不是使用通配符*

其次是Session的线程安全与资源释放。WebSocket连接断开后,服务端要确保从容器中移除该Session,否则会累积无效连接,导致内存泄漏。对于原生方式,必须在@OnClose中清理。对于STOMP方式,Spring会管理Session生命周期,但自定义的Session存储仍然需要做好清理。此外,如果消息量大,建议使用线程池处理消息发送,避免阻塞WebSocket工作线程。

在集群或多实例部署时,内存中的Session和简单消息代理无法跨节点工作。此时需要使用Redis、RabbitMQ等外部组件作为消息代理,或者通过发布订阅机制同步Session状态。Spring提供了对RabbitMQ STOMP插件和外部Broker的支持,将enableSimpleBroker替换为enableStompBrokerRelay即可将消息路由交给外部代理,从而支持多实例横向扩展。对于消息大小超过默认限制的场景,可以通过WebSocketContainersetDefaultMaxTextMessageBufferSize调整缓冲区大小,但更推荐对大数据做分片或使用专用文件传输通道。

Spring BootSpring WebSocket实时通信修改时间:2026-09-19 13:38:06

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。