先说点无关本文的问题, 这段时间特别的不爽, 可能有些同学也遇到过. 其实也可以说是小事一桩, 但感觉也是不容忽视的. 我刚毕业时的公司, 每个人每次提交代码都有着严格的规范, 像 table 和 space 的缩进都有严格的要求, 可以说你不遵守开发规范就相当于线上 bug 问题, 还是比较严重的. 现在发现外面的公司真的是没那么重视这个不重要却又特别重要的问题啊, 啊啊啊啊啊啊!!!
什么是动态配置定时任务?
回归正题, 说下这次主题, 动态配置. 没接触过定时任务的同学可以先看下此篇: 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);
- }
彩蛋部分~
好了, 正文部分基本上就这些, 不多废话了, 本文没有太多的讲解原理, 只是简单的应用, 水平不够还望大家见谅.
看了上面的效果图是不是还是有种看的见摸不着的感觉, 老爷爷这里也做了个线上的实例供大家体验~ 有兴趣的同学可以亲自试验一下.
实例地址: http://www.laoyeye.net/management/index 账号 / 密码: test/123456 菜单: 系统设置 / 计划任务
当然了还有最重要的源码和 API 文档我也不会忘的.
源码地址: https://github.com/allanzhuo/yyblog
Quartz 文档地址: https://github.com/allanzhuo/yyblog/tree/master/doc
最后的最后, 看都看了, 码字不易, 顺手点个赞白~^_^
来源: https://www.cnblogs.com/laoyeye/p/9352002.html