跳到主要内容
苍穹外卖实战:SpringTask 定时任务与 WebSocket 实时通信 | 极客日志
Java 大前端 java
苍穹外卖实战:SpringTask 定时任务与 WebSocket 实时通信 SpringTask 提供轻量级定时任务方案,配合 WebSocket 实现全双工通信。文章详解配置流程、Cron 表达式及线程池调优,并结合苍穹外卖项目演示来单提醒、催单通知与实时数据推送的落地实践。重点涵盖服务端连接管理、前端 JS 对接以及无支付场景下的订单状态流转优化,确保系统具备实时响应能力。
邪神洛基 发布于 2026/3/29 更新于 2026/4/25 1 浏览苍穹外卖实战:SpringTask 定时任务与 WebSocket 实时通信
在苍穹外卖项目的开发中,订单的定时处理、来单提醒以及客户催单是提升用户体验的关键功能。为了实现这些需求,我们需要掌握 Spring Task 自动调度机制以及 WebSocket 双向通信协议。
Spring Task 概览
Spring Task 是 Spring 框架内置的轻量级定时任务调度器,它通过注解方式提供简洁的声明式解决方案。相比 Quartz 等第三方框架,它的优势在于零侵入性和与 Spring 生态的无缝集成。
核心特点
配置简单 :基于注解,无需复杂 XML 配置
轻量级 :适合单体应用中的常规定时任务
调度灵活 :支持 Cron 表达式、固定延迟(fixedDelay)和固定频率(fixedRate)
依赖注入 :天然支持 Spring 事务管理
快速上手
1. 环境准备
Spring Boot 项目通常已包含相关依赖,无需额外引入。
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter</artifactId >
</dependency >
2. 开启调度支持
在启动类或配置类上添加 @EnableScheduling 注解。
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main (String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. 编写任务类
使用 @Component 标记 Bean,并在方法上使用 @Scheduled 定义规则。
@Component
public class ScheduledTasks {
private LoggerFactory.getLogger(ScheduledTasks.class);
{
logger.info( , System.currentTimeMillis());
}
{
logger.info( , System.currentTimeMillis());
}
{
logger.info( , System.currentTimeMillis());
}
}
static
final
Logger
logger
=
@Scheduled(fixedDelay = 5000)
public
void
taskWithFixedDelay
()
"FixedDelay Task - 当前时间:{}"
@Scheduled(fixedRate = 3000)
public
void
taskWithFixedRate
()
"FixedRate Task - 当前时间:{}"
@Scheduled(cron = "10 * * * * ?")
public
void
taskWithCron
()
"Cron Task - 当前时间:{}"
Cron 表达式详解 Cron 表达式决定了任务的执行时机,格式为:秒 分 时 日 月 周 [年]。
位置 字段 取值范围 特殊字符 1 秒 0-59 , - * /2 分 0-59 , - * /3 时 0-23 , - * /4 日 1-31 , - * ? / L W5 月 1-12 或 JAN-DEC , - * /6 周 1-7 或 SUN-SAT , - * ? / L #7 年(可选) 空或 1970-2099 , - * /
@Scheduled(cron = "0 0 2 * * ?")
@Scheduled(cron = "0 */10 * * * ?")
@Scheduled(cron = "0 0 9 * * MON-FRI")
@Scheduled(cron = "59 59 23 L * ?")
线程池配置(重要) 默认情况下,Spring Task 使用单线程执行任务。如果某个任务耗时过长,会阻塞后续任务。生产环境中建议配置异步线程池。
@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks (ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10 ));
}
}
spring:
task:
scheduling:
pool:
size: 10
thread-name-prefix: my-scheduler-
最佳实践
避免阻塞 :耗时操作应放入异步线程中执行。
@Scheduled(cron = "0/10 * * * * ?")
public void longRunningTask () {
CompletableFuture.runAsync(() -> {
});
}
异常处理 :定时任务异常不会自动打印到控制台,需手动捕获记录日志。
动态配置 :可通过配置文件动态修改 Cron 表达式,实现运行时调整。
WebSocket 实时通信 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,解决了 HTTP 单向通信的局限性,非常适合即时通讯和实时推送场景。
为什么需要 WebSocket?
HTTP 局限 :客户端必须主动发起请求,服务器无法主动推送数据。轮询方式效率低且延迟高。
WebSocket 优势 :全双工通信,服务器可主动推送;一次握手建立持久连接;低延迟。
服务端实现
1. 依赖引入
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-websocket</artifactId >
</dependency >
<dependency >
<groupId > javax.websocket</groupId >
<artifactId > javax.websocket-api</artifactId >
<version > 1.1</version >
<scope > provided</scope >
</dependency >
2. 配置类 需要注册 ServerEndpointExporter 以自动扫描 @ServerEndpoint 注解。
package com.sky.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter () {
return new ServerEndpointExporter ();
}
}
3. 核心类 使用 ConcurrentHashMap 存储在线会话,保证线程安全。
package com.sky.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap <>();
private Session session;
private String sid;
@OnOpen
public void onOpen (Session session, @PathParam("sid") String sid) {
this .session = session;
this .sid = sid;
sessionMap.put(sid, session);
System.out.println("【连接】用户 " + sid + " 已连接,当前在线人数:" + sessionMap.size());
sendMessage(sid, "欢迎 " + sid + " 连接成功!" );
}
@OnMessage
public void onMessage (String message, @PathParam("sid") String sid) {
System.out.println("【消息】用户 " + sid + " 发送:" + message);
if ("ping" .equals(message)) {
sendMessage(sid, "pong" );
} else {
sendToAll(sid + " 说:" + message);
}
}
@OnClose
public void onClose (@PathParam("sid") String sid) {
sessionMap.remove(sid);
System.out.println("【关闭】用户 " + sid + " 已断开,当前在线人数:" + sessionMap.size());
sendToAll("用户 " + sid + " 离开了聊天室" );
}
@OnError
public void onError (Session session, Throwable error) {
System.out.println("【错误】" + error.getMessage());
error.printStackTrace();
}
public void sendMessage (String sid, String message) {
Session s = sessionMap.get(sid);
if (s != null && s.isOpen()) {
try {
s.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void sendToAll (String message) {
for (String key : sessionMap.keySet()) {
sendMessage(key, message);
}
}
public static int getOnlineCount () {
return sessionMap.size();
}
}
前端客户端实现 前端通过 JavaScript 的 WebSocket API 建立连接。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<title > WebSocket 聊天室</title >
<style >
body { font-family : 'Microsoft YaHei' , sans-serif; background : linear-gradient (135deg , #667eea 0% , #764ba2 100% ); height : 100vh ; display : flex; justify-content : center; align-items : center; margin : 0 ; }
.chat-container { width : 800px ; height : 600px ; background : white; border-radius : 10px ; box-shadow : 0 10px 30px rgba (0 ,0 ,0 ,0.3 ); display : flex; flex-direction : column; overflow : hidden; }
.chat-header { background : #4a90e2 ; color : white; padding : 15px 20px ; font-size : 18px ; font-weight : bold; }
.chat-messages { flex : 1 ; overflow-y : auto; padding : 20px ; background : #f5f5f5 ; }
.message { margin-bottom : 15px ; display : flex; align-items : flex-start; }
.message .system { justify-content : center; }
.message .system .content { background : #e0e0e0 ; color : #666 ; font-size : 12px ; padding : 5px 15px ; border-radius : 15px ; }
.message .other { justify-content : flex-start; }
.message .self { justify-content : flex-end; }
.message .content { max-width : 70% ; padding : 10px 15px ; border-radius : 10px ; word-wrap : break-word; }
.message .other .content { background : white; border : 1px solid #ddd ; }
.message .self .content { background : #4a90e2 ; color : white; }
.chat-input { padding : 20px ; background : white; border-top : 1px solid #ddd ; display : flex; gap : 10px ; }
.chat-input input { flex : 1 ; padding : 10px ; border : 1px solid #ddd ; border-radius : 5px ; font-size : 14px ; }
.chat-input button { padding : 10px 20px ; background : #4a90e2 ; color : white; border : none; border-radius : 5px ; cursor : pointer; font-size : 14px ; }
.status { padding : 10px 20px ; background : #f0f0f0 ; font-size : 12px ; color : #666 ; border-top : 1px solid #ddd ; }
</style >
</head >
<body >
<div class ="chat-container" >
<div class ="chat-header" > 🚀 WebSocket 聊天室</div >
<div class ="chat-messages" id ="messages" > </div >
<div class ="chat-input" >
<input type ="text" id ="messageInput" placeholder ="输入消息..." onkeypress ="handleKeyPress(event)" >
<button onclick ="sendMessage()" > 发送</button >
</div >
<div class ="status" id ="status" > 连接状态:未连接</div >
</div >
<script >
let ws = null ;
let userId = 'user_' + Math .random ().toString (36 ).substr (2 , 6 );
function addMessage (message, type, sender ) {
const messagesDiv = document .getElementById ('messages' );
const messageDiv = document .createElement ('div' );
messageDiv.className = `message ${type} ` ;
let html = '' ;
if (type === 'system' ) {
html = `<div class="content">📢 ${message} </div>` ;
} else {
html = `<div class="info">${sender || (type === 'self' ? '我' : userId)} </div><div class="content">${message} </div>` ;
}
messageDiv.innerHTML = html;
messagesDiv.appendChild (messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight ;
}
function connect ( ) {
const wsUrl = `ws://localhost:8080/ws/${userId} ` ;
ws = new WebSocket (wsUrl);
ws.onopen = function ( ) {
. ( );
. ( ). = ;
( , );
};
ws. = ( ) {
. ( , event. );
(event. , , );
};
ws. = ( ) {
. ( );
. ( ). = ;
( , );
};
ws. = ( ) {
. ( , error);
( , );
};
}
( ) {
input = . ( );
message = input. . ();
(!message) ;
(ws && ws. === . ) {
ws. (message);
(message, , );
input. = ;
} {
( , );
}
}
( ) {
(event. === ) ();
}
. = ( ) { (); };
. = ( ) { (ws) ws. (); };
</script >
</body >
</html >
苍穹外卖场景落地 在实际项目中,我们利用上述技术实现订单状态的实时同步。
1. 订单状态流转优化 在无微信支付场景中,下单后直接跳过待付款状态,进入待接单流程。
Service 层修改 :在 submitOrder 方法中,将订单状态设为 2(待接单),支付状态设为 1(已支付)。
数据库 :无需修改表结构,保持原有状态码定义即可。
2. 来单提醒 商家端登录后自动连接 WebSocket,当有新订单时,后端推送消息触发前端弹窗或提示音。
let ws = null ;
let shopId = 'shop_001' ;
function connectWebSocket ( ) {
ws = new WebSocket (`ws://localhost:8080/ws/${shopId} ` );
ws.onopen = function ( ) {
console .log ('WebSocket 连接成功,商家 ID:' + shopId);
};
ws.onmessage = function (event ) {
const message = JSON .parse (event.data );
if (message.type === 'NEW_ORDER' ) {
playNewOrderSound ();
showNotification ('有新订单啦!' );
}
};
ws.onclose = function ( ) {
setTimeout (connectWebSocket, 5000 );
};
}
3. 催单通知 用户点击催单按钮时,后端查询订单并立即通过 WebSocket 推送给对应商家。
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private WebSocketServer webSocketServer;
@Override
public void remindOrder (Long orderId) {
Orders order = orderMapper.getById(orderId);
if (order == null ) throw new BusinessException ("订单不存在" );
Map<String, Object> remindMessage = new HashMap <>();
remindMessage.put("type" , "REMINDER" );
remindMessage.put("data" , Map.of(
"orderId" , order.getId(),
"orderNumber" , order.getNumber()
));
String jsonMessage = JSON.toJSONString(remindMessage);
webSocketServer.sendToClient("shop_001" , jsonMessage);
}
}
4. 实时数据统计 通过定时任务每隔几秒获取今日订单数、营业额等数据,推送到所有在线商家端,实现数据大屏实时更新。
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
@Autowired
private OrderMapper orderMapper;
@Scheduled(cron = "0/5 * * * * ?")
public void pushRealTimeData () {
Map<String, Object> data = new HashMap <>();
data.put("type" , "REAL_TIME_DATA" );
data.put("todayOrders" , orderMapper.getTodayOrderCount());
data.put("todayAmount" , orderMapper.getTodayTurnover());
data.put("onlineCount" , WebSocketServer.getOnlineCount());
String jsonMessage = JSON.toJSONString(data);
webSocketServer.sendToAll(jsonMessage);
}
}
通过 Spring Task 与 WebSocket 的组合,我们可以构建出响应迅速、交互流畅的后端服务,显著提升系统的实时性和用户体验。
console
log
'WebSocket 连接成功'
document
getElementById
'status'
innerHTML
'连接状态:已连接 ✓'
addMessage
'已连接到服务器'
'system'
onmessage
function
event
console
log
'收到消息:'
data
addMessage
data
'other'
'服务器'
onclose
function
console
log
'WebSocket 连接关闭'
document
getElementById
'status'
innerHTML
'连接状态:已断开 ✗'
addMessage
'连接已断开'
'system'
onerror
function
error
console
error
'WebSocket 错误:'
addMessage
'连接出错'
'system'
function
sendMessage
const
document
getElementById
'messageInput'
const
value
trim
if
return
if
readyState
WebSocket
OPEN
send
addMessage
'self'
'我'
value
''
else
addMessage
'连接未建立,无法发送消息'
'system'
function
handleKeyPress
event
if
key
'Enter'
sendMessage
window
onload
function
connect
window
onbeforeunload
function
if
close
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online