跳到主要内容
苍穹外卖实战:Spring Task 定时任务与 WebSocket 实时通信 | 极客日志
Java 大前端 java
苍穹外卖实战:Spring Task 定时任务与 WebSocket 实时通信 针对苍穹外卖项目中订单定时处理与实时通知需求,采用 Spring Task 实现待支付订单清理等定时逻辑,结合 WebSocket 达成商家端来单提醒、用户催单及订单状态实时推送。方案包含 Spring Task 注解配置、线程池调优、Cron 表达式详解,以及 WebSocket 服务端与客户端全双工通信实践,助力构建高响应度即时通讯功能。
lzdxwyh 发布于 2026/3/26 更新于 2026/4/25 0 浏览在苍穹外卖项目的开发中,订单的定时处理与实时通知是提升用户体验的关键环节。今天我们重点讲解如何利用 Spring Task 实现定时任务,以及通过 WebSocket 达成商家端来单提醒、用户催单及订单状态实时推送。
Spring Task 介绍与入门
什么是 Spring Task
Spring Task 是 Spring 框架内置的轻量级定时任务调度框架,提供了简洁的声明式解决方案。开发者只需使用注解即可快速实现基于 cron 表达式、固定延迟或固定频率的任务调度。
核心特点
零侵入性 :基于注解,配置简单
轻量级 :无需依赖 Quartz 等第三方框架
支持多种调度方式 :cron、fixedDelay、fixedRate 等
与 Spring 生态无缝集成 :天然支持依赖注入、事务管理等
应用场景广泛,例如信用卡每月还款、火车票售票系统处理未支付订单、入职纪念日提醒等。
快速入门
1. 环境准备
确保项目中已引入 Spring 相关依赖(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
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 - 当前时间:{}"
核心注解与属性
@Scheduled 常用参数 参数 说明 示例 croncron 表达式,灵活指定执行时间 cron = "0 0 12 * * ?" 每天中午 12 点fixedDelay上次执行结束后延迟多久执行(毫秒) fixedDelay = 5000fixedRate固定频率执行(毫秒) fixedRate = 3000initialDelay首次执行延迟时间(毫秒) initialDelay = 10000
组合使用示例
@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void delayedTask () {
System.out.println("延迟启动的定时任务" );
}
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 * ?")
@Scheduled(cron = "0 0/30 9-17 * * MON-FRI")
配置线程池(重要) 默认情况下,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-
最佳实践
1. 避免长时间任务阻塞 @Scheduled(cron = "0/10 * * * * ?")
public void longRunningTask () {
CompletableFuture.runAsync(() -> {
});
}
2. 异常处理 @Scheduled(cron = "0/30 * * * * ?")
public void robustTask () {
try {
} catch (Exception e) {
logger.error("定时任务执行失败" , e);
}
}
3. 动态配置 cron 表达式 @Component
public class DynamicTask {
@Value("${task.cron:0 0 2 * * ?}")
private String cron;
@Scheduled(cron = "${task.cron:0 0 2 * * ?}")
public void dynamicTask () {
}
}
Spring Task vs Quartz 对比项 Spring Task Quartz 复杂度 简单,轻量级 复杂,功能强大 配置方式 注解 + 简单配置 XML/Java 配置 + 数据库存储 集群支持 ❌ 不支持集群 ✅ 支持集群(需数据库) 动态调度 部分支持(需自行实现) ✅ 原生支持 适用场景 单体应用、简单定时任务 分布式、复杂任务调度
Spring Task 是 Spring 生态中最便捷的定时任务解决方案,适合 80% 的常规定时任务场景。对于需要集群部署、任务持久化、动态管理 等高级特性的场景,建议考虑 Quartz 或 XXL-JOB 等专业调度框架。
启动类加 @EnableScheduling
任务类加 @Component
方法上加 @Scheduled 并配置时间规则(可以使用在线生成器)
记得配置线程池避免任务阻塞
WebSocket 简介 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它解决了 HTTP 协议单向通信的局限性。
为什么需要 WebSocket? **HTTP 的局限:**客户端必须主动发起请求才能获取数据,服务器无法主动推送数据给客户端,实时性差,需要使用轮询(频繁请求)才能实现'伪实时'。
**WebSocket 的优势:**全双工通信,服务器可以主动推送数据;一次连接,持久通信;低延迟,实时性好。适合即时通讯、消息通知、实时监控、协同编辑、多人实时游戏等场景。
基础概念
连接生命周期 客户端 服务器
|
|
|<
|
|<
|
|<
|
|
|<
关键事件
onOpen :连接建立时触发
onMessage :收到消息时触发
onClose :连接关闭时触发
onError :发生错误时触发
Java WebSocket 服务端实现
项目依赖(pom.xml)
<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 >
WebSocket 配置类 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 ();
}
}
WebSocket 服务端核心类 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 session = sessionMap.get(sid);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
System.out.println("【发送】给 " + sid + ":" + message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void sendToAll (String message) {
for (String sid : sessionMap.keySet()) {
sendMessage(sid, message);
}
}
public static int getOnlineCount () {
return sessionMap.size();
}
}
苍穹外卖集成示例
订单状态流转建议 在没有微信支付功能的场景下,下单流程可简化为直接设置待接单状态,跳过待付款环节。
下单时直接设置 :在 OrderServiceImpl 的 submitOrder 方法中,将订单状态设为 2(待接单),支付状态设为 1(已支付)。
数据库无需额外设置 :不需要修改表结构,订单状态按照正常流程流转即可:2(待接单) → 3(待配送) → 4(配送中) → 5(已完成)。
来单提醒功能
商家端连接 WebSocket 商家登录后自动连接 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);
showToast ('连接成功,等待新订单...' );
};
ws.onmessage = function (event ) {
const message = JSON .parse (event.data );
handlePushMessage (message);
};
ws.onclose = function ( ) {
console .log ('WebSocket 连接断开,5 秒后重连' );
setTimeout (connectWebSocket, 5000 );
};
ws.onerror = function (error ) {
console .error ('WebSocket 错误:' , error);
};
}
function handlePushMessage (message ) {
switch (message.type ) {
case 'NEW_ORDER' :
addNewOrder (message.data );
playNewOrderSound ();
showNotification ('有新订单啦!' );
break ;
case 'REMINDER' :
showReminder (message.data );
break ;
case 'ORDER_STATUS' :
updateOrderStatus (message.data );
break ;
default :
console .log ('未知消息类型:' , message.type );
}
}
function addNewOrder (order ) {
const ordersDiv = document .getElementById ('orderList' );
const orderCard = `
<div>
<div>订单号:${order.number} </div>
<div>金额:¥${order.amount} </div>
<div>时间:${order.createTime} </div>
<button onclick="acceptOrder(${order.id} )">接单</button>
<button onclick="rejectOrder(${order.id} )">拒单</button>
</div>
` ;
ordersDiv.insertAdjacentHTML ('afterbegin' , orderCard);
}
function playNewOrderSound ( ) {
const audio = new Audio ('/audio/new_order.mp3' );
audio.play ().catch (e => console .log ('播放失败:' , e));
}
document .addEventListener ('DOMContentLoaded' , function ( ) {
connectWebSocket ();
});
window .addEventListener ('beforeunload' , function ( ) {
if (ws) {
ws.close ();
}
});
用户端下单时触发 WebSocket 推送
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private WebSocketServer webSocketServer;
@Override
@Transactional
public void submitOrder (OrderSubmitDTO orderSubmitDTO) {
Orders order = new Orders ();
orderMapper.insert(order);
Map<String, Object> pushMessage = new HashMap <>();
pushMessage.put("type" , "NEW_ORDER" );
pushMessage.put("data" , order);
pushMessage.put("timestamp" , System.currentTimeMillis());
String jsonMessage = JSON.toJSONString(pushMessage);
webSocketServer.sendToClient("shop_001" , jsonMessage);
System.out.println("【来单提醒】已推送新订单:" + order.getNumber());
}
}
催单提醒功能
用户端发送催单消息
function remindOrder (orderId ) {
fetch ('/user/order/remind' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json'
},
body : JSON .stringify ({ orderId : orderId })
})
.then (response => response.json ())
.then (data => {
if (data.code === 1 ) {
alert ('催单成功,商家将尽快处理' );
}
});
}
后端处理催单并推送
@RestController
@RequestMapping("/user/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/remind")
@ApiOperation("用户催单")
public Result remind (@RequestBody RemindDTO remindDTO) {
orderService.remindOrder(remindDTO.getOrderId());
return Result.success();
}
}
@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 ("订单不存在" );
}
if (order.getStatus() != Orders.PENDING_PAYMENT && order.getStatus() != Orders.TO_BE_CONFIRMED) {
throw new BusinessException ("当前订单状态不支持催单" );
}
orderMapper.updateRemindTime(orderId);
Map<String, Object> remindMessage = new HashMap <>();
remindMessage.put("type" , "REMINDER" );
remindMessage.put("data" , Map.of(
"orderId" , order.getId(),
"orderNumber" , order.getNumber(),
"remindTime" , LocalDateTime.now()
));
String jsonMessage = JSON.toJSONString(remindMessage);
webSocketServer.sendToClient("shop_001" , jsonMessage);
log.info("用户催单:订单号 {},已通知商家" , order.getNumber());
}
}
定时推送订单统计数据
WebSocketTask 定时任务 完善 sendMessageToClient 方法,定期推送实时数据:
package com.sky.task;
import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
@Autowired
private OrderMapper orderMapper;
@Scheduled(cron = "0/5 * * * * ?")
public void pushRealTimeData () {
try {
Map<String, Object> data = new HashMap <>();
data.put("type" , "REAL_TIME_DATA" );
data.put("timestamp" , LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss" )));
Integer todayOrders = orderMapper.getTodayOrderCount();
data.put("todayOrders" , todayOrders);
Double todayAmount = orderMapper.getTodayTurnover();
data.put("todayAmount" , todayAmount);
Integer pendingOrders = orderMapper.getPendingOrderCount();
data.put("pendingOrders" , pendingOrders);
data.put("onlineCount" , WebSocketServer.getOnlineCount());
String jsonMessage = JSON.toJSONString(data);
webSocketServer.sendToAll(jsonMessage);
System.out.println("定时推送实时数据:" + jsonMessage);
} catch (Exception e) {
e.printStackTrace();
}
}
}
商家端接收实时数据
function handlePushMessage (message ) {
if (message.type === 'REAL_TIME_DATA' ) {
document .getElementById ('todayOrders' ).innerText = message.todayOrders ;
document .getElementById ('todayAmount' ).innerText = '¥' + message.todayAmount ;
document .getElementById ('pendingOrders' ).innerText = message.pendingOrders ;
document .getElementById ('onlineCount' ).innerText = message.onlineCount ;
document .getElementById ('updateTime' ).innerText = message.timestamp ;
}
}
相关免费在线工具 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