Spring Task 定时任务实战
在订单处理场景中,我们经常需要执行一些后台任务,比如清理超时未支付订单、发送催单通知等。Spring Task 是 Spring 框架内置的轻量级调度工具,无需引入 Quartz 等第三方依赖,通过注解即可快速实现。
一、核心配置与入门
要在项目中启用定时任务支持,只需在启动类或配置类上添加 @EnableScheduling 注解。
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
接下来定义具体的任务逻辑。使用 @Scheduled 注解标记方法,并指定触发规则。常见的调度方式有三种:
- 固定频率 (
fixedRate):每隔 N 毫秒执行一次,不考虑上次任务是否完成。 - 固定延迟 (
fixedDelay):上次任务结束后,再等待 N 毫秒执行下一次。 - 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());
}
}
线程池配置(重要)
默认情况下,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-
Cron 表达式速查
Cron 表达式格式为:秒 分 时 日 月 周 [年]。
| 字段 | 取值范围 | 特殊字符 |
|---|---|---|
| 秒 | 0-59 | , - * / |
| 分 | 0-59 | , - * / |
| 时 | 0-23 | , - * / |
| 日 | 1-31 | , - * ? / L W |
| 月 | 1-12 | , - * / |
| 周 | 1-7 | , - * ? / L # |
常用示例:
- 每天凌晨 2 点:
0 0 2 * * ? - 每 10 分钟:
0 */10 * * * ? - 工作日 9 点到 17 点:
0 0 9-17 * * MON-FRI
注意:实际开发中建议配合在线生成器编写 Cron 表达式,避免手写出错。
二、WebSocket 实时通信
HTTP 协议是单向的,服务器无法主动推送数据给客户端。对于来单提醒、状态变更通知等场景,WebSocket 的全双工特性是最佳选择。
1. 服务端配置
首先引入 WebSocket 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
创建一个配置类,自动注册带有 @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();
}
}
2. 核心业务类
这里以聊天室为例,展示如何管理会话和发送消息。关键点是使用 ConcurrentHashMap 存储在线 Session,保证线程安全。
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 + " 已断开");
sendToAll("用户 " + sid + " 离开了聊天室");
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("【错误】" + error.getMessage());
}
/**
* 发送消息给指定用户
*/
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();
}
}
3. 前端对接
前端使用原生 WebSocket API 即可建立连接。以下是一个简单的 HTML 示例,展示了连接、发送和接收消息的流程。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket 测试</title>
</head>
<body>
<div id="status">连接状态:未连接</div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button onclick="sendMessage()">发送</button>
<div id="messages"></div>
<script>
let ws = null;
let 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() {
document.getElementById('status').innerHTML = '连接状态:已连接 ✓';
};
ws.onmessage = function(event) {
console.log('收到消息:', event.data);
addMessage(event.data, 'other');
};
ws.onclose = function() {
document.getElementById('status').innerHTML = '连接状态:已断开 ✗';
};
}
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(message);
addMessage(message, 'self');
input.value = '';
}
}
function addMessage(text, type) {
const div = document.createElement('div');
div.innerText = `[${type}] ${text}`;
document.getElementById('messages').appendChild(div);
}
window.onload = connect;
</script>
</body>
</html>
三、苍穹外卖项目集成方案
在实际的苍穹外卖项目中,我们可以利用上述技术实现商家端的实时通知功能。即使没有微信支付,WebSocket 依然能解决订单流转中的信息滞后问题。
1. 订单状态调整
由于部分版本移除了待付款环节,下单后直接设置为'待接单'状态,方便后续推送。
// OrderServiceImpl.java
order.setStatus(2); // 待接单
order.setPayStatus(1); // 已支付
2. 来单提醒推送
当用户提交订单成功后,后端立即通过 WebSocket 将订单详情推送到商家的连接通道。
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private WebSocketServer webSocketServer;
@Override
@Transactional
public void submitOrder(OrderSubmitDTO orderSubmitDTO) {
// ... 保存订单逻辑 ...
// 构建推送消息
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);
// 发送给特定商家 ID
webSocketServer.sendMessage("shop_001", jsonMessage);
}
}
3. 催单与统计推送
除了新订单,用户催单和定时统计数据更新也可以通过 WebSocket 实时触达。
催单逻辑:
@Override
public void remindOrder(Long orderId) {
Orders order = orderMapper.getById(orderId);
// ... 校验状态 ...
Map<String, Object> remindMessage = new HashMap<>();
remindMessage.put("type", "REMINDER");
remindMessage.put("data", order);
String jsonMessage = JSON.toJSONString(remindMessage);
webSocketServer.sendMessage("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("todayAmount", orderMapper.getTodayTurnover());
String jsonMessage = JSON.toJSONString(data);
webSocketServer.sendToAll(jsonMessage);
}
}
商家端接收到消息后,解析 JSON 并更新页面 DOM,即可实现类似'心跳'般的实时效果。这种方案比轮询更高效,且用户体验更流畅。


