Spring Task 与 WebSocket 实战:苍穹外卖订单实时通知方案
在苍穹外卖项目的开发中,订单的定时处理和实时通知是提升用户体验的关键环节。为了实现来单提醒、催单以及订单状态更新等功能,我们需要引入 Spring Task 进行定时调度,并利用 WebSocket 实现服务端主动推送。
Spring Task 入门与实践
什么是 Spring Task
Spring Task 是 Spring 框架内置的轻量级定时任务调度框架。相比 Quartz,它配置更简单,无需依赖第三方库,非常适合单体应用中的常规定时任务场景。
核心优势包括零侵入性(基于注解)、轻量级以及对 Spring 生态的无缝集成(支持依赖注入和事务管理)。
快速上手
1. 开启调度支持
在启动类或配置类上添加 @EnableScheduling 注解即可激活定时任务功能:
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. 编写任务类
创建一个组件,使用 @Scheduled 注解标记需要定时执行的方法。支持三种调度模式:
- 固定延迟 (
fixedDelay):上次任务结束后等待指定时间再执行下一次。 - 固定频率 (
fixedRate):每隔指定时间执行一次,不等待上次任务完成。 - Cron 表达式 (
cron):灵活定义执行时间点。
@Component
public class ScheduledTasks {
private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class);
/**
* 固定延迟:上次执行完成后间隔 5 秒执行
*/
@Scheduled(fixedDelay = 5000)
public void taskWithFixedDelay() {
logger.info("FixedDelay Task - 当前时间:{}", System.currentTimeMillis());
}
/**
* Cron 表达式:每分钟的第 10 秒执行
*/
@Scheduled(cron = "10 * * * * ?")
public void taskWithCron() {
logger.info("Cron Task - 当前时间:{}", System.currentTimeMillis());
}
}
Cron 表达式详解
Cron 表达式格式为:秒 分 时 日 月 周 [年]。
| 位置 | 字段 | 取值范围 | 特殊字符 |
|---|---|---|---|
| 1 | 秒 | 0-59 | , - * / |
| 2 | 分 | 0-59 | , - * / |
| 3 | 时 | 0-23 | , - * / |
| 4 | 日 | 1-31 | , - * ? / L W |
| 5 | 月 | 1-12 | , - * / |
| 6 | 周 | 1-7 | , - * ? / L # |
常用示例:
// 每天凌晨 2 点执行
@Scheduled(cron = "0 0 2 * * ?")
// 每 10 分钟执行一次
@Scheduled(cron = "0 */10 * * * ?")
// 工作日上午 9 点执行
@Scheduled(cron = "0 0 9 * * MON-FRI")
线程池配置(重要)
默认情况下,Spring Task 使用单线程执行所有任务。如果某个任务耗时过长,会阻塞后续任务。生产环境建议配置异步线程池:
@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 设置线程池大小为 10
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
或者在 application.yml 中配置:
spring:
task:
scheduling:
pool:
size: 10
thread-name-prefix: my-scheduler-
最佳实践
- 避免阻塞:耗时操作应放入异步线程或 CompletableFuture 中执行。
- 异常处理:定时任务中的异常不会自动打印到控制台,务必捕获并记录日志。
- 动态配置:可通过配置文件动态修改 Cron 表达式,便于运维调整。
WebSocket 实时通信
HTTP 协议是单向的,客户端必须主动请求才能获取数据。对于订单提醒等场景,轮询效率低且延迟高。WebSocket 提供了全双工通信能力,允许服务器主动向客户端推送数据。
服务端实现
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 配置类
需要注册 ServerEndpointExporter 以自动扫描 @ServerEndpoint 注解的类:
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3. 核心业务逻辑
使用 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 + " 已断开");
sendToAll("用户 " + sid + " 离开了聊天室");
}
@OnError
public void onError(Session session, Throwable error) {
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);
}
}
}
前端对接
浏览器端通过 JavaScript 建立连接,监听 onmessage 事件接收推送:
let ws = null;
const userId = 'user_' + Math.random().toString(36).substr(2, 6);
function connect() {
const wsUrl = `ws://localhost:8080/ws/${userId}`;
ws = new WebSocket(wsUrl);
ws.onopen = function() {
console.log('WebSocket 连接成功');
};
ws.onmessage = function(event) {
console.log('收到消息:', event.data);
// 解析 JSON 并更新 UI
const data = JSON.parse(event.data);
handlePushMessage(data);
};
ws.onclose = function() {
console.log('连接已断开');
};
}
connect();
苍穹外卖场景整合
无支付场景适配
若项目未接入微信支付,下单时需直接跳过待付款状态,将订单状态设为'待接单':
order.setStatus(2); // 待接单
order.setPayStatus(1); // 已支付
这样后续的 WebSocket 推送逻辑可直接基于该状态流转。
来单提醒实现
当用户提交订单后,后端通过 WebSocket 向商家端推送新订单信息:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private WebSocketServer webSocketServer;
@Override
@Transactional
public void submitOrder(OrderSubmitDTO orderSubmitDTO) {
// 1. 保存订单
Orders order = new Orders();
// ... 设置属性 ...
orderMapper.insert(order);
// 2. 构建推送消息
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);
// 3. 发送给对应商家
// 实际场景中需根据订单中的 shopId 匹配对应的 sessionId
webSocketServer.sendToClient("shop_001", jsonMessage);
}
}
催单功能
用户点击催单按钮时,后端查询订单并推送提醒消息:
@Override
public void remindOrder(Long orderId) {
Orders order = orderMapper.getById(orderId);
if (order == null || order.getStatus() != Orders.TO_BE_CONFIRMED) {
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);
}
定时统计数据推送
利用 Spring Task 定期推送今日订单统计,帮助商家掌握经营情况:
@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("pendingOrders", orderMapper.getPendingOrderCount());
data.put("onlineCount", WebSocketServer.getOnlineCount());
String jsonMessage = JSON.toJSONString(data);
webSocketServer.sendToAll(jsonMessage);
}
}
通过以上组合,我们实现了从订单创建、状态变更到数据统计的全链路实时通知,显著提升了系统的响应速度和用户体验。


