Springboot实现动态定时任务管理
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=1 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 xxx.recording.rule.entity.pojo.ScheduleJobEnum;
import xxx.recording.rule.task.component.CronTaskRegistrar;
import xxx.recording.rule.task.component.SchedulingRunnable;
import xxx.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("定时任务更新");
boolean updateFlag = taskUtils.updateTaskJob(scheduleJob);
if (!updateFlag) {
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("~~~~~~~~~~~~~~~~~~~~~ 定时任务初始化完成 ~~~~~~~~~~~~~~~~~~~~~");
}
}
}
**************************************************** 林深时见鹿,海蓝时见鲸 ****************************************************