CREATE TABLE IF NOT EXISTS `schedule_setting` (
`job_id` int NOT NULL AUTO_INCREMENT COMMENT '任務ID',
`bean_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'bean名稱',
`method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '方法名稱',
`method_params` varchar(8192) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '方法參數',
`cron_expression` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'cron表達式',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '備註',
`job_status` tinyint(1) DEFAULT NULL COMMENT '狀態(1爲啓用,0爲停用)',
`create_time` datetime DEFAULT NULL COMMENT '創建時間',
`update_time` datetime DEFAULT NULL COMMENT '修改時間',
PRIMARY KEY (`job_id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
2、定時任務實體類及對應Mapper
關於定時任務實體類一些基本的增刪改查接口代碼,這裏爲了簡便操作,引入了mybatis-plus中的ActiveRecord 模式,通過實體類繼承Model類實現,關於Model類的說明,參看如下文檔:https://baomidou.com/pages/49cc81/#activerecord-%E6%A8%A1%E5%BC%8F
2.1、定時任務實體類
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 定時任務實體類
*
* @author 星空流年
* @date 2023/7/5
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ScheduleSetting extends Model<ScheduleSetting> {
/**
* 任務ID
*/
@TableId(type = IdType.AUTO)
private Integer jobId;
/**
* bean名稱
*/
private String beanName;
/**
* 方法名稱
*/
private String methodName;
/**
* 方法參數
*/
private String methodParams;
/**
* cron表達式
*/
private String cronExpression;
/**
* 狀態(1爲啓用, 0爲停用)
*/
private Integer jobStatus;
/**
* 備註
*/
private String remark;
/**
* 創建時間
*/
private Date createTime;
/**
* 更新時間
*/
private Date updateTime;
}
/**
* 定時任務啓用、停用枚舉類
*
* @author 星空流年
* @date 2023/7/5
*/
public enum ScheduleJobEnum {
/**
* 啓用
*/
ENABLED(1),
/**
* 停用
*/
DISABLED(0);
private final int statusCode;
ScheduleJobEnum(int code) {
this.statusCode = code;
}
public int getStatusCode() {
return statusCode;
}
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xxx.entity.ScheduleSetting;
import org.apache.ibatis.annotations.Mapper;
/**
* ScheduleSetting表數據庫訪問層
*
* @author 星空流年
* @date 2023/7/5
*/
@Mapper
@SuppressWarnings("all")
public interface ScheduleSettingMapper extends BaseMapper<ScheduleSetting> {
}
3、定時任務線程池相關類
3.1、定時任務線程池配置類
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 執行定時任務的線程池配置類
*
* @author 星空流年
* @date 2023/7/5
*/
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
// 獲取系統處理器個數, 作爲線程池數量
int corePoolSize = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 定時任務執行線程池核心線程數
taskScheduler.setPoolSize(corePoolSize);
taskScheduler.setRemoveOnCancelPolicy(true);
taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
return taskScheduler;
}
}
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 獲取Spring中Bean工具類
*
* @author 星空流年
* @date 2023/7/5
*/
@Component
@SuppressWarnings("all")
public class SpringContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
public static Class<? extends Object> getType(String name) {
return applicationContext.getType(name);
}
}
import lombok.extern.slf4j.Slf4j;
import xxx.util.SpringContextUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* Runnable接口實現類
* 被定時任務線程池調用, 用來執行指定bean裏面的方法
*
* @author 星空流年
* @date 2023/7/5
*/
@Slf4j
@SuppressWarnings("all")
public class SchedulingRunnable implements Runnable {
private final String beanName;
private final String methodName;
private final String params;
private final Integer jobId;
public SchedulingRunnable(String beanName, String methodName, String params, Integer jobId) {
this.beanName = beanName;
this.methodName = methodName;
this.params = params;
this.jobId = jobId;
}
@Override
public void run() {
log.info("定時任務開始執行 - bean: {}, 方法: {}, 參數: {}, 任務ID: {}", beanName, methodName, params, jobId);
long startTime = System.currentTimeMillis();
try {
Object target = SpringContextUtils.getBean(beanName);
Method method;
if (StringUtils.isNotEmpty(params)) {
method = target.getClass().getDeclaredMethod(methodName, String.class);
} else {
method = target.getClass().getDeclaredMethod(methodName);
}
ReflectionUtils.makeAccessible(method);
if (StringUtils.isNotEmpty(params)) {
method.invoke(target, params);
} else {
method.invoke(target);
}
} catch (Exception ex) {
log.error("定時任務執行異常 - bean: {}, 方法: {}, 參數: {}, 任務ID: {}", beanName, methodName, params, jobId, ex);
}
long times = System.currentTimeMillis() - startTime;
log.info("定時任務執行結束 - bean: {}, 方法: {}, 參數: {}, 任務ID: {}, 耗時: {}毫秒", beanName, methodName, params, jobId, times);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (Objects.isNull(obj) || getClass() != obj.getClass()) {
return false;
}
SchedulingRunnable that = (SchedulingRunnable) obj;
if (Objects.isNull(params)) {
return beanName.equals(that.beanName) &&
methodName.equals(that.methodName) &&
that.params == null;
}
return beanName.equals(that.beanName) &&
methodName.equals(that.methodName) &&
params.equals(that.params) &&
jobId.equals(that.jobId);
}
@Override
public int hashCode() {
if (Objects.isNull(params)) {
return Objects.hash(beanName, methodName, jobId);
}
return Objects.hash(beanName, methodName, params, jobId);
}
}
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
/**
* 定時任務包裝類
* <p>
* ScheduledFuture是ScheduledExecutorService定時任務線程池的執行結果
* </p>
*
* @author 星空流年
* @date 2023/7/5
*/
@SuppressWarnings("all")
public final class ScheduledTask {
volatile ScheduledFuture<?> future;
/**
* 取消定時任務
*/
public void cancel() {
ScheduledFuture<?> scheduledFuture = this.future;
if (Objects.nonNull(scheduledFuture)) {
scheduledFuture.cancel(true);
}
}
}
import javax.annotation.Resource;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* 定時任務註冊類
* <p>
* 定時任務註冊類, 主要用於增加、刪除定時任務
* </p>
*
* @author 星空流年
* @date 2023/7/5
*/
@Component
@SuppressWarnings("all")
public class CronTaskRegistrar implements DisposableBean {
private final Map<Runnable, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);
@Resource
private TaskScheduler taskScheduler;
public void addCronTask(Runnable task, String cronExpression) {
addCronTask(new CronTask(task, cronExpression));
}
public void addCronTask(CronTask cronTask) {
if (Objects.nonNull(cronTask)) {
Runnable task = cronTask.getRunnable();
if (this.scheduledTasks.containsKey(task)) {
removeCronTask(task);
}
this.scheduledTasks.put(task, scheduleCronTask(cronTask));
}
}
public void removeCronTask(Runnable task) {
ScheduledTask scheduledTask = this.scheduledTasks.remove(task);
if (Objects.nonNull(scheduledTask)) {
scheduledTask.cancel();
}
}
public ScheduledTask scheduleCronTask(CronTask cronTask) {
ScheduledTask scheduledTask = new ScheduledTask();
scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
return scheduledTask;
}
@Override
public void destroy() {
this.scheduledTasks.values().forEach(ScheduledTask::cancel);
this.scheduledTasks.clear();
}
}
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import javax.annotation.Resource;
import net.cnki.nxgp.metric.recording.rule.entity.pojo.ScheduleJobEnum;
import net.cnki.nxgp.metric.recording.rule.task.component.CronTaskRegistrar;
import net.cnki.nxgp.metric.recording.rule.task.component.SchedulingRunnable;
import net.cnki.nxgp.metric.recording.rule.task.entity.ScheduleSetting;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 定時任務動態管理具體實現工具類
*
* @author 星空流年
* @date 2023/7/5
*/
@Component
public class TaskUtils {
@Resource
private CronTaskRegistrar cronTaskRegistrar;
/**
* 添加定時任務
*
* @param scheduleJob 定時任務實體類
* @return boolean
*/
public ScheduleSetting insertTaskJob(ScheduleSetting scheduleJob) {
scheduleJob.setCreateTime(new Date());
scheduleJob.setUpdateTime(new Date());
boolean insert = scheduleJob.insert();
if (!insert) {
return null;
}
// 添加成功, 並且狀態是啓用, 則直接放入任務器
if (scheduleJob.getJobStatus().equals(ScheduleJobEnum.ENABLED.getStatusCode())) {
SchedulingRunnable task = new SchedulingRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(), scheduleJob.getMethodParams(), scheduleJob.getJobId());
cronTaskRegistrar.addCronTask(task, scheduleJob.getCronExpression());
}
return scheduleJob;
}
/**
* 更新定時任務
*
* @param scheduleJob 定時任務實體類
* @return boolean
*/
public boolean updateTaskJob(ScheduleSetting scheduleJob) {
scheduleJob.setCreateTime(new Date());
scheduleJob.setUpdateTime(new Date());
// 查詢修改前任務
ScheduleSetting existedSysJob = new ScheduleSetting();
LambdaQueryWrapper<ScheduleSetting> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ScheduleSetting::getJobId, scheduleJob.getJobId());
existedSysJob = existedSysJob.selectOne(queryWrapper);
// 修改任務
LambdaUpdateWrapper<ScheduleSetting> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ScheduleSetting::getJobId, scheduleJob.getJobId());
boolean update = scheduleJob.update(updateWrapper);
if (!update) {
return false;
}
// 修改成功, 則先刪除任務器中的任務, 並重新添加
SchedulingRunnable preTask = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams(), existedSysJob.getJobId());
cronTaskRegistrar.removeCronTask(preTask);
// 如果修改後的任務狀態是啓用, 就加入任務器
if (scheduleJob.getJobStatus().equals(ScheduleJobEnum.ENABLED.getStatusCode())) {
SchedulingRunnable task = new SchedulingRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(), scheduleJob.getMethodParams(), scheduleJob.getJobId());
cronTaskRegistrar.addCronTask(task, scheduleJob.getCronExpression());
}
return true;
}
/**
* 刪除定時任務
*
* @param jobId 定時任務id
* @return boolean
*/
public boolean deleteTaskJob(Integer jobId) {
// 先查詢要刪除的任務信息
ScheduleSetting existedJob = new ScheduleSetting();
LambdaQueryWrapper<ScheduleSetting> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ScheduleSetting::getJobId, jobId);
existedJob = existedJob.selectOne(queryWrapper);
// 刪除
boolean delete = existedJob.delete(queryWrapper);
if (!delete) {
return false;
}
// 刪除成功, 並刪除定時任務器中的對應任務
SchedulingRunnable task = new SchedulingRunnable(existedJob.getBeanName(), existedJob.getMethodName(), existedJob.getMethodParams(), jobId);
cronTaskRegistrar.removeCronTask(task);
return true;
}
/**
* 停止/啓動定時任務
*
* @param jobId 定時任務id
* @param jobStatus 定時任務狀態
* @return boolean
*/
public boolean changeStatus(Integer jobId, Integer jobStatus) {
// 修改任務狀態
ScheduleSetting scheduleSetting = new ScheduleSetting();
scheduleSetting.setJobStatus(jobStatus);
boolean update = scheduleSetting.update(new LambdaUpdateWrapper<ScheduleSetting>().eq(ScheduleSetting::getJobId, jobId));
if (!update) {
return false;
}
// 查詢修改後的任務信息
ScheduleSetting existedJob = new ScheduleSetting();
existedJob = existedJob.selectOne(new LambdaQueryWrapper<ScheduleSetting>().eq(ScheduleSetting::getJobId, jobId));
// 如果狀態是啓用, 則添加任務
SchedulingRunnable task = new SchedulingRunnable(existedJob.getBeanName(), existedJob.getMethodName(), existedJob.getMethodParams(), jobId);
if (existedJob.getJobStatus().equals(ScheduleJobEnum.ENABLED.getStatusCode())) {
cronTaskRegistrar.addCronTask(task, existedJob.getCronExpression());
} else {
// 反之, 則刪除任務
cronTaskRegistrar.removeCronTask(task);
}
return true;
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 定時任務類
*
* @author 星空流年
* @date 2023/7/5
*/
@Slf4j
@Component("jobTaskTest")
public class JobTask {
/**
* 此處爲需要執行定時任務的方法, 可以根據需求自行添加對應的定時任務方法
*/
public void upsertTask(String params) {
// ...
log.info("定時任務執行啦...");
}
}
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class ScheduleJobApplicationTests {
@Resource
private TaskUtils taskUtils;
@Test
public void testInsertTask() {
ScheduleSetting scheduleJob = new ScheduleSetting();
// 此處beanName, methodName對應定時任務執行類中定義的beanName、方法名
scheduleJob.setBeanName("jobTaskTest");
scheduleJob.setMethodName("upsertTask");
// 方法參數由於定時任務Runnable接口包裝類中定義爲字符串類型, 如果爲其他類型,注意轉換
scheduleJob.setMethodParams("params");
scheduleJob.setJobStatus(ScheduleJobEnum.ENABLED.getStatusCode());
String cron = "*/30 * * * * ?";
scheduleJob.setCronExpression(cron);
scheduleJob.setRemark("定時任務新增");
ScheduleSetting scheduleTask = taskUtils.insertTaskJob(scheduleJob);
if (Objects.isNull(scheduleTask)) {
log.error("定時任務新增失敗");
}
}
@Test
public void testUpdateTask() {
ScheduleSetting scheduleJob = new ScheduleSetting();
scheduleJob.setJobId(1);
// 此處beanName, methodName對應定時任務執行類中定義的beanName、方法名
scheduleJob.setBeanName("jobTaskTest");
scheduleJob.setMethodName("upsertTask");
// 方法參數由於定時任務Runnable接口包裝類中定義爲字符串類型, 如果爲其他類型,注意轉換
scheduleJob.setMethodParams("params");
scheduleJob.setJobStatus(ScheduleJobEnum.ENABLED.getStatusCode());
String cron = "*/60 * * * * ?";
scheduleJob.setCronExpression(cron);
scheduleJob.setRemark("定時任務更新");
ScheduleSetting scheduleTask = taskUtils.updateTaskJob(scheduleJob);
if (Objects.isNull(scheduleTask)) {
log.error("定時任務更新失敗");
}
}
@Test
public void testChangeTaskStatus() {
boolean changeFlag = taskUtils.changeStatus(1, 0);
if (!changeFlag) {
log.error("定時任務狀態更新失敗");
}
}
@Test
public void testDeleteTask() {
boolean deleteFlag = taskUtils.deleteTaskJob(1);
if (!deleteFlag) {
log.error("定時任務刪除失敗");
}
}
}
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import xxx.entity.pojo.ScheduleJobEnum;
import xxx.entity.ScheduleSetting;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 初始化數據庫中啓用狀態下的定時任務
*
* @author 星空流年
* @date 2023/7/5
*/
@Slf4j
@Component
public class TaskJobInitRunner implements CommandLineRunner {
@Resource
private CronTaskRegistrar cronTaskRegistrar;
@Override
public void run(String... args) {
// 初始化加載數據庫中狀態爲啓用的定時任務
ScheduleSetting existedSysJob = new ScheduleSetting();
List<ScheduleSetting> jobList = existedSysJob.selectList(new LambdaQueryWrapper<ScheduleSetting>().eq(ScheduleSetting::getJobStatus, ScheduleJobEnum.ENABLED.getStatusCode()));
if (CollectionUtils.isNotEmpty(jobList)) {
jobList.forEach(job -> {
SchedulingRunnable task = new SchedulingRunnable(job.getBeanName(), job.getMethodName(), job.getMethodParams(), job.getJobId());
cronTaskRegistrar.addCronTask(task, job.getCronExpression());
});
log.info("~~~~~~~~~~~~~~~~~~~~~ 定時任務初始化完成 ~~~~~~~~~~~~~~~~~~~~~");
}
}
}