背景
最近要用到这个定时任务,之前就简单使用注解的那种方式,需求一变化,就得重新修改。就想到了动态定时任务,连接数据库来动态选择,这样确实解决了问题。但是仍然有一个缺陷,就是没法设置任务的执行时间,比如超过半小时未付款,把订单状态改为取消。
前言
阅读完本文:
知晓 SpringBoot
用注解如何实现定时任务
明白 SpringBoot
如何实现一个动态定时任务 (与数据库相关联实现)
理解 SpringBoot
实现设置时间执行定时任务 (使用 ThreadPoolTaskScheduler
实现)
一、注解实现定时任务
用注解实现是真的简单,只要会 cron 表达式就行。
第一步: 主启动类上加上 @EnableScheduling
注解
1 2 3 4 5 6 7 8
| @EnableScheduling @SpringBootApplication public class SpringBootScheduled {
public static void main(String[] args) { SpringApplication.run(SpringBootScheduled.class); } }
|
第二步:写一个类,注入到Spring,关键就是 @Scheduled
注解。 ()
里就是 cron 表达式,用来说明这个方法的执行周期的。
我常常也记不住,通常是在线生成的: Cron 表达式在线生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@Component public class SchedulingTaskBasic {
@Scheduled(cron = "*/5 * * * * ?") private void printNowDate() { long nowDateTime = System.currentTimeMillis(); System.out.println("固定定时任务执行:--->"+nowDateTime+",此任务为每五秒执行一次"); } }
|
执行效果:
源码:springboot-scheduled
@Scheduled除去cron还有三种方式:fixedRate,fixedDelay,initialDelay
cron: 表达式可以定制化执行任务,但是执行的方式是与fixedDelay相近的,也是会按照上一次方法结束时间开始算起。
fixedDelay: 控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次。
1 2 3 4 5 6 7 8 9
| @Configuration @EnableScheduling public class ScheduleTask1 { @Scheduled(fixedDelay = 3000) private void myTasks() { System.out.println("I do myself per third seconds"); } }
|
fixedRate: 是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行。
1 2 3 4 5 6 7 8 9
| @Component @EnableScheduling public class ScheduleTask2 { @Scheduled(fixedRate = 10000) private void myTasks2() { System.out.println("我是一个定时任务"); } }
|
initialDelay: initialDelay = 10000 表示在容器启动后,延迟10秒后再执行一次定时器。
1 2 3 4 5 6 7 8 9
| @Component @EnableScheduling public class ScheduleTask { @Scheduled(initialDelay = 10000, fixedRate = 10000) private void myTasks3() { System.out.println("我是一个定时任务3"); } }
|
二、动态定时任务
其实也非常的简单。
2.1、建数据表
第一步:建个数据库表。
1 2 3 4 5 6 7 8
| CREATE TABLE `tb_cron` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '动态定时任务时间表', `cron_expression` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '定时任务表达式', `cron_describe` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `tb_cron` VALUES (1, '0 0/1 * * * ?', '每分钟执行一次');
|
2.2、导入依赖,基础编码
实体类:
1 2 3 4 5 6 7
| @Data @TableName("tb_cron") public class Cron { private Long id; private String cronExpression; private String cronDescribe; }
|
mapper层:
1 2 3 4 5
| @Repository public interface CronMapper extends BaseMapper<Cron> { @Select("select cron_expression from tb_cron where id=1") String getCron1(); }
|
2.3、主要实现代码
写一个类 实现 SchedulingConfigurer
实现 void configureTasks(ScheduledTaskRegistrar taskRegistrar);
方法,此方法的作用就是根据给定的 ScheduledTaskRegistrar 注册 TaskScheduler 和特定的Task实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Component public class CompleteScheduleConfig implements SchedulingConfigurer {
@Autowired @SuppressWarnings("all") CronMapper cronMapper;
@Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addTriggerTask( () -> System.out.println("执行动态定时任务1: " + LocalDateTime.now().toLocalTime()+",此任务执行周期由数据库中的cron表达式决定"), triggerContext -> { String cron = cronMapper.getCron1(); if (cron!=null) { } return new CronTrigger(cron).nextExecutionTime(triggerContext); } ); } }
|
2.4、效果
注意:当你修改了任务执行周期后,生效时间为执行完最近一次任务后。这一点是需要注意的,用生活中的例子理解就是我们取消电话卡的套餐也要下个月生效,含义是一样的。
源码:springboot-scheduled
三、实现延迟任务
ThreadPoolTaskScheduler 这个类是在 org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
这个包中。
ThreadPoolTaskScheduler 是 spring taskSchedule
接口的实现,可以用来做定时任务使用。
ThreadPoolTaskScheduler 四个版本定时任务方法:
-
schedule(Runnable task, Date stateTime)
,在指定时间执行一次定时任务(实现一次性延迟任务)
-
schedule(Runnable task, Trigger trigger)
,动态创建指定表达式cron的定时任务
-
scheduleAtFixedRate
,指定间隔时间执行一次任务,间隔时间为前一次执行开始到下次任务开始时间
-
scheduleWithFixedDelay
,指定间隔时间执行一次任务,间隔时间为前一次任务完成到下一次开始时间
具体实现
我们需要向 spring 容器中注入一个 ThreadPoolTaskScheduler 的 bean,用于调度定时任务,以及需要增加一个缓存用于存入当前执行任务的 scheduleFuture 对象,将 ScheduledFuture 对象缓存的原因是在于,为了方面于停止对应的任务。
我们点击 ThreadPoolTaskScheduler 类,看具体的源码的时候会发现有这么一段断码:
这里是表示初始化的线程池的大小为 1,也就是说线程调度器设置只有一个线程容量,如果存在多个任务被触发时,会等第一个任务执行完毕才会执行下一个任务。所以这里还是需要自己去定义线程的大小,避免因为默认的线程池导致出现奇奇怪怪的问题。
第一步
创建 ThreadPoolTaskScheduler Bean 以及 用于存放定时任务的 map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Configuration public class ScheduleConfig {
public static ConcurrentHashMap<String, ScheduledFuture> taskList= new ConcurrentHashMap<String, ScheduledFuture>();
@Bean public ThreadPoolTaskScheduler threadPoolTaskScheduler(){ ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(10); threadPoolTaskScheduler.setThreadNamePrefix("taskExecutor-"); threadPoolTaskScheduler.setAwaitTerminationSeconds(60); threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true); return threadPoolTaskScheduler; } }
|
第二步
增加用于外部访问的接口 controller,等程序启动完成之后,我们需要调用对应的方法进行访问测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @RestController public class TestController {
@Autowired private DynamicTask task;
@RequestMapping("start") public void startTask() { com.demo.task.startCron(); }
@RequestMapping("stopById") public void stopById(String taskId) { com.demo.task.stop(taskId); }
@RequestMapping("stopAll") public void stopAll() { com.demo.task.stopAll(); } }
|
第三步
核心逻辑
在这里当在执行 threadPoolTaskScheduler.schedule()
时,会传入一个自定义的 com.demo.task,以及一个 trigger。
调用完成之后会返回一个 scheduledFuture,这个就是当前的任务调度器,停止的时候需要找到这个调度器,用这个调用器来终止。
-
threadPoolTaskScheduler.schedule(com.demo.task, cron)
用来调度任务
-
boolean cancelled = scheduledFuture.isCancelled();
用来判断是否已经取消
-
scheduledFuture.cancel(true)
用来将当前的任务取消
下面是核心代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| @Component public class DynamicTask { private final static Logger logger = LoggerFactory.getLogger(DynamicTask.class);
@Autowired private ThreadPoolTaskScheduler threadPoolTaskScheduler;
public void startCron(){
getTasks().forEach(customizeTask -> { ScheduledFuture scheduledFuture = threadPoolTaskScheduler.schedule(customizeTask, new CronTrigger(customizeTask.getCron())); ScheduleConfig.taskList.put(customizeTask.getName(), scheduledFuture); }); }
public void stop(String taskId) { if (ScheduleConfig.taskList.isEmpty()) return; if (ScheduleConfig.taskList.get(taskId) == null) return;
ScheduledFuture scheduledFuture = ScheduleConfig.taskList.get(taskId);
if (scheduledFuture != null) { scheduledFuture.cancel(true); ScheduleConfig.taskList.remove(taskId); } }
public void stopAll(){ if (ScheduleConfig.taskList.isEmpty()) return; ScheduleConfig.taskList.values().forEach(scheduledFuture -> scheduledFuture.cancel(true) ); }
private List<CustomizeTask> getTasks(){ return Arrays.asList( new CustomizeTask("任务一", "0/2 * * * * ?"), new CustomizeTask("任务二", "0/3 * * * * ?") ); }
private class CustomizeTask implements Runnable { private String name; private String cron;
CustomizeTask(String name, String cron) { this.name = name; this.cron = cron; }
public String getCron(){ return this.cron; }
public String getName(){ return this.name; }
@Override public void run() { logger.info("当前任务名称:{}", name ); } } }
|
对于上面的任务类 CustomizeTask
可以自定义进行封装,可以增加成员属性调用的 服务方法类
以及 调用参数
,在 run 方法上面就可以去调用对应的服务方法了,详见:SpringBoot动态定时任务(完整版)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private class CustomizeTask implements Runnable { private String name; private String cron; private String data; private String method;
CustomizeTask(String name, String cron, String data, String method) { this.name = name; this.cron = cron; this.data = data; this.method = method; }
public String getCron(){ return this.cron; }
public String getName(){ return this.name; }
@Override public void run() { logger.info("当前任务名称:{}", name ); } }
|
测试
当源码搭建完成之后,启动服务,我们测试下,首先我们先访问下启动任务
1
| http://localhost:8080/start
|
这里可以看到控制台交替输出任务的名称
接着,我们再来测试下停止其中的某个服务,当访问下面的链接之后,会发现任务一就已经被停止了
1
| http://localhost:8080/stopById?taskId=任务一
|
最后,我们访问下停止所有的任务:
1
| http://localhost:8080/stopAll
|
就会发现控制台没有任务在执行了。
摘自:SpringBoot实现固定、动态定时任务的三种实现方式