springboot3.5.8集成websocket问题

项目架构:使用的是若依的前后端分离架构3.9.0(springboot版本3.5.8),做了一些调整,比如添加多数据源,运行模块划分等等改动。

问题一: 启动项目报Error creating bean with name 'serverEndpointExporter' defined in class path resource [com/xxx/xxx/config/WebSocketConfig.class]: jakarta.websocket.server.ServerContainer not available
解决方案:

参考:https://developer.aliyun.com/article/1430614

依赖包冲突导致,使用maven Helper或者idea自带的maven解析工具,搜索websocket,把冲突的排除掉即可,我的问题是多数据源包导致

java 复制代码
<!-- 动态数据源 -->
 <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
    <!-- 需排除掉,不然websocket启动无法创建ServerEndpointExporter bean -->
    <exclusions>
       <exclusion>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-undertow</artifactId>
       </exclusion>
    </exclusions>
</dependency>

问题二: idea http client或者postman连接websocket返回Unexpected server response: 200
解决方案:

WebSocket连接尝试访问/websocket/xxx/xxx路径,但由于它不在Spring Security的允许匿名访问列表中,因此被安全配置拦截,导致返回HTTP 200而不是正确的WebSocket握手响应。

在SecurityConfig.calss中添加.requestMatchers("/websocket/**").permitAll()。

:1. 若依架构还有@Anonymous注解,但PermitAllUrlProperties类只扫描Spring MVC控制器中的@Anonymous注解,无法识别WebSocket端点上的注解@ServerEndpoint

  1. 请求头携带token后不需要添加(后端登录可以关闭验证码,sys_config表中sys.account.captchaEnabled改成false,只用用户名和密码登录)
java 复制代码
// 注解标记允许匿名访问的url
            .authorizeHttpRequests((requests) -> {
                permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll());
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
                    // 静态资源,可匿名访问
                    .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
                        // 如果携带token则可以删掉
                    .requestMatchers("/websocket/**").permitAll()
                    .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated();
            })

核心代码:

java 复制代码
        <!-- SpringBoot Websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

         <!-- 动态数据源 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
            <!-- 需排除掉,不然websocket启动无法创建ServerEndpointExporter bean -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-undertow</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * websocket 配置
 * 
 * @author 21541
 */
@Configuration
public class WebSocketConfig
{
    @Bean
    public ServerEndpointExporter serverEndpointExporter()
    {
        return new ServerEndpointExporter();
    }
}
java 复制代码
import com.alibaba.fastjson2.JSON;
import com.mengtaigroup.cellcontrol.domain.dto.CellRealtimeCurveDto;
import com.mengtaigroup.common.annotation.Anonymous;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 实时数据WebSocket端点
 * 用于向前端推送xxx数据
 * 
 * @date 2026-01-05
 */
@Slf4j
@Component
@ServerEndpoint("/websocket/xxxx/{xxx}")
public class CellRealtimeWebSocket {

    /**
     * 静态变量,用来记录当前在线连接数
     */
    private static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0);

    /**
     * concurrent包的线程安全Map,用来存放每个cellId对应的所有WebSocket连接
     * key为cellId,value为该cellId的所有WebSocket连接集合
     */
    private static final ConcurrentHashMap<Long, Set<CellRealtimeWebSocket>> WEB_SOCKET_MAP = new ConcurrentHashMap<>();

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 标识ID
     */
    private Long cellId;

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("cellId") Long cellId) {
        this.session = session;
        this.cellId = cellId;
        // 将当前WebSocket实例添加到对应cellId的集合中
        WEB_SOCKET_MAP.computeIfAbsent(cellId, k -> new CopyOnWriteArraySet<>()).add(this);
        // 在线数加1
        ONLINE_COUNT.incrementAndGet();
        log.info("WebSocket连接建立,ID: {},当前在线人数: {}", cellId, ONLINE_COUNT.get());
        try {
            sendMessage("连接成功");
        } catch (IOException e) {
            log.error("WebSocket IO异常", e);
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        // 从对应cellId的集合中移除当前WebSocket实例
        Set<CellRealtimeWebSocket> webSockets = WEB_SOCKET_MAP.get(this.cellId);
        if (webSockets != null) {
            webSockets.remove(this);
            // 如果该cellId没有其他连接了,则从map中移除该key
            if (webSockets.isEmpty()) {
                WEB_SOCKET_MAP.remove(this.cellId);
            }
        }
        // 在线数减1
        ONLINE_COUNT.decrementAndGet();
        log.info("WebSocket连接关闭,ID: {},当前在线人数: {}", this.cellId, ONLINE_COUNT.get());
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("来自{}的客户端消息: {}", this.cellId, message);
        // 群发消息
        for (Set<CellRealtimeWebSocket> webSocketSet : WEB_SOCKET_MAP.values()) {
            for (CellRealtimeWebSocket item : webSocketSet) {
                try {
                    item.sendMessage(message);
                } catch (IOException e) {
                    log.error("WebSocket发送消息异常", e);
                }
            }
        }
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket发生错误", error);
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 实现服务器主动推送曲线数据
     */
    public void sendCellRealtimeCurveData(CellRealtimeCurveDto curveDto) throws IOException {
        String message = JSON.toJSONString(curveDto);
        this.sendMessage(message);
    }

    /**
     * 发送曲线数据给指定ID的所有客户端
     */
    public static void sendInfo(CellRealtimeCurveDto curveDto, Long cellId) throws IOException {
        Set<CellRealtimeWebSocket> webSockets = WEB_SOCKET_MAP.get(cellId);
        if (webSockets != null && !webSockets.isEmpty()) {
            for (CellRealtimeWebSocket webSocket : webSockets) {
                try {
                    webSocket.sendCellRealtimeCurveData(curveDto);
                } catch (IOException e) {
                    log.error("向{}的WebSocket连接推送曲线数据时发生异常", cellId, e);
                }
            }
        }
    }

    /**
     * 获取当前在线连接数
     */
    public static Integer getOnlineCount() {
        return ONLINE_COUNT.get();
    }

    /**
     * 获取WebSocket连接数Map
     */
    public static ConcurrentHashMap<Long, Set<CellRealtimeWebSocket>> getWebSocketMap() {
        return WEB_SOCKET_MAP;
    }
}
相关推荐
小北方城市网1 天前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
步步为营DotNet1 天前
深度剖析.NET中IHostedService:后台服务管理的关键组件
服务器·网络·.net
毕设源码-钟学长1 天前
【开题答辩全过程】以 基于SpringBoot的智能书城推荐系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Ares-Wang1 天前
网络》》路由引入 、路由控制 》》路由策略 route-policy 、Filter-Policy(过滤策略)
网络·智能路由器
Jia ming1 天前
虚拟地址与物理地址:64位VS48位
网络
DARLING Zero two♡1 天前
【计算机网络】简学深悟启示录:http
网络协议·计算机网络·http
的卢马飞快1 天前
【C语言进阶】给数据一个“家”:从零开始掌握文件操作
c语言·网络·数据库
Yu_Lijing1 天前
《图解HTTP》笔记与读后感(上)
网络·笔记·网络协议·http
北京耐用通信1 天前
耐达讯自动化 Profibus 光纤链路模块:破解变频器通信难题,助力物流自动化升级
人工智能·物联网·网络协议·自动化·信息与通信
czy87874751 天前
connect() 的阻塞特性取决于它所关联的 socket 是否被设置为非阻塞模式,connect() 会等待 TCP 三次握手的超时时间
服务器·网络·tcp/ip