餐饮智能互联平台技术要点——定时处理(Spring Task)
简介
Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
作用:定时自动执行某段Java代码
应用场景
- 信用卡每月还款提醒
- 银行贷款每月还款提醒
- 火车票售票系统处理未支付订单
- 入职纪念日为用户发送通知
- ... (只要是需要定时处理的场景都可以使用Spring Task)
cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间。
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义。
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
e.g., 2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022
cron表达式在线生成器:https://cron.qqe2.com/
说明:一般日和周的值不同时设置,其中一个设置,另一个用?表示。
比如: 描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。
案例分析
使用步骤
- 导入maven坐标 spring-context
- 启动类添加注解 @EnableScheduling 开启任务调度
- 自定义定时任务类
需求分析
用户下单后可能存在的情况:
- 下单后未支付,订单一直处于“待支付”状态
- 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态
对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:
- 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
- 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”
代码开发
1). 自定义定时任务类OrderTask(待完善):
package com.sky.task;
/**
* 自定义定时任务,实现订单状态定时处理
*/
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 处理支付超时订单
*/
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
log.info("处理支付超时订单:{}", new Date());
}
/**
* 处理“派送中”状态的订单
*/
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("处理派送中订单:{}", new Date());
}
}
2). 在OrderMapper接口中扩展方法:
/**
* 根据状态和下单时间查询订单
* @param status
* @param orderTime
*/
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);
3). 完善定时任务类的processTimeoutOrder方法:
/**
* 处理支付超时订单
*/
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
log.info("处理支付超时订单:{}", new Date());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
// select * from orders where status = 1 and order_time < 当前时间-15分钟
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
if(ordersList != null && ordersList.size() > 0){
ordersList.forEach(order -> {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("支付超时,自动取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
});
}
}
4). 完善定时任务类的processDeliveryOrder方法:
/**
* 处理“派送中”状态的订单
*/
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("处理派送中订单:{}", new Date());
// select * from orders where status = 4 and order_time < 当前时间-1小时
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);
if(ordersList != null && ordersList.size() > 0){
ordersList.forEach(order -> {
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
});
}
}
底层原理
Spring通过 TaskExecutor 和 TaskScheduler 接口提供了一个抽象层,用于实现异步定时任务。这意味着 Spring 允许你选择使用其他定时任务框架,同时也提供了自身的定时任务实现,即 Spring Task。 Spring Task支持线程池,能够高效处理多种不同的定时任务需求。

TaskExecutor
TaskExecutor 是 Spring 框架中的一个抽象接口,它自然地与Java标准库中的 java.util.concurrent.Executor 产生了关联。然而,Spring 引入 TaskExecutor 的目的不是为了替代Java标准库中的Executor,而是为了增强其功能,特别是在支持定时任务的执行上。TaskExecutor源码如下:
public interface TaskExecutor extends Executor {
void execute(Runnable var1);
}
TaskScheduler
TaskScheduler是Spring Task的第二个抽象,用于提供定时任务的支持。
它需要传入一个 Runnable 任务作为参数,并指定任务的执行时间或触发器,以便周期性执行任务。
传入时间很好理解,有意思的是传入一个触发器(Trigger)的情况,因为这里需要使用cron表达式去触发一个定时任务。Spring 提供了一个 CronTrigger,通过传入一个 Runnable任务和 CronTrigger,就可以使用cron表达式去指定定时任务了。
scheduler.schedule(task, new CronTrigger("30 * * * * ?"));
TaskScheduler的抽象优点在于,它使需要执行定时任务的代码不必直接依赖于特定的定时任务框架(例如Timer或Quartz)。其中一种更简便的实现是ThreadPoolTaskScheduler,它实际上是对JDK中的SchedulingTaskExecutor的代理,并同时实现了TaskExecutor接口。因此,如果需要频繁执行定时任务的场景,可以考虑使用这个实现,而这也是Spring官方推荐的方式。
关键源代码
postProcessAfterInitialization

processScheduled
重点在第 4 步,processScheduled 封装了较多的主要处理逻辑。
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
// 1. 创建线程
Runnable runnable = createRunnable(bean, method);
...
// 2. 处理 initialDelay 与 initialDelayString 属性
long initialDelay = scheduled.initialDelay();
String initialDelayString = scheduled.initialDelayString();
...
// 3. 处理 cron 与 zone 属性
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
if (this.embeddedValueResolver != null) {
cron = this.embeddedValueResolver.resolveStringValue(cron);
zone = this.embeddedValueResolver.resolveStringValue(zone);
}
if (StringUtils.hasLength(cron)) {
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true;
// 4. 此处用到了注解的第一个属性 CRON_DISABLED
if (!Scheduled.CRON_DISABLED.equals(cron)) {
TimeZone timeZone;
if (StringUtils.hasText(zone)) {
timeZone = StringUtils.parseTimeZoneString(zone);
}
else {
timeZone = TimeZone.getDefault();
}
// 5. 处理重点
// cron 与 zone 构造 CronTrigger,再同方法开始构造好的线程一起构造 CronTask ( Task 的子类 )
// 再将构造好的 CronTask 存到到 ScheduledTaskRegistrar 的 CronTask 列表中
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}
}
...
// 6. 处理 fixedDelay ,与上述过程类似
long fixedDelay = scheduled.fixedDelay();
...
// 7. 处理 fixedRate,与上述过程类似
long fixedRate = scheduled.fixedRate();
...
// 8. 最后将 task 注册到 scheduledTasks 中
synchronized (this.scheduledTasks) {
Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
regTasks.addAll(tasks);
}
}
...
}
处理重点在第五步:
private final ScheduledTaskRegistrar registrar;
...
// 5. 处理重点
// cron 与 zone 构造 CronTrigger,再同方法开始构造好的线程一起构造 CronTask ( Task 的子类 )
// 再将构造好的 CronTask 存到到 ScheduledTaskRegistrar 的 CronTask 列表中
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
概括——项目启动时,在初始化 bean 后,带 @Scheduled 注解的方法会被拦截,然后依次:构建执行线程,解析参数,加入线程池。
注:默认的线程池用的是单线程。

浙公网安备 33010602011771号