在spring boot项目中,可以通过@EnableScheduling注解和@Scheduled注解实现定时任务,也可以通过SchedulingConfigurer接口来实现定时任务。但是这两种方式不能动态添加、删除、启动、停止任务。要实现动态增删启停定时任务功能,比较广泛的做法是集成Quartz框架。但是本人的开发原则是:在满足项目需求的情况下,尽量少的依赖其它框架,避免项目过于臃肿和复杂。查看spring-context这个jar包中org.springframework.scheduling.ScheduledTaskRegistrar这个类的源代码,发现可以通过改造这个类就能实现动态增删启停定时任务功能。



springbootyml 配置clickhouse 时区_定时任务

定时任务列表页



springbootyml 配置clickhouse 时区_定时任务_02

定时任务执行日志

添加执行定时任务的线程池配置类

1@Configuration
 2public class SchedulingConfig {
 3    @Bean
 4    public TaskScheduler taskScheduler() {
 5        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
 6        // 定时任务执行线程池核心线程数
 7        taskScheduler.setPoolSize(4);
 8        taskScheduler.setRemoveOnCancelPolicy(true);
 9        taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
10        return taskScheduler;
11    }
12}

添加ScheduledFuture的包装类。ScheduledFuture是ScheduledExecutorService定时任务线程池的执行结果。

1public final class ScheduledTask {
 2
 3    volatile ScheduledFuture> future;
 4
 5    /** 6     * 取消定时任务 7     */
 8    public void cancel() {
 9        ScheduledFuture> future = this.future;
10        if (future != null) {
11            future.cancel(true);
12        }
13    }
14}

添加Runnable接口实现类,被定时任务线程池调用,用来执行指定bean里面的方法。

1public class SchedulingRunnable implements Runnable {
 2
 3    private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);
 4
 5    private String beanName;
 6
 7    private String methodName;
 8
 9    private String params;
10
11    public SchedulingRunnable(String beanName, String methodName) {
12        this(beanName, methodName, null);
13    }
14
15    public SchedulingRunnable(String beanName, String methodName, String params) {
16        this.beanName = beanName;
17        this.methodName = methodName;
18        this.params = params;
19    }
20
21    @Override
22    public void run() {
23        logger.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params);
24        long startTime = System.currentTimeMillis();
25
26        try {
27            Object target = SpringContextUtils.getBean(beanName);
28
29            Method method = null;
30            if (StringUtils.isNotEmpty(params)) {
31                method = target.getClass().getDeclaredMethod(methodName, String.class);
32            } else {
33                method = target.getClass().getDeclaredMethod(methodName);
34            }
35
36            ReflectionUtils.makeAccessible(method);
37            if (StringUtils.isNotEmpty(params)) {
38                method.invoke(target, params);
39            } else {
40                method.invoke(target);
41            }
42        } catch (Exception ex) {
43            logger.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), ex);
44        }
45
46        long times = System.currentTimeMillis() - startTime;
47        logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times);
48    }
49
50    @Override
51    public boolean equals(Object o) {
52        if (this == o) return true;
53        if (o == null || getClass() != o.getClass()) return false;
54        SchedulingRunnable that = (SchedulingRunnable) o;
55        if (params == null) {
56            return beanName.equals(that.beanName) &&
57                    methodName.equals(that.methodName) &&
58                    that.params == null;
59        }
60
61        return beanName.equals(that.beanName) &&
62                methodName.equals(that.methodName) &&
63                params.equals(that.params);
64    }
65
66    @Override
67    public int hashCode() {
68        if (params == null) {
69            return Objects.hash(beanName, methodName);
70        }
71
72        return Objects.hash(beanName, methodName, params);
73    }
74}

添加定时任务注册类,用来增加、删除定时任务。

1@Component
 2public class CronTaskRegistrar implements DisposableBean {
 3
 4    private final Map scheduledTasks = new ConcurrentHashMap<>(16); 5 6    @Autowired 7    private TaskScheduler taskScheduler; 8 9    public TaskScheduler getScheduler() {10        return this.taskScheduler;11    }1213    public void addCronTask(Runnable task, String cronExpression) {14        addCronTask(new CronTask(task, cronExpression));15    }1617    public void addCronTask(CronTask cronTask) {18        if (cronTask != null) {19            Runnable task = cronTask.getRunnable();20            if (this.scheduledTasks.containsKey(task)) {21                removeCronTask(task);22            }2324            this.scheduledTasks.put(task, scheduleCronTask(cronTask));25        }26    }2728    public void removeCronTask(Runnable task) {29        ScheduledTask scheduledTask = this.scheduledTasks.remove(task);30        if (scheduledTask != null)31            scheduledTask.cancel();32    }3334    public ScheduledTask scheduleCronTask(CronTask cronTask) {35        ScheduledTask scheduledTask = new ScheduledTask();36        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());3738        return scheduledTask;39    }404142    @Override43    public void destroy() {44        for (ScheduledTask task : this.scheduledTasks.values()) {45            task.cancel();46        }4748        this.scheduledTasks.clear();49    }50}

添加定时任务示例类

1@Component("demoTask")
 2public class DemoTask {
 3    public void taskWithParams(String params) {
 4        System.out.println("执行有参示例任务:" + params);
 5    }
 6
 7    public void taskNoParams() {
 8        System.out.println("执行无参示例任务");
 9    }
10}

定时任务数据库表设计



springbootyml 配置clickhouse 时区_System_03

定时任务数据库表设计

添加定时任务实体类

