⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 cnblogs.com/laoyeye/p/9352002.html 「老爷爷」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

什么是动态配置定时任务?

首先说下这次主题,动态配置。没接触过定时任务的同学可以先看下此篇:JAVA定时任务实现的几种方式

定时任务实现方式千人千种,不过基础的无外乎 1、JDK 的Timer类 2、Quartz 3、SpringTask 。生产上三种方式我都有使用过。但是使用过程中用的最多的便是xml配置的方式,这种方式最简单,无代码侵入,也比较好理解。 但是却有个致命的缺点,比如你要改某个任务的触发时间,亦或是你要新增一个任务,暂停一个任务。怎么做? 停应用!改XML配置!重新启动! 是不是很致命。最近重新研究了下Quartz的配置,实现了不停机添加、暂停、删除、立即触发任务的方法,在本篇分享出来,其实也不算是完全的研究,在上家公司已经有大佬实现了,这次是也是基于大佬的理解重新实现下。

国际惯例~先看效果图 ps:文末有彩蛋哦~~

管理界面:

效果图:实在不知道该跑什么了,每隔十秒打一段话吧

技术实现

maven依赖

使用springboot做框架支持

<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>

数据表

CREATE TABLE `sys_task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
`description` varchar(255) DEFAULT NULL COMMENT '任务描述',
`cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
`bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
`job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
`job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
`create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;

实现步骤

①启动项目,启动task监听

②读取数据库,将开启的任务job和trigger加载到scheduler调度器

③根据任务调度运行job类

④每次运行利用AdaptableJobFactory实例化job类,以便注入要运行的service

听着是不是很简单,但却还是一头雾水,且听我慢慢道来~~

代码逻辑

第一步:启动项目,加载监听

Quartz配置

Springboot的配置方法,常规Spring项目可以在xml中配置

@Configuration
public class QuartzConfigration {

@Autowired
private JobFactory jobFactory;

@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
try {
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setQuartzProperties(quartzProperties());
schedulerFactoryBean.setJobFactory(jobFactory);
} catch (Exception e) {
e.printStackTrace();
}
return schedulerFactoryBean;
}

// 指定quartz.properties,可在配置文件中配置相关属性
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/config/quartz.properties"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}

// 创建schedule
@Bean(name = "scheduler")
public Scheduler scheduler() {
return schedulerFactoryBean().getScheduler();
}
}

监听器

@Component
@Order(value = 1)
public class ScheduleJobInitListener implements CommandLineRunner {

@Autowired
TaskService scheduleJobService;

@Override
public void run(String... arg0) throws Exception {
try {
scheduleJobService.initSchedule();
} catch (Exception e) {
e.printStackTrace();
}

}

}

CommandLineRunner类似Spring框架的ApplicationListener监听器。官方的解释是:

Interface used to indicate that a bean should run when it is contained within a SpringApplication. Multiple CommandLineRunner beans can be defined within the same application context and can be ordered using the Ordered interface or Order @Order annotation. 接口被用作将其加入spring容器中时执行其run方法。多个CommandLineRunner可以被同时执行在同一个spring上下文中并且执行顺序是以order注解的参数顺序一致。

第二步:读取数据库,加载scheduler调度器

job方法

@Override
public void initSchedule() throws SchedulerException {
// 这里获取任务信息数据
List<TaskDO> jobList = taskMapper.list();
for (TaskDO task : jobList) {
if (JobStatusEnum.RUNNING.getCode().equals(task.getJobStatus())) {
quartzManager.addJob(task);
}
}
}

添加任务到Quartz调度器

/**
* 添加任务*/
@SuppressWarnings("unchecked")
public void addJob(TaskDO task) {
try {
// 创建jobDetail实例,绑定Job实现类
// 指明job的名称,所在组的名称,以及绑定job类
Class<? extends Job> jobClass = (Class<? extends Job>) (Class.forName(task.getBeanClass()).newInstance()
.getClass());
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(task.getJobName(), task.getJobGroup())// 任务名称和组构成任务key
.build();
// 定义调度触发规则
// 使用cornTrigger规则
Trigger trigger = TriggerBuilder.newTrigger().withIdentity(task.getJobName(), task.getJobGroup())// 触发器key
.startAt(DateBuilder.futureDate(1, IntervalUnit.SECOND))
.withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression())).startNow().build();
// 把作业和触发器注册到任务调度中
scheduler.scheduleJob(jobDetail, trigger);
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}

