一、pom依赖版本:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!--添加以下排除方法-->
            <exclusions>
                <!-- 排除所有 Tomcat 相关 -->
                <exclusion>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-jetty</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--websocket服务-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!-- 确保使用正确的 WebSocket 依赖 -->
        <dependency>
            <groupId>jakarta.websocket</groupId>
            <artifactId>jakarta.websocket-api</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- Undertow 容器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

二、websocket配置类

@Configuration
@EnableWebSocket
public class WebSocketWarningConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler(), "/websocket")
                .setAllowedOrigins("*"); // 允许跨域访问
    }

    @Bean
    public WebSocketHandler webSocketHandler() {
        // 使用自定义的WebSocket处理器
        return new CustomWebSocketHandler();
    }

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

}

三、WebSocketHandler

@Slf4j
@Component
public class CustomWebSocketHandler extends TextWebSocketHandler {

    // 用于存储WebSocket会话
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();


    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String sessionId = session.getId();
        sessions.put(sessionId, session);
        log.info("WebSocket连接建立成功:{}", sessionId);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("收到消息:{}", payload);
        if (payload.equals("ping")) {
            session.sendMessage(new TextMessage("pong"));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String sessionId = session.getId();
        sessions.remove(sessionId);
        log.info("WebSocket连接关闭:{}", sessionId);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.error("WebSocket传输错误", exception);
    }

    @Scheduled(fixedRate = 30000) // 每30秒发送一次心跳,需要启动类或配置类上添加@EnableScheduling
    public void sendHeartbeat() {
        String heartbeat = "heartbeat";
        sessions.entrySet().removeIf(entry -> {
            WebSocketSession session = entry.getValue();
            try {
                session.sendMessage(new TextMessage(heartbeat));
                return false; // 会话仍然有效,不删除
            } catch (IOException e) {
                log.error("发送心跳消息失败,移除会话: {}", entry.getKey(), e);
                try {
                    session.close(); // 尝试关闭会话
                } catch (IOException closeException) {
                    log.error("关闭会话失败: {}", entry.getKey(), closeException);
                }
                return true; // 标记会话为无效,需要删除
            }
        });
    }

    public void sendMessageWithRetry(WebSocketSession session, String message, int maxRetries) {
        int retryCount = 0;
        while (retryCount < maxRetries) {
            try {
                session.sendMessage(new TextMessage(message));
                return;
            } catch (IOException e) {
                retryCount++;
                log.error("消息发送失败,尝试重试 {}/{}", retryCount, maxRetries);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
        log.error("消息发送失败,达到最大重试次数");
    }

四、启动报错

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverEndpointExporter' defined in class path resource [com/surfilter/fuxi/data/config/WebSocketWarningConfig.class]: jakarta.websocket.server.ServerContainer not available

五、问题排查

排除掉了jetty和tomcat容器,项目依旧使用jetty启动,未使用undertow启动,serverContainer获取null值报错

protected void initServletContext(ServletContext servletContext) {
    if (this.serverContainer == null) {
        this.serverContainer = (ServerContainer)servletContext.getAttribute("jakarta.websocket.server.ServerContainer");
    }
}

Jetty 使用不同的属性名存储 ServerContainer,进而无法获取,导致ServerEnpointExporter实例化失败。采用undertow处理

六、问题处理

6.1 添加依赖

<dependency>
    <groupId>io.undertow</groupId>
    <artifactId>undertow-websockets-jsr</artifactId>
    <version>2.3.10.Final</version> <!-- Spring Boot 3.0.6 对应的版本 -->
</dependency>

6.2 添加undertow配置类

@Configuration
public class UndertowWebSocketConfig {

    @Bean
    public UndertowServletWebServerFactory undertowWebSocketFactory() {
        UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();

        factory.addDeploymentInfoCustomizers(deploymentInfo -> {
            // 配置 WebSocket
            WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
            // 添加到部署信息
            deploymentInfo.addServletContextAttribute(
                    WebSocketDeploymentInfo.ATTRIBUTE_NAME,
                    webSocketDeploymentInfo
            );
            // 设置 Jakarta WebSocket 容器属性
            deploymentInfo.addServletContextAttribute(
                    "jakarta.websocket.server.ServerContainer",
                    webSocketDeploymentInfo
            );
        });
        return factory;
    }
}

服务器工厂配置:创建 UndertowServletWebServerFactory Bean 来定制 Undertow 服务器
WebSocket 支持:通过 WebSocketDeploymentInfo 配置 WebSocket 部署信息
容器属性设置:设置 Jakarta WebSocket 容器属性,确保 WebSocket 能够正常工作(可不配置)

七、最终效果

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