1public class SysJobPO {
  2    /**
  3     * 任务ID
  4     */
  5    private Integer jobId;
  6    /**
  7     * bean名称
  8     */
  9    private String beanName;
 10    /**
 11     * 方法名称
 12     */
 13    private String methodName;
 14    /**
 15     * 方法参数
 16     */
 17    private String methodParams;
 18    /**
 19     * cron表达式
 20     */
 21    private String cronExpression;
 22    /**
 23     * 状态(1正常 0暂停)
 24     */
 25    private Integer jobStatus;
 26    /**
 27     * 备注
 28     */
 29    private String remark;
 30    /**
 31     * 创建时间
 32     */
 33    private Date createTime;
 34    /**
 35     * 更新时间
 36     */
 37    private Date updateTime;
 38
 39    public Integer getJobId() {
 40        return jobId;
 41    }
 42
 43    public void setJobId(Integer jobId) {
 44        this.jobId = jobId;
 45    }
 46
 47    public String getBeanName() {
 48        return beanName;
 49    }
 50
 51    public void setBeanName(String beanName) {
 52        this.beanName = beanName;
 53    }
 54
 55    public String getMethodName() {
 56        return methodName;
 57    }
 58
 59    public void setMethodName(String methodName) {
 60        this.methodName = methodName;
 61    }
 62
 63    public String getMethodParams() {
 64        return methodParams;
 65    }
 66
 67    public void setMethodParams(String methodParams) {
 68        this.methodParams = methodParams;
 69    }
 70
 71    public String getCronExpression() {
 72        return cronExpression;
 73    }
 74
 75    public void setCronExpression(String cronExpression) {
 76        this.cronExpression = cronExpression;
 77    }
 78
 79    public Integer getJobStatus() {
 80        return jobStatus;
 81    }
 82
 83    public void setJobStatus(Integer jobStatus) {
 84        this.jobStatus = jobStatus;
 85    }
 86
 87    public String getRemark() {
 88        return remark;
 89    }
 90
 91    public void setRemark(String remark) {
 92        this.remark = remark;
 93    }
 94
 95    public Date getCreateTime() {
 96        return createTime;
 97    }
 98
 99    public void setCreateTime(Date createTime) {
100        this.createTime = createTime;
101    }
102
103    public Date getUpdateTime() {
104        return updateTime;
105    }
106
107    public void setUpdateTime(Date updateTime) {
108        this.updateTime = updateTime;
109    }
110
111}

新增定时任务



springbootyml 配置clickhouse 时区_定时任务_04

新增定时任务

1boolean success = sysJobRepository.addSysJob(sysJob);
 2if (!success)
 3    return OperationResUtils.fail("新增失败");
 4else {
 5    if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
 6        SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
 7        cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());
 8    }
 9}
10
11return OperationResUtils.success();

修改定时任务,先移除原来的任务,再启动新任务

1boolean success = sysJobRepository.editSysJob(sysJob);
 2if (!success)
 3    return OperationResUtils.fail("编辑失败");
 4else {
 5    //先移除再添加
 6    if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
 7        SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
 8        cronTaskRegistrar.removeCronTask(task);
 9    }
10
11    if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
12        SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
13        cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());
14    }
15}
16
17return OperationResUtils.success();

删除定时任务

1boolean success = sysJobRepository.deleteSysJobById(req.getJobId());
 2if (!success)
 3    return OperationResUtils.fail("删除失败");
 4else{
 5    if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
 6        SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
 7        cronTaskRegistrar.removeCronTask(task);
 8    }
 9}
10
11return OperationResUtils.success();

定时任务启动/停止状态切换

1if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
2    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
3    cronTaskRegistrar.addCronTask(task, existedSysJob.getCronExpression());
4} else {
5    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
6    cronTaskRegistrar.removeCronTask(task);
7}

添加实现了CommandLineRunner接口的SysJobRunner类,当spring boot项目启动完成后,加载数据库里状态为正常的定时任务。

1@Service
 2public class SysJobRunner implements CommandLineRunner {
 3
 4    private static final Logger logger = LoggerFactory.getLogger(SysJobRunner.class);
 5
 6    @Autowired
 7    private ISysJobRepository sysJobRepository;
 8
 9    @Autowired
10    private CronTaskRegistrar cronTaskRegistrar;
11
12    @Override
13    public void run(String... args) {
14        // 初始加载数据库里状态为正常的定时任务
15        List jobList = sysJobRepository.getSysJobListByStatus(SysJobStatus.NORMAL.ordinal());16        if (CollectionUtils.isNotEmpty(jobList)) {17            for (SysJobPO job : jobList) {18                SchedulingRunnable task = new SchedulingRunnable(job.getBeanName(), job.getMethodName(), job.getMethodParams());19                cronTaskRegistrar.addCronTask(task, job.getCronExpression());20            }2122            logger.info("定时任务已加载完毕...");23        }24    }25}

工具类SpringContextUtils,用来从spring容器里获取bean

1@Component
 2public class SpringContextUtils implements ApplicationContextAware {
 3
 4    private static ApplicationContext applicationContext;
 5
 6    @Override
 7    public void setApplicationContext(ApplicationContext applicationContext) 8            throws BeansException {
 9        SpringContextUtils.applicationContext = applicationContext;
10    }
11
12    public static Object getBean(String name) {
13        return applicationContext.getBean(name);
14    }
15
16    public static  T getBean(Class requiredType) {17        return applicationContext.getBean(requiredType);18    }1920    public static  T getBean(String name, Class requiredType) {21        return applicationContext.getBean(name, requiredType);22    }2324    public static boolean containsBean(String name) {25        return applicationContext.containsBean(name);26    }2728    public static boolean isSingleton(String name) {29        return applicationContext.isSingleton(name);30    }3132    public static Class extends Object> getType(String name) {33        return applicationContext.getType(name);34    }35}