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;
    }
}
相关推荐
独自破碎E1 天前
Spring Boot支持哪些嵌入Web容器?
前端·spring boot·后端
疯狂成瘾者1 天前
后端Spring Boot 核心知识点
java·spring boot·后端
xwz小王子1 天前
PNAS:神经形态机器人电子皮肤
网络·人工智能·机器人
wenxin-1 天前
NS3学习-Packet数据包结构
网络·学习·ns3·ns3内核
IT 行者1 天前
Spring Boot 4.x 安全监控新篇章:基于 ObservationFilterChainDecorator 的可观测性实践
java·spring boot·后端
国科安芯1 天前
商业卫星多轴步进驱动系统的抗辐照MCU集成方案
运维·网络·单片机·嵌入式硬件·安全·安全威胁分析·risc-v
pyniu1 天前
Spring Boot租房管理系统
java·spring boot·后端
ICT系统集成阿祥1 天前
哪些功能是对交换机的性能消耗比较大?
网络·网络协议