本文主要介绍了在 Spring 框架中实现 WebSocket 服务的几种解决方案,并提供了 Spring WebSocket 最佳实践,以及需要注意的问题。
WebSocket 实现方案概述
在 Spring 项目中实现 WebSocket 服务一般有如下几种解决方案:
- Spring-WebSocket 模块:Spring 官方提供的原生支持,与 Spring 生态深度整合。
- Jakarta EE 规范 API:基于 Java EE 标准的 WebSocket 实现,适用于兼容 Jakarta EE 的容器。
- Netty 实现:基于高性能网络框架 Netty 自定义开发,灵活性高但开发成本较大
本文重点探讨前两种主流方案的实现与实践。
Jakarta EE WebSocket
启动 WebScoket 支持
通过配置 ServerEndpointExporter
扫描并注册所有带有 @ServerEndpoint
注解的端点:
java
@Configuration
public class WebSocketConfig {
/**
* ServerEndpointExporter类的作用是,会扫描所有的服务器端点,
* 把带有@ServerEndpoint 注解的所有类都添加进来
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer
WebSocketServer
类相当于 WS 协议的控制器,通过 @ServerEndpoint
和 @Component
注解启用,并实现生命周期方法:@OnOpen
,@OnClose
,@OnMessage
等方法。
- @OnOpen:当WebSocket建立连接时,会触发这个注解修饰的方法。
- @onClose: 当WebSocket关闭连接时,会触发这个注解修饰的方法。
- @onMessage: 当WebSocket接收到消息时,会触发这个注解修饰的方法。
java
/**
* 消息中心 websocket 连接
*/
@ServerEndpoint("/subscribe/{userName}")
@CrossOrigin
@Component
@Slf4j
public class MessageWsServer {
/**
* key: userName
* value: 连接的客户端
*/
@Getter
private static final Map<String, CopyOnWriteArraySet<Session>> clients = new ConcurrentHashMap<>();
/**
* 当前在线连接数统计。线程安全
*/
private static final AtomicInteger onlineCount = new AtomicInteger(0);
public static <T> void sendToAllClientByUserName(String userName, WsMessage<T> message) {
CopyOnWriteArraySet<Session> sessions = clients.get(userName);
if (sessions != null) {
final Iterator<Session> iterator = sessions.iterator();
while (iterator.hasNext()) {
Session session = iterator.next();
if (!session.isOpen()) {
iterator.remove();
log.warn("{} 的 session 关闭, 数据无法发送", userName);
continue;
}
sendMessage(session, JSON.toJSONString(message));
}
} else {
log.warn("{} 没有在线的客户端", userName);
}
}
public static int getOnlineCount() {
return onlineCount.get();
}
private static void sendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("WebSocket 数据发送异常:{}", e.getMessage());
}
}
@OnOpen
public void onOpen(Session session, @PathParam("userName") String param) {
Collection<Session> list = clients.computeIfAbsent(param, c -> new CopyOnWriteArraySet<>());
list.add(session);
incrementCount(param);
}
@OnMessage
public void onMessage(Session session, @PathParam("userName") String param, String message) {
log.info("WebSocket 收到 {} 客户端发来的消息: {}", param, message);
try {
session.getBasicRemote().sendText("ok");
} catch (Exception e) {
log.error(e.toString());
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket 连接发生未知错误", error);
}
@OnClose
public void onClose(Session session, @PathParam("userName") String param) {
Collection<Session> list = clients.get(param);
if (CollUtil.isNotEmpty(list) && (list.remove(session))) {
decrementCount(param);
}
}
private void incrementCount(String param) {
onlineCount.incrementAndGet();
log.info("{} 建立新的连接, 当前在线客户端总数: {}", param, getOnlineCount());
}
private void decrementCount(String param) {
onlineCount.decrementAndGet();
log.info("{} 连接断开, 当前在线客户端总数: {}", param, getOnlineCount());
}
}
会话管理 :使用 ConcurrentHashMap
和 CopyOnWriteArraySet
存储用户会话,保证多线程环境下的安全操作。
生命周期方法:
@OnOpen
:连接建立时将会话加入集合,并更新在线计数。@OnClose
:连接关闭时移除会话,并更新在线计数。@OnMessage
:接收消息后回复确认,并记录日志。
消息发送:支持向指定用户的所有在线客户端发送消息,自动过滤已关闭的会话
跨域问题
如果您想要使用@ServerEndpoint
来创建WebSocket服务端,并且允许来自不同源的客户端连接,您可能需要配合@CrossOrigin
使用,否则可能会遇到跨域问题。
高并发问题
在高并发下的问题,如果你同时向在线的 3 个 WebSocket 在线客户端发送消息,即广播所有在线用户(目前是 3 个),每个用户每秒10条,那就是说,你每秒要发送 30 条数据,我们调用上述的 sendText() 方法,有时候会出现
ini
java.lang.IllegalStateException: 远程 endpoint 处于 [xxxxxx] 状态,如:
The remote endpoint was in state [TEXT_FULL_WRITING] which is an invalid state for calle
这是因为在高并发的情况下,出现了 session 抢占的问题,导致 session 的状态不一致,所以这里需要加锁操作
Spring WebSocket(推荐用法)
在介绍 Spring WebSocket 中我会拿出已经实现的封装,目前来看还是够用的,所以配置代码会相对较多,而不是简单的配置
首先我们需要集成 Spring Websocket 的 starter 包
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置 WebSocketConfig
新增可配置属性类 WebSocketProperties
java
@Data
@ConfigurationProperties("allin.ws")
public class WebSocketProperties {
/**
* 发送时间的限制,默认3秒,单位:毫秒
*/
private Integer sendTimeLimit = 1000 * 3;
/**
* 发送消息缓冲上线,5MB
*/
private Integer bufferSizeLimit = 1024 * 1024 * 5;
/**
* 核心线程池数量,默认10个
*/
private Integer coreThreadSize = 10;
/**
* 最大线程池数量,默认50个
*/
private Integer maxThreadSize = 50;
/**
* 消息队列容量,默认100
*/
private Integer queueCapacity = 100;
}
新增配置类,实现 WebSocketConfigurer ,主要配置 websocket 的注册连接地址。
- 通过注入
List<CustomParamWebSocketHandler>
自动收集所有注册的 WebSocket 处理器 - 在
defaultWebSocketConfigurer
方法中遍历所有处理器,并根据各自的 urlPath 进行注册
java
/**
* 开启 websocket
*
*/
@Slf4j
@EnableConfigurationProperties(WebSocketProperties.class)
@EnableWebSocket
@Configuration
public class WebSocketAutoConfiguration implements InitializingBean {
private final WebSocketProperties webSocketProperties;
private final TokenApi tokenApi;
public WebSocketAutoConfiguration(WebSocketProperties webSocketProperties,
TokenApi tokenApi) {
this.webSocketProperties = webSocketProperties;
this.tokenApi = tokenApi;
}
@Bean
public WebSocketConfigurer defaultWebSocketConfigurer(List<CustomParamWebSocketHandler> customParamWebSocketHandlers) {
return registry -> {
for (CustomParamWebSocketHandler customParamWebSocketHandler : customParamWebSocketHandlers) {
registry.addHandler(customParamWebSocketHandler, customParamWebSocketHandler.getUrlPath())
.setAllowedOrigins("*");
log.info("注册 WebSocketHandler, 连接路径:{}, 路径模板:{}, 连接参数:{}",
customParamWebSocketHandler.getUrlPath(),
customParamWebSocketHandler.getUriTemplate(),
customParamWebSocketHandler.getParamKey());
}
};
}
@Primary
@Bean("defaultWebSocketHandler")
public CustomParamWebSocketHandler customParamWebSocketHandler() {
return new CustomParamWebSocketHandler(webSocketProperties);
}
@Override
public void afterPropertiesSet() {
// 初始化ws消息发送的线程池
WebSocketMessageSender.initializeExecutor(webSocketProperties);
}
}
封装通用的处理器 WebSocketHandler
WebSocketHandler 就是监听 websocket 连接之后的操作,也是上面继承的TextWebSocketHandle
,我们只要在原有的基础上进行业务处理就行了。
它提供了一些方法来处理 WebSocket 会话的各个阶段,使用只要继承 TextWebSocketHandler 类就行。
afterConnectionEstablished()
:当客户端建立连接时调用,用于执行连接建立后的操作。handleTextMessage()
:当接收到消息时调用,用于处理客户端发送的消息handleTransportError()
:当连接发生错误时调用,用于处理连接错误afterConnectionClosed()
:当连接关闭时调用,用于执行连接关闭后的操作。
在这里我们继承了TextWebSocketHandler
并实现了部分封装,添加了 urlPath 和 paramKey 属性,分别用于指定 WebSocket 的 URL 路径和参数名,这样就可以根据注释中的部分实现复用
java
/**
* 复用示例
*/
@Configuration
public class MyWebSocketConfig {
@Bean
public CustomParamWebSocketHandler userWebSocketHandler(WebSocketProperties properties) {
// 自定义URL路径、URI模板和参数名
return new CustomParamWebSocketHandler(
properties,
"/user/*", // URL路径模式
"/user/{userId}", // URI模板
"userId" // 参数名
);
}
@Bean
public CustomParamWebSocketHandler roomWebSocketHandler(WebSocketProperties properties) {
return new CustomParamWebSocketHandler(
properties,
"/room/*",
"/room/{roomId}",
"roomId"
);
}
}
/**
* 自定义参数发送信息的WebSocket
*/
@Slf4j
public class CustomParamWebSocketHandler extends TextWebSocketHandler implements ApplicationContextAware {
@Getter
private final WebSocketSessionManager sessionManager;
@Getter
private final WebSocketMessageSender sender;
private final WebSocketProperties properties;
@Getter
private final String uriTemplate;
@Getter
private final String urlPath;
/**
* 用于标识 WebSocket 连接的主键参数名
* 默认为"param",可以通过构造函数自定义
*/
@Getter
private final String paramKey;
/**
* Spring 事件发布器,用于发布 WebSocket 消息事件
*/
private ApplicationEventPublisher eventPublisher;
/**
* 构造函数
*
* @param properties WebSocket属性配置
*/
public CustomParamWebSocketHandler(WebSocketProperties properties) {
this(properties, "/websocket/*", "/websocket/{param}", "param");
}
/**
* 构造函数
*
* @param properties WebSocket属性配置
* @param urlPath URL路径模式,例如"/websocket/*"
* @param uriTemplate URI模板,例如"/websocket/{param}
* @param paramKey 用于标识 WebSocket 连接的主键参数名
*/
public CustomParamWebSocketHandler(WebSocketProperties properties, String urlPath, String uriTemplate, String paramKey) {
this.sessionManager = new WebSocketSessionManager(uriTemplate);
this.sender = new WebSocketMessageSender(sessionManager);
this.properties = properties;
this.uriTemplate = uriTemplate;
this.urlPath = urlPath;
this.paramKey = paramKey;
}
/**
* 获取路径变量映射
*
* @param session WebSocket会话
* @return 路径变量映射
*/
protected String getPathVariable(WebSocketSession session) {
final URI uri = session.getUri();
if (uri == null || uri.getPath() == null) {
log.error("获取 websocket url 失败");
return null;
}
UriTemplate template = new UriTemplate(uriTemplate);
Map<String, String> pathVariables = template.match(uri.getPath());
return pathVariables.getOrDefault(paramKey, "");
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String pathVariable = getPathVariable(session);
if (StrUtil.isNotBlank(pathVariable)) {
// 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
session = new ConcurrentWebSocketSessionDecorator(session,
properties.getSendTimeLimit(),
properties.getBufferSizeLimit());
sessionManager.add(pathVariable, session);
log.info("{}, {} 建立连接, 该Key连接总数: {}, 系统连接总数: {}", uriTemplate, pathVariable,
sessionManager.getSession(pathVariable).size(),
sessionManager.getAllSession().size());
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String pathVariable = getPathVariable(session);
if (StrUtil.isBlank(pathVariable)) {
return;
}
String messagePayload = message.getPayload();
if (!JSON.isValid(messagePayload)) {
// 非 JSON 格式,直接回复 ok
sender.sendToParam(pathVariable, "ok");
return;
}
// 解析为 JSON
JSONObject jsonMessage = JSON.parseObject(messagePayload);
// 检查是否包含 type 字段
if (jsonMessage != null && jsonMessage.containsKey("type")) {
String messageType = jsonMessage.getString("type");
// 异步发布事件
if (eventPublisher != null) {
var eventData =
new WebSocketMessageEvent.WebSocketMessageEventData(messageType, jsonMessage, session);
// 发布事件,避免阻塞 WebSocket 消息处理
eventPublisher.publishEvent(new WebSocketMessageEvent(eventData));
log.debug("WebSocket 消息事件已发布: paramKey={}, type={}", pathVariable, messageType);
}
} else {
// JSON 格式但没有 type 字段,回复 ok
sender.sendToParam(pathVariable, "ok");
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
if (!(exception instanceof EOFException)) {
log.error("WebSocket 连接发生错误", exception);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
String primaryKey = getPathVariable(session);
if (StrUtil.isNotBlank(primaryKey)) {
sessionManager.remove(primaryKey, session);
}
log.info("{}, {} 关闭连接, 该Key连接总数: {}, 系统连接总数: {}", uriTemplate, primaryKey,
sessionManager.getSession(primaryKey).size(),
sessionManager.getAllSession().size());
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
eventPublisher = applicationContext;
}
}
在这里我们使用ConcurrentWebSocketSessionDecorator
来处理线程安全问题,他是 Spring WebSocket 提供的一个装饰器类,用于增强底层的 WebSocketSession 的线程安全性。它通过并发安全的方式包装原始的 WebSocketSession 对象,确保在多线程环境下安全地访问和修改会话属性,以及进行消息发送操作。
构造方法
delegate
需要代理的sessionsendTimeLimit
表示发送单个消息的最大时间bufferSizeLimit
表示发送消息的队列最大字节数,不是消息的数量而是消息的总大小overflowStrategy
表示大小超过限制时的策略,默认是断开连接,还有个选项就是丢弃最老的数据,直到大小满足

会话管理
实现会话管理,用来实现服务端向指定的客户端单发或者群发消息
java
/**
* WebSocket Session管理实现,不同的CustomParamWebSocketHandler之间WebSocketSessionManager不复用
* <p>
* 每个key是一个分组,每个key下支持多个客户端
*
*/
@Slf4j
public class WebSocketSessionManager {
/**
* 全局管理器
*/
public static Map<String, Map<String, CopyOnWriteArraySet<WebSocketSession>>> webSocketSessionManagers = new HashMap<>();
/**
* key 与 WebSocketSession 映射
* value 为集合
*/
private final ConcurrentMap<String, CopyOnWriteArraySet<WebSocketSession>> sessions
= new ConcurrentHashMap<>();
public WebSocketSessionManager(String uriTemplate) {
webSocketSessionManagers.put(uriTemplate, this.sessions);
}
/**
* 添加 Session
*
* @param session Session
*/
public void add(String key, WebSocketSession session) {
// 使用compute方法来确保线程安全地添加会话
sessions.compute(key, (k, v) -> {
if (v == null) {
v = new CopyOnWriteArraySet<>();
}
v.add(session);
return v;
});
}
/**
* 移除 Session
*
* @param session Session
*/
public void remove(String key, WebSocketSession session) {
CopyOnWriteArraySet<WebSocketSession> webSocketSessions = sessions.get(key);
if (CollUtil.isNotEmpty(webSocketSessions)) {
webSocketSessions.removeIf(t -> t.getId().equals(session.getId()));
}
}
/**
* 移除 key 下的 所有 Session
*/
public void remove(String key) {
CopyOnWriteArraySet<WebSocketSession> sessionByKeys = sessions.get(key);
if (CollUtil.isNotEmpty(sessionByKeys)) {
synchronized (sessionByKeys) {
for (WebSocketSession session : sessionByKeys) {
try {
session.close();
} catch (IOException e) {
log.error("关闭 {} 的 ws 连接失败", key);
}
}
sessions.remove(key);
}
}
}
/**
* 获得指定 key 的 Session 列表
*
* @param key key
* @return Session
*/
public Collection<WebSocketSession> getSession(String key) {
if (StrUtil.isEmpty(key)) {
return Collections.emptyList();
}
return sessions.getOrDefault(key, new CopyOnWriteArraySet<>());
}
/**
* 获取所有session
*/
public Collection<WebSocketSession> getAllSession() {
return sessions.values().stream().flatMap(Collection::stream).toList();
}
/**
* 获取所有key
*/
public Set<String> getAllKeys() {
return sessions.keySet();
}
}
客户端消息广播
当接收到客户端消息时,我们可以约定一种规范,来将这个消息做为 Spring 事件广播出去,由事件监听者来处理后续动作,例如以下约束
非 JSON 消息或JSON 消息(不包含 type 字段)
- 输入:任何非 JSON 格式的文本消息或JSON 消息(不包含 type 字段)
- 处理:直接回复 "ok"
- 示例:
arduino
客户端发送: "hello"
服务端回复: "ok"
JSON 消息(包含 type 字段)
- 输入:有效 JSON 且包含 type 字段
- 处理:
-
- 构造 WsMessage 格式回复
- 异步发布 WebSocketMessageEvent 事件
- 示例:
css
客户端发送: {"type": "heartbeat"}
服务端回复: {
"type": "heartbeat",
"payload": "客户端需要的数据",
"sendTime": "2025-01-08 10:30:00"
}
封装的事件类如下
java
/**
* WebSocket 消息事件
* 当 WebSocket 接收到包含 type 字段的 JSON 消息时触发此事件
*
* @see #verify(String)
*/
public class WebSocketMessageEvent extends ApplicationEvent {
public WebSocketMessageEvent(WebSocketMessageEventData source) {
super(source);
}
/**
* 获取事件数据
*/
public WebSocketMessageEventData getData() {
return (WebSocketMessageEventData) getSource();
}
/**
* 校验是不是需要处理的类型
*/
public boolean verify(String type) {
if (type == null) {
return false;
}
return type.equals(getData().getType());
}
/**
* WebSocket 消息事件数据
*/
@Data
public static class WebSocketMessageEventData {
/**
* 消息类型
*/
private String type;
/**
* 解析后的 JSON 对象
*/
private JSONObject parsedMessage;
/**
* WebSocket 会话
*/
private WebSocketSession session;
public WebSocketMessageEventData(String type,
JSONObject parsedMessage,
WebSocketSession session) {
this.type = type;
this.parsedMessage = parsedMessage;
this.session = session;
}
/**
* 获取项目id
*/
public String getProjectId() {
return WsContextHolder.getProjectId(session);
}
/**
* 获取用户id
*/
public String getUserId() {
return WsContextHolder.getUserId(session);
}
}
}
监听器示例
java
@Component
public class EventInvasionWebSocketController {
private final String EventInvasionRedDotWsMessageType = "event_invasion_red_dot";
private final EventInvasionQueryService queryService;
public EventInvasionWebSocketController(EventInvasionQueryService queryService) {
this.queryService = queryService;
}
@Async
@EventListener
public void redDot(WebSocketMessageEvent messageEvent) {
// 校验是不是自己关注的消息类型
if (messageEvent.verify(EventInvasionRedDotWsMessageType)) {
final WebSocketMessageEvent.WebSocketMessageEventData messageEventData = messageEvent.getData();
final WebSocketSession session = messageEventData.getSession();
final String userId = messageEventData.getUserId();
final String projectId = messageEventData.getProjectId();
WebSocketMessageSender.sendToSession(session, WsMessage.of(EventInvasionRedDotWsMessageType,
queryService.unhandled(userId, projectId)));
}
}
}
消息发送
这里需要提醒一点,WebSocketMessageSender 消息发送类所有的消息都由该类实例化的对象发送,但所有的对象共用一个线程池,但是线程池的参数可以通过配置文件配置,所以你可以根据项目实际情况去修改这些参数。
java
/**
* 消息发送类
*
*/
@Slf4j
public class WebSocketMessageSender {
/**
* 共用一个线程池
*/
private static ThreadPoolTaskExecutor executor;
private final WebSocketSessionManager sessionManager;
public WebSocketMessageSender(WebSocketSessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public static synchronized void initializeExecutor(WebSocketProperties properties) {
if (executor != null) {
return;
}
executor = ThreadPoolUtils.createThreadPoolTaskExecutor(
"websocket-sender",
properties.getCoreThreadSize(),
properties.getMaxThreadSize(),
properties.getQueueCapacity()
);
log.info("初始化 WebSocketMessageSender 线程池成功");
}
/**
* 发送消息到某个连接
*
* @param session websocket连接
* @param message 发送的消息
*/
public static void sendToSession(WebSocketSession session, WsMessage<?> message) {
executor.execute(() -> {
// 1. 各种校验,保证 Session 可以被发送
if (session == null || !session.isOpen()) {
return;
}
// 2. 执行发送
try {
session.sendMessage(new TextMessage(JSON.toJSONString(message)));
} catch (IOException ex) {
log.error(StrUtil.format("给[{}]发送消息失败", session.getId()), ex);
} catch (SessionLimitExceededException ex) {
// 一旦有一条消息发送超时,或者发送数据大于限制,limitExceeded 标志位就会被设置成true,标志这这个 session 被关闭
// 后面的发送调用都是直接返回不处理,但只是被标记为关闭连接本身可能实际上并没有关闭,这是一个坑需要注意。
try {
session.close();
} catch (IOException e) {
log.error(StrUtil.format("主动关闭[{}]连接失败", session.getId()), e);
}
log.error(StrUtil.format("给[[{}]发送消息失败", session.getId()), ex);
}
});
}
/**
* 发送消息到某个参数的客户端
*
* @param param websocket连接时的参数
* @param message 发送的消息
*/
public void sendToParam(String param, String message) {
executor.execute(() -> {
// 1. 获得 Session 列表
Collection<WebSocketSession> sessions = sessionManager.getSession(param);
if (CollUtil.isEmpty(sessions)) {
return;
}
// 2. 执行发送
sessions.forEach(session -> {
// 1. 各种校验,保证 Session 可以被发送
if (session == null || !session.isOpen()) {
sessionManager.remove(param, session);
return;
}
// 2. 执行发送
try {
session.sendMessage(new TextMessage(message));
} catch (IOException ex) {
log.error(StrUtil.format("给[{}]分组的[{}]发送消息失败", param, session.getId()), ex);
} catch (SessionLimitExceededException ex) {
// 一旦有一条消息发送超时,或者发送数据大于限制,limitExceeded 标志位就会被设置成true,标志这这个 session 被关闭
// 后面的发送调用都是直接返回不处理,但只是被标记为关闭连接本身可能实际上并没有关闭,这是一个坑需要注意。
try {
session.close();
sessionManager.remove(param, session);
} catch (IOException e) {
log.error(StrUtil.format("主动关闭[{}]分组的[{}]连接失败", param, session.getId()), e);
}
log.error(StrUtil.format("给[{}]分组的[{}]发送消息失败", param, session.getId()), ex);
}
});
});
}
/**
* 发送消息到客户端
*
* @param param 分组
* @param message 发送的消息
*/
public void sendToParam(String param, WsMessage<?> message) {
sendToParam(param, JSON.toJSONString(message));
}
/**
* 广播消息到全部客户端
*
* @param message 发送的消息
*/
public void sendToAll(String message) {
for (String key : sessionManager.getAllKeys()) {
sendToParam(key, message);
}
}
/**
* 广播消息到全部客户端
*
* @param message 发送的消息
*/
public void sendToAll(WsMessage<?> message) {
for (String key : sessionManager.getAllKeys()) {
sendToParam(key, message);
}
}
}
注意 @EnableScheduling 的自动配置线程池失效场景
在 SpringBoot 2.x 中同时使用 @EnableWebSocket
和 @EnableScheduling
时,org.springframework.web.socket.config.annotation.WebSocketConfigurationSupport#defaultSockJsTaskScheduler
会导致@EnableScheduling
中自动配置线程池失效,所以需要手动创建线程池。
java
@Bean(name = "taskScheduler")
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("CommonScheduling-");
// 线程数
scheduler.setPoolSize(corePoolSize);
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return scheduler;
}
在 SpringBoot 3.x 中该问题已经被修复,参考: github.com/spring-proj...