Spring Task
- 框架定位:Spring框架提供的任务调度工具
- 核心特性:
- 按照约定时间自动执行代码逻辑
- 区别于请求响应模式,无需外部触发
- 类比闹钟机制,到设定时间自动执行
cron表达式
- 本质:cron表达式本质上是一个字符串,用于定义任务触发的时间
- 作用:作为Spring Task任务调度框架的核心配置,通过表达式指定任务执行的时间点
- 域的数量:由6或7个域组成,用空格分隔
- 域的顺序:从左到右依次代表秒、分钟、小时、日、月、周、年(年可选)
- 特殊规则:日和周两个域通常互斥,只能指定其中一个,另一个用问号表示
- 示例解析:
- 2022年10月12日上午9点整对应的表达式:0 0 9 12 10 ? 2022
- 各域对应关系:秒(0) 分(0) 时(9) 日(12) 月(10) 周(?) 年(2022)
- 周使用问号的原因:当指定具体日期时,周信息应设为不指定
cron表达式在线生成器:在线Cron表达式生成器
使用步骤
1.导入maven坐标spring-context
- 框架特点: Spring Task是一个非常轻量级的框架,没有独立的jar包,其API集成在spring-context包中
- 依赖关系: 项目中已通过spring-boot-starter传递引入了spring-context依赖,无需额外导入
- 验证方式: 可在项目依赖树中查看org.springframework:spring-context:5.3.22是否存在
2.开启任务调度
- 注解使用: 在启动类上添加
@EnableScheduling注解即可开启任务调度功能 - 类比说明: 类似@EnableTransactionManagement开启事务、@EnableCaching开启缓存的使用方式
- 实现原理: 该注解会注册ScheduledAnnotationBeanPostProcessor处理定时任务
3.自定义定时任务类
- 组件要求: 类需添加@Component注解纳入Spring容器管理
- 方法规范:
- 方法需无返回值(void)
- 方法名可任意定义
- 业务逻辑代码编写在方法体内
- 定时配置: 使用@Scheduled(cron=“表达式”)注解指定触发时间
- 表达式格式: 秒 分 时 日 月 周(年可选)
- 示例: “0/5 * * * * ?”表示每5秒触发一次
案例
/**
* 自定义定时任务类
*/
@Component
@Slf4j
public class MyTask {
/**
* 定时任务,每5秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void myTask() {
log.info("定时任务开始执行:{}", LocalDateTime.now());
}
}订单状态定时处理
需求分析
下单后未支付
- 业务规则:用户需在15分钟内完成支付,超时订单应自动取消
- 处理方案:通过定时任务每分钟检查一次超时订单
- 执行频率:
- 每分钟检查一次(时效性要求高)
- 不建议每秒检查(系统压力过大)
- 判定标准:下单时间超过15分钟且状态仍为”待支付”
- 状态变更:自动将订单状态修改为”已取消”
用户收货后管理端未点击完成按钮
- 问题现象:订单长期处于”派送中”状态,与实际收货情况不符
- 处理方案:通过定时任务检查超时派送订单
- 执行频率:
- 每天凌晨1点检查一次(避免高峰期误判)
- 不建议白天检查(可能误判正常派送订单)
- 时间选择:选择店铺打烊后统一处理
- 状态变更:将符合条件的订单状态修改为”已完成”
代码开发
/**
* 处理超时订单
*/
@Scheduled(cron = "0 * * * * ?") // 每分钟执行一次
public void processTimeoutOrders() {
log.info("定时处理超时订单:{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().minusMinutes(15);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
if (ordersList != null && ordersList.size() > 0) {
for (Orders orders : ordersList) {
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
/**
* 处理处于派送中的订单
*/
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行一次
public void processDeliveryOrders() {
log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().minusMinutes(60);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
if (ordersList != null && ordersList.size() > 0) {
for (Orders orders : ordersList) {
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}
}@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);WebSocket
- 握手过程: 客户端首先发送握手请求,服务器应答后建立持久连接
- 连接特性: 建立的是长连接,连接建立后会一直保持
- 通信方向: 支持全双工通信,浏览器和服务器可以同时双向传输数据
- 应用类比: 类似电话通信,连接建立后双方可随时主动说话
- 底层基础: 同样基于TCP协议实现数据传输
导入maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>案例
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
在com/sky/config/WebSocketConfiguration.java下
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}com/sky/websocket/WebSocketServer.java
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}com/sky/task/WebSocketTask.java
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}来电提醒
需求分析与设计
- 业务场景:当用户下单并支付成功后,系统需立即通知外卖商家进行备货
- 通知形式:
- 语音播报(示例:“您有新的订单,请注意查收”)
- 弹出提示框(显示订单详情)
- 实现目标:商家端实时接收订单通知,包含套餐名称、订单号等关键信息
- 技术方案:采用WebSocket实现服务端到客户端的长连接推送
- 实现流程:
- 建立连接:商家管理端页面与服务端建立WebSocket长连接
- 消息推送:支付成功后,服务端调用WebSocket API推送订单消息
- 消息解析:客户端解析消息类型(1=来单提醒,2=客户催单)
- 消息展示:根据消息类型执行语音播报和弹窗提示
- 数据格式:
- 关键字段说明:
- type:数字标识(1为来单提醒,2为催单)
- orderId:关联的具体订单编号
- content:提示框显示的完整文本内容
代码开发
在sky-server/src/main/java/com/sky/service/impl/OrderServiceImpl.java中
public void paySuccess(String outTradeNo) {
// 根据订单号查询订单
Orders ordersDB = orderMapper.getByNumber(outTradeNo);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
// 通过websocket 向客户端推送消息
Map map = new HashMap();
map.put("type", 1); // 1表示来电提醒 2表示客户催单
map.put("orderId", ordersDB.getId());
map.put("content", "订单号:" + outTradeNo);
String jsonString = JSONObject.toJSONString(map);
webSocketServer.sendToAllClient(jsonString);
}客户催单
需求分析与设计
- 触发条件:用户在小程序支付成功后且订单处于待接单状态时,可点击催单按钮
- 通知形式:商家端会收到两种形式的通知
- 语音播报提示”有用户催单了”
- 弹出提示框显示催单信息
- 业务场景:主要用于客户催促商家尽快接单的场景
- 实现流程:
- 通过WebSocket建立管理端页面与服务端的长连接
- 用户点击催单按钮后调用WebSocket API向服务端推送消息
- 客户端解析消息类型并做出相应提示
- 数据格式:
- 采用JSON格式包含三个字段:
- type:消息类型(1为来单提醒,2为客户催单)
- orderId:订单ID
- content:消息内容
- 采用JSON格式包含三个字段:
- 与来单提醒的区别:
- 消息类型值不同(催单为2)
- 触发方式不同(催单由客户主动触发)
public void reminder(Long id) {
Orders orderDB = orderMapper.getById(id);
if (orderDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
Map map = new HashMap();
map.put("type", 2);
map.put("orderId", orderDB.getId());
map.put("content", "订单号:" + orderDB.getNumber());
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}