Scheduler作为Quartz的核心调度器,有将近50多个API接口,包括任务的添加,暂停,恢复,删除等一系列的API,这里仅介绍一些常用的,想要了解更多可以稍后看下彩蛋部分。

1、start()方法:只有调用start()方法后,Scheduler线程才开始启动触发器trigger,运行job

2、pauseJob(JobKey jobKey) :根据指定的JobDetail key暂停job。

3、resumeJob(JobKey jobKey) :根据指定的key恢复一个job。

4、deleteJob(JobKey jobKey) :删除一个job

5、triggerJob(JobKey jobKey) :触发一个JobDetail(现在执行)。

6、rescheduleJob(TriggerKey triggerKey, Trigger newTrigger):

用给定的键删除触发器,并存储新的触发器,它必须与同一个作业相关联(新触发器必须具有指定的作业名和组)-然而,新触发器不必具有与旧触发器相同的名称。

第三步:根据任务调度运行job类 其实这一步是不需要我们编写的,在我们将正确的JobDetail 和 Trigger 表达式加载到任务调度后,调度器会自动触发任务的执行

第四步:实例化job类,注入要运行的service

工厂类

@Component
public class JobFactory extends AdaptableJobFactory {
//这个对象Spring会帮我们自动注入进来,也属于Spring技术范畴.
//为什么需要这个类呢,在我写的这个demo中,大家可以将此类删掉,发现程序也可以正确运行,可是我为什么还是加上呢。
//大家可以看下我们的任务类,大家可以看到Job对象的实例化过程是在Quartz中进行的,这时候我们将spring的东西注入进来,肯定是行不通的,所以需要这个类
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;

@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
//调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
//进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}

任务类

@DisallowConcurrentExecution //作业不并发
@Component
public class HelloWorldJob implements Job{

@Override
public void execute(JobExecutionContext arg0) throws JobExecutionException {

System.out.println("欢迎使用yyblog,这是一个定时任务 --小卖铺的老爷爷!"+ DateUtils.fullTime(new Date()));

}

}

好了,大功告成,一个简单的动态配置的定时任务已经完成。是不是so easy,下面我们再来简单实现下其他的几种常用的api吧。

暂停一个job

public void pauseJob(TaskDO task) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(task.getJobName(), task.getJobGroup());
scheduler.pauseJob(jobKey);
}

恢复一个job

public void resumeJob(TaskDO task) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(task.getJobName(), task.getJobGroup());
scheduler.resumeJob(jobKey);
}

删除一个job

public void deleteJob(TaskDO task) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(task.getJobName(), task.getJobGroup());
scheduler.deleteJob(jobKey);
}

立即触发job

public void runJobNow(TaskDO task) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(task.getJobName(), task.getJobGroup());
scheduler.triggerJob(jobKey);
}

更新job表达式

public void updateJobCron(TaskDO task) throws SchedulerException {

TriggerKey triggerKey = TriggerKey.triggerKey(task.getJobName(), task.getJobGroup());

CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);

CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(task.getCronExpression());

trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();

scheduler.rescheduleJob(triggerKey, trigger);
}

彩蛋部分~

好了,正文部分基本上就这些,不多废话了,本文没有太多的讲解原理,只是简单的应用,水平不够还望大家见谅。

看了上面的效果图是不是还是有种看的见摸不着的感觉,老爷爷这里也做了个线上的实例供大家体验~有兴趣的同学可以亲自试验一下。

Quartz文档地址:https://github.com/allanzhuo/yyblog/tree/master/doc

最后的最后,看都看了,码字不易,顺手点个赞白~^_^

源码地址

首先点击右下方在看,再长按下二维码关注哦,并后台回复【p037】即可获得使用示例 GitHub。

二维码

文章目录
  1. 1. 什么是动态配置定时任务?
  2. 2. 国际惯例~先看效果图 ps:文末有彩蛋哦~~
  3. 3. 技术实现
    1. 3.0.1. maven依赖
    2. 3.0.2. 数据表
    3. 3.0.3. 实现步骤
    4. 3.0.4. 代码逻辑
  • 4. 彩蛋部分~
  • 5. 源码地址