Springboot实现动态定时任务管理

最近在做spring boot项目开发中,由于使用@EnableScheduling注解和@Scheduled注解来实现的定时任务,只能静态的创建定时任务,不能动态修改、添加、删除、启/停任务。由于项目开发体量不大,如果引入xxl-job等开源框架处理,会导致项目过于臃肿和复杂,同时通过查找相关资料,发现可以通过改造spring-context.jar包中org.springframework.scheduling.ScheduledTaskRegistrar类实现动态增删启停定时任务功能,而且网上也有类似文章介绍,于是便动手实践了一下,发现是可行的。

1、定时任务表设计

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;
}

2.2、定时任务状态枚举类

/**
 * 定时任务启用、停用枚举类
 *
 * @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;
    }
}

2.3、定时任务Mapper类

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;
    }
}

3.2、获取Bean工具类

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);
    }
}

3.3、Runnable接口实现类

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);
    }
}

3.4、ScheduledFuture包装类

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);
        }
    }
}

3.5、定时任务注册类

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();
    }
}

4、定时任务增删改工具类

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;
    }
}

5、定时任务执行类

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("定时任务执行啦...");
    }
}

6、定时任务测试类

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("定时任务删除失败");
        }
    }
}

7、初始化定时任务

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("~~~~~~~~~~~~~~~~~~~~~ 定时任务初始化完成 ~~~~~~~~~~~~~~~~~~~~~");
        }
    }
}
posted @ 2023-07-07 10:24  星空流年  阅读(4097)  评论(3编辑  收藏  举报