项目架构:使用的是若依的前后端分离架构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
- 请求头携带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;
}
}