一. SchedulingConfigurer解析
Spring 中,創建定時任務除了使用@Scheduled 註解外,還可以使用 SchedulingConfigurer。既然兩者都可以實現定時任務,那有什麼不同呢?
@Schedule註解的一個缺點就是其定時時間不能動態更改,它適用於具有固定任務週期的任務,若要修改任務執行週期,只能走“停服務→修改任務執行週期→重啓服務”這條路。而基於 SchedulingConfigurer 接口方式可以做到。SchedulingConfigurer 接口可以實現在@Configuration 類上,同時不要忘了,還需要@EnableScheduling 註解的支持。
package org.springframework.scheduling.annotation;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@FunctionalInterface
public interface SchedulingConfigurer {
void configureTasks(ScheduledTaskRegistrar var1);
}
ScheduledTaskRegistrar類包括以下幾個重要方法:
從方法的命名上可以猜到,方法包含定時任務,延時任務,基於 Cron 表達式的任務,以及 Trigger 觸發的任務。
二. 代碼示例
create table if not exists sys_schedule_lock(
task_id varchar(32) primary key not null comment '任務類型id',
task_desc varchar(32) default null comment '任務類型說明',
exec_ip varchar(32) default null comment '獲取鎖的機器ip',
acquire_time datetime DEFAULT NULL comment '獲取鎖的時間',
status int(11) comment '當前任務id鎖狀態: 0-空閒 1-佔用'
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='定時任務執行數據庫鎖';
INSERT INTO zzxypm.sys_schedule_lock (task_id, task_desc, exec_ip, acquire_time, status) VALUES ('TASK_TODO', '定時推送待辦任務', '127.0.0.1', null, 0);
@NoArgsConstructor
@Data
@TableName("sys_schedule_lock")
public class ScheduleLock {
@TableId(value = "task_id")
private String taskId;
private String taskDesc;
private Date acquireTime;
private String execIp;
private int status;
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zzxypm.entity.ScheduleLock;
import java.util.Map;
public interface ScheduleLockMapper extends BaseMapper<ScheduleLock> {
int lock(Map<String,Object> map);
int unlock(Map<String,Object> map);
int batchResetStatus();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzxypm.mapper.ScheduleLockMapper">
<update id="lock" parameterType="map">
update sys_schedule_lock
set exec_ip = #{execIp},
acquire_time = now(),
status = 1
where task_id = #{taskId}
and status = 0;
</update>
<update id="unlock" parameterType="map">
update sys_schedule_lock
set
acquire_time = now(),
status = 0
where task_id = #{taskId}
and status = 1
and exec_ip = #{execIp};
</update>
<update id="batchResetStatus">
update sys_schedule_lock
set status = 0
where status = 1
</update>
</mapper>
package com.zzxypm.schedule;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public enum TaskIdEnum {
TASK_TODO("TASK_TODO", "定時推送待辦任務");
TaskIdEnum(String taskId, String desc) {
this.taskId = taskId;
this.desc = desc;
}
/**
* 任務唯一id
*/
private String taskId;
/**
* 任務類型描述
*/
private String desc;
public String getTaskId() {
return taskId;
}
public String getDesc() {
return desc;
}
public static TaskIdEnum acquire(final String taskId) {
Optional<TaskIdEnum> serializeEnum =
Arrays.stream(TaskIdEnum.values())
.filter(v -> Objects.equals(v.getTaskId(), taskId))
.findFirst();
return serializeEnum.orElse(TaskIdEnum.TASK_TODO);
}
public static List<TaskIdEnum> getEnumList() {
return Arrays.asList(TaskIdEnum.values());
}
}
package com.zzxypm.schedule;
import com.zzxypm.common.util.DateUtil;
import com.zzxypm.common.util.IPV4Util;
import com.zzxypm.common.util.SpringContextHolder;
import com.zzxypm.mapper.ScheduleLockMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
/**
* 抽象任務骨架定義
*/
@Slf4j
public abstract class AbstractTask implements Runnable{
private TaskIdEnum taskIdEnum;
private String cron;
private ScheduleLockMapper lockMapper = SpringContextHolder.getBean(ScheduleLockMapper.class);
public AbstractTask(TaskIdEnum taskIdEnum,String cron) {
this.taskIdEnum = taskIdEnum;
this.cron = cron;
}
@Transactional
@Override
public void run() {
String taskId = taskIdEnum.getTaskId();
String ip = IPV4Util.getLocalIpv4Address();
Map<String,Object> map = new HashMap<>();
map.put("taskId",taskId);
map.put("execIp",ip);
int lock = lockMapper.lock(map);
log.info("lock: taskId=[{}],ip=[{}],lock=[{}]", taskId, ip, lock);
if (lock == 1) {
log.info("taskId:[{}],任務執行開始時間:[{}]", taskId, DateUtil.now());
doWork();
log.info("taskId:[{}],任務執行結束時間:[{}]", taskId, DateUtil.now());
int unlock = lockMapper.unlock(map);
log.info("unlock: taskId=[{}],ip=[{}],unlock=[{}]", taskId, ip, unlock);
}
}
protected abstract void doWork();
public TaskIdEnum getTaskIdEnum() {
return taskIdEnum;
}
public String getCron() {
return cron;
}
}
package com.zzxypm.schedule.task;
import com.zzxypm.schedule.AbstractTask;
import com.zzxypm.schedule.TaskIdEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 待辦推送任務
*/
@Slf4j
public class TodoPushTask extends AbstractTask {
private static TaskIdEnum taskIdEnum = TaskIdEnum.TASK_TODO;
// 每隔5秒執行一次
private static final String CRON = "*/5 * * * * ?";
public TodoPushTask() {
super(taskIdEnum,CRON);
}
@Override
protected void doWork() {
//定時從數據庫中掃表,封裝爲 定時任務對象,丟到 工廠裏 讓 執行器 執行
log.info("TodoPushTask,推送待辦任務");
}
}
package com.zzxypm.config;
import com.zzxypm.common.support.cls.DefaultClassScanner;
import com.zzxypm.schedule.AbstractTask;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.scheduling.support.CronTrigger;
import java.util.ArrayList;
import java.util.List;
/**
* 動態定時任務配置
* (配置數據庫動態執行)
**/
@Slf4j
@Configuration
public class DynamicScheduleConfig implements SchedulingConfigurer {
@Autowired
private DefaultClassScanner classScanner;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
try {
List<Class<?>> taskClassList = classScanner.getClassListBySuper("com.zzxypm.schedule", AbstractTask.class);
if (CollectionUtils.isNotEmpty(taskClassList)) {
List<TriggerTask> taskList = new ArrayList<>(taskClassList.size());
TriggerTask task = null;
for (Class<?> cls : taskClassList) {
AbstractTask abstractTask = (AbstractTask) cls.newInstance();
String cron = abstractTask.getCron();
task = new TriggerTask((AbstractTask) cls.newInstance(),
triggerContext -> new CronTrigger(cron).nextExecutionTime(triggerContext)
);
taskList.add(task);
}
taskRegistrar.setTriggerTasksList(taskList);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 默認的,SchedulingConfigurer 使用的也是單線程的方式,如果需要配置多線程,則需要指定 PoolSize
* @return
*/
@Bean("taskScheduler")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(20);
taskScheduler.setThreadNamePrefix("Scheduled-");
taskScheduler.setRejectedExecutionHandler((r, e) -> {
if (!e.isShutdown()) {
r.run();
}
// 記錄執行失敗的任務到數據庫表中
// 發送告警郵件給相關負責人
});
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.setAwaitTerminationSeconds(60);
taskScheduler.initialize();
return taskScheduler;
}
}
三. 動態配置定時規則
在不重啓服務的情況下,如何動態的修改定時任務的cron參數?
網上有兩種方法,都有缺點。
-
https://blog.csdn.net/xht555/article/details/53121962
此方法,是在觸發運行的時候,刷新定時規則,這種方法的缺點是,刷新規則的時間必須是在某次觸發運行的時候。 -
https://blog.csdn.net/jianggujin/article/details/77937316
此方法基於 SchedulingConfigurer 的源碼,捕獲 ScheduledTaskRegistrar 類的實例,通過該類中的 TaskScheduler 實例操作定時任務的增刪,而非採用 ScheduledTaskRegistrar.addTriggerTask 方法維護定時任務。所以需要自行寫代碼維護定時任務列表,控制任務的刪減,代碼的實現比較繁瑣。
如果想要實現可以動態修改的定時策略,建議使用開源組件 Quartz。