SpringBoot集成quartz实现定时任务
作者:bug菌¹
1. 前言
现如今,随着市场竞争加剧,各个企业都在不断寻求提高效率、降低成本的方法,此时使用自动化工具已成为必不可少的选择。而在众多的自动化工具中,定时任务已经成为一项必备工具,而Quartz就是一个非常好用的定时任务框架,它的轻量级、高可靠性、易于使用等特点,使得它成为一个非常受欢迎的定时任务框架。而在本篇文章中,我们将会介绍如何使用SpringBoot整合Quartz,并将定时任务写入库中(持久化存储),还可以任意对定时任务进行如删除、暂停、恢复等操作。
那么,具体如何实现呢?这将又会是干货满满的一期,全程无尿点不废话只抓重点教,具有非常好的学习效果,拿好小板凳准备就坐!希望学习的过程中大家认真听好好学,学习的途中有任何不清楚或疑问的地方皆可评论区留言或私信,bug菌将第一时间给予解惑,那么废话不多说,直接开整!Fighting!!
2. 环境说明
本地的开发环境:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 1.8
- Spring Boot版本:2.3.1 RELEASE
- Maven版本:3.8.2
3. 整合Quartz
下面实现一个简单的任务调度系统,并且将定时任务的信息持久化入库,希望可以帮助到大家,Quartz的非持久化使用本文就不赘述了。
3.1 搭建Spring Boot应用
首先,我们需要搭建一个Spring Boot项目,如果还不会点这里,此处就不详细赘述啦。
3.2 添加依赖
在Spring Boot项目中,我们可以使用Quartz Starter来集成Quartz框架。只需要在pom.xml文件中引入以下依赖即可:
<!--集成quartz--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> <version>2.3.12.RELEASE</version> </dependency>
基础相关依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mysql依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--util工具类--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency>
3.3 配置Quartz相关配置文件
然后,我们需要在application.properties文件中配置数据源及Quartz相关配置:
spring: datasource: name: db_quartz driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db_quartz?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF8&rewriteBatchedStatements=true username: root password: 123456 #quartz配置 quartz: #数据存储方式 默认是 MEMORY(内存方式) job-store-type: jdbc properties: org: quartz: scheduler: #调度标识名 instanceName: clusteredScheduler #ID设置为自动获取 每一个必须不同 instanceId: AUTO jobStore: #数据保存方式为持久化 class: org.quartz.impl.jdbcjobstore.JobStoreTX #数据库平台 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate #表的前缀 tablePrefix: QRTZ_ #加入集群 isClustered: false #调度实例失效的检查时间间隔 clusterCheckinInterval: 10000 #设置为TRUE不会出现序列化非字符串类到 BLOB 时产生的类版本问题 useProperties: false threadPool: #ThreadPool 实现的类名 class: org.quartz.simpl.SimpleThreadPool #线程数量 threadCount: 5 #线程优先级 threadPriority: 5 #自创建父线程 threadsInheritContextClassLoaderOfInitializingThread: true
其中,job-store-type表示使用的持久化方式,这里选择了使用jdbc完成任务持久化入库。jdbc的相关配置包括initialize-schema,comment-prefix,use-properties,table-prefix,scheduler-name等,这里不再赘述,具体的我已在代码中进行了详细注释。
3.4 添加Quartz表
接下来,我们需要创建一个数据库表,用于存储任务信息。在使用quartz做持久化的时候需要用到quartz的11张表,可以去quartz官网下载对应版本的quartz,你进入quartz的官网Quartz Enterprise Job Scheduler,点击Downloads,若想要现成的,可以去我的github仓库取https://github.com/luoyong0603/SpringBoot-demos。下载并解压,打开docs/dbTables里面有对应数据库的建表语句。演示截图如下:
我这选择的是mysql数据库且使用innodb引擎,对应的脚本文件是tables_mysql_innodb.sql,这里大家可以自行去了解下innodb与myisam的区别。这些表不需要我们做任何操作,是Quartz框架使用的。把创表sql执行后,就如下这样了,总共11张表,示例如下:
接下来对每张表的作用做以下总结,Quartz将Job保存在数据库中所需表的说明:
- QRTZ_CALENDARS:以 Blob 类型存储 Quartz 的 Calendar 信息
- QRTZ_CRON_TRIGGERS:存储 Cron Trigger,包括 Cron表达式和时区信息
- QRTZ_FIRED_TRIGGERS:存储与已触发的 Trigger 相关的状态信息,以及相联 Job的执行信息
- QRTZ_PAUSED_TRIGGER_GRPS:存储已暂停的 Trigger 组的信息
- QRTZ_SCHEDULER_STATE 存储少量的有关 Scheduler 的状态信息,和别的 Scheduler实例(假如是用于一个集群中)
- QRTZ_LOCKS 存储程序的悲观锁的信息(假如使用了悲观锁)
- QRTZ_JOB_DETAILS 存储每一个已配置的 Job 的详细信息
- QRTZ_SIMPLE_TRIGGERS 存储简单的Trigger,包括重复次数,间隔,以及已触的次数
- QRTZ_BLOG_TRIGGERS Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候)
- QRTZ_TRIGGER_LISTENERS 存储已配置的 TriggerListener 的信息
- QRTZ_TRIGGERS 存储已配置的 Trigger 的信息
并对其中的4张常用表进行表结构解读,辅助大家理解:
表qrtz_job_details: 保存job详细信息,该表需要用户根据实际情况初始化
job_name:集群中job的名字,该名字用户自己可以随意定制,无强行要求
job_group:集群中job的所属组的名字,该名字用户自己随意定制,无强行要求
job_class_name:集群中个note job实现类的完全包名,quartz就是根据这个路径到classpath找到该job类
is_durable:是否持久化,把该属性设置为1,quartz会把job持久化到数据库中
job_data:一个blob字段,存放持久化job对象
表qrtz_triggers: 保存trigger信息
trigger_name: trigger的名字,该名字用户自己可以随意定制,无强行要求
trigger_group:trigger所属组的名字,该名字用户自己随意定制,无强行要求
job_name: qrtz_job_details表job_name的外键
job_group: qrtz_job_details表job_group的外键
trigger_state:当前trigger状态,设置为ACQUIRED,如果设置为WAITING,则job不会触发
trigger_cron:触发器类型,使用cron表达式
表qrtz_cron_triggers:存储cron表达式表
trigger_name: qrtz_triggers表trigger_name的外键
trigger_group: qrtz_triggers表trigger_group的外键
cron_expression:cron表达式
表qrtz_scheduler_state:存储集群中note实例信息,quartz会定时读取该表的信息判断集群中每个实例的当前状态
instance_name:之前配置文件中org.quartz.scheduler.instanceId配置的名字,就会写入该字段,如果设置为AUTO,quartz会根据物理机名和当前时间产生一个名字
last_checkin_time:上次检查时间
checkin_interval:检查间隔时间
3.5 创建Controller
这里我们创建个QuartzController控制器,用来添加多个操作Quartz的请求方式,例如:删除定时任务、暂停定时任务、查看所有定时任务等。Swagger示例截图如下:
示例代码如下:仅供参考,有想法的也可以拓展或者修改。
@RestController @RequestMapping("/quartz") public class QuartzController { @Autowired private QuartzService quartzService; /** * 新增任务 */ @PostMapping("/insert") @ApiOperation(value = "新增任务", notes = "新增任务") public Boolean insertTask(@RequestBody QuartzModel quartzModel) { return quartzService.addJob(quartzModel); } /** * 暂停任务 */ @PostMapping("/pause") @ApiOperation(value = "暂停任务", notes = "暂停任务") public Boolean pauseTask(@RequestBody OperationModel operationModel) { return quartzService.pauseJob(operationModel); } /** * 恢复任务 */ @PostMapping("/resume") @ApiOperation(value = "恢复任务", notes = "恢复任务") public Boolean resumeTask(@RequestBody OperationModel operationModel) { return quartzService.resumeJob(operationModel); } /** * 删除任务 */ @PostMapping("/delete") @ApiOperation(value = "删除任务", notes = "删除任务") public Boolean deleteTask(@RequestBody OperationModel operationModel) { return quartzService.deleteJob(operationModel); } /** * 查看所有定时任务 */ @GetMapping("/query-all-job") @ApiOperation(value = "查看所有定时任务", notes = "查看所有定时任务") public List<Map<String, Object>> queryAllJob() { return quartzService.queryAllJob(); } /** * 查看正在运行的任务 */ @GetMapping("/query-all-running-job") @ApiOperation(value = "查看正在执行的任务", notes = "查看正在执行的任务") public List<Map<String, Object>> queryAllRunningJob() { return quartzService.queryAllRunningJob(); } }
3.6 编写QuartzService接口
这里将被定义所对应的接口,用于操作Quartz任务。
@Service public interface QuartzService { /** * 新增定时任务 */ Boolean addJob(QuartzModel quartzModel); /** * 暂停定时任务 */ Boolean pauseJob(OperationModel operationModel); /** * 继续定时任务 */ Boolean resumeJob(OperationModel operationModel); /** * 删除定时任务 */ Boolean deleteJob(OperationModel operationModel); /** * 查询所有计划中的任务列表 */ List<Map<String, Object>> queryAllJob(); /** * 查看正在运行的任务 */ List<Map<String, Object>> queryAllRunningJob(); }
3.7 实现定时任务执行器
这里我们手动写任务执行器,目的不仅是为了执行定时任务也想通过区分定时任务执行不同的逻辑,示例代码如下,仅供参考:
/** * 定时任务执行器 */ @Slf4j public class TaskDistributor extends QuartzJobBean { /** * 根据不同的jobDetail携带的type参数区分,执行不同业务 */ @Override protected void executeInternal(JobExecutionContext context) { synchronized (this) { log.info("开始执行定时任务..............."); try { //获取job中携带的内容并转成jobDetailModel对象 JobDetailModel jobDetailModel = (JobDetailModel) context.getJobDetail().getJobDataMap().get("jobDetailModel"); Integer type = jobDetailModel.getType(); String content = jobDetailModel.getContent(); //根据type执行不同的业务逻辑 switch (type) { case 1: System.out.println("task1即将被执行,content:" + content); //模拟处理逻辑 Thread.sleep(5000); log.info("---------task1定时任务执行结束----------"); break; case 2: System.out.println("task2即将被执行,content:" + content); //模拟处理逻辑 Thread.sleep(5000); log.info("----------task2定时任务结束---------"); break; default: log.info("---------定时任务执行开始----------"); log.info("---------定时任务执行结束----------"); break; } } catch (Throwable t) { log.error(t.getMessage(), t); } log.info("定时任务执行结束..............."); } } }
3.8 创建QuartzServiceImpl实现类
这里我们将QuartzService接口中的所有接口进行实现,示例代码如下:
@Slf4j @Service public class QuartzServiceImpl implements QuartzService { @Autowired private Scheduler scheduler; /** * 新增定时任务 */ @Override public Boolean addJob(QuartzModel quartzModel) { try { String cron = quartzModel.getCron(); //校验cron是否有效 boolean valid = CronUtils.isValid(cron); if (!valid) { return false; } JobDetail jobDetail = JobBuilder.newJob(TaskDistributor.class) .withIdentity(quartzModel.getJobName(), quartzModel.getJobGroup()) .build(); // 携带job内容 此处可以携带业务所需要的执行参数 JobDetailModel jobDetailModel = quartzModel.getJobData(); if (!Objects.isNull(jobDetailModel)) { jobDetail.getJobDataMap().put("jobDetailModel", jobDetailModel); } CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity(quartzModel.getTriggerName(), quartzModel.getTriggerGroup()) .startNow() .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); //把作业和触发器注册到任务调度中 scheduler.scheduleJob(jobDetail, trigger); //启用调度 scheduler.start(); log.info("---------定时任务成功添加进quartz队列中!----------"); } catch (Exception e) { e.printStackTrace(); } return true; } /** * 暂停定时任务 */ @Override public Boolean pauseJob(OperationModel operationModel) { try { scheduler.pauseJob(JobKey.jobKey(operationModel.getJobName(), operationModel.getJobGroup())); System.out.println("暂停定时任务成功"); } catch (Exception e) { e.printStackTrace(); } return true; } /** * 继续定时任务 */ @Override public Boolean resumeJob(OperationModel operationModel) { try { scheduler.resumeJob(JobKey.jobKey(operationModel.getJobName(), operationModel.getJobGroup())); System.out.println("恢复定时任务成功"); } catch (SchedulerException e) { e.printStackTrace(); } return true; } /** * 删除定时任务 */ @Override public Boolean deleteJob(OperationModel operationModel) { try { // TriggerKey 定义了trigger的名称和组别 ,通过任务名和任务组名获取TriggerKey TriggerKey triggerKey = TriggerKey.triggerKey(operationModel.getJobName(), operationModel.getJobGroup()); // 停止触发器 scheduler.resumeTrigger(triggerKey); // 移除触发器 scheduler.unscheduleJob(triggerKey); scheduler.deleteJob(JobKey.jobKey(operationModel.getJobName(), operationModel.getJobGroup())); System.out.println("删除定时任务成功"); } catch (SchedulerException e) { e.printStackTrace(); } return true; } /** * 查看所有定时任务 * * @return */ @Override public List<Map<String, Object>> queryAllJob() { List<Map<String, Object>> jobList = null; try { GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup(); Set<JobKey> jobKeys = scheduler.getJobKeys(matcher); jobList = new ArrayList<Map<String, Object>>(); for (JobKey jobKey : jobKeys) { List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey); for (Trigger trigger : triggers) { jobList.add(this.buildMap(jobKey, trigger)); } } } catch (SchedulerException e) { e.printStackTrace(); } return jobList; } /** * 返回指定的map集合 */ private Map<String, Object> buildMap(JobKey jobKey, Trigger trigger) throws SchedulerException { Map<String, Object> map = new HashMap<>(); map.put("jobName", jobKey.getName()); map.put("jobGroupName", jobKey.getGroup()); map.put("description", "触发器:" + trigger.getKey()); Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey()); map.put("jobStatus", triggerState.name()); if (trigger instanceof CronTrigger) { CronTrigger cronTrigger = (CronTrigger) trigger; String cronExpression = cronTrigger.getCronExpression(); map.put("jobTime", cronExpression); } return map; } /** * 获取所有正在运行的job * * @return */ @Override public List<Map<String, Object>> queryAllRunningJob() { List<Map<String, Object>> jobList = null; try { List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs(); jobList = new ArrayList<Map<String, Object>>(executingJobs.size()); for (JobExecutionContext executingJob : executingJobs) { JobDetail jobDetail = executingJob.getJobDetail(); JobKey jobKey = jobDetail.getKey(); Trigger trigger = executingJob.getTrigger(); jobList.add(this.buildMap(jobKey, trigger)); } } catch (SchedulerException e) { e.printStackTrace(); } return jobList; } }
其中addJob方法将一个定时任务添加到调度器中,其中jobName和jobGroupName表示任务的名称和分组名称,triggerName和triggerGroupName表示触发器的名称和分组名称,jobClass表示任务的类,cron表示定时任务执行的时间表达式。scheduler.scheduleJob(jobDetail, trigger);目的是把作业和触发器注册到任务调度中。
3.9 接口测试
重启项目后,我们来进行接口的请求测试,验证我们所添加的定时任务是否会执行且被新增入库持久化保存?就让我们拭目以待吧!
如下我直接通过Swagger在线接口文档作为测试,测试如下:
3.9.1 添加定时任务task1
具体请求参数附上:
{
"cron": "*/10 * * * * ? ",
"jobData": {
"content": "bug菌写定时任务(执行逻辑1)",
"type": 1
},
"jobGroup": "job_group_task1",
"jobName": "job_name_task1",
"triggerGroup": "trigger_group_task1",
"triggerName": "trigger_task1"
}
3.9.2 添加定时任务task2
具体请求参数附上:
{
"cron": "*/20 * * * * ? ",
"jobData": {
"content": "bug菌写定时任务(执行逻辑2)",
"type": 2
},
"jobGroup": "job_group_task2",
"jobName": "job_name_task2",
"triggerGroup": "trigger_group_task2",
"triggerName": "trigger_task2"
}
3.9.3 测试获取所有的定时任务
添加两个定时任务后,可以查看一下所有定时任务列表:
定时任务成功添加进quartz队列中!被插入的定时任务task1被成功执行且执行的type = 1的业务逻辑,打印了" task1即将被执行,content:bug菌写定时任务(执行逻辑1) "
示例截图如下:
定时任务成功添加进quartz队列中!被插入的定时任务task2被成功执行且执行的type = 2的业务逻辑,打印了" task2即将被执行,content:bug菌写定时任务(执行逻辑2) "
示例截图如下:
3.9.4 验证数据库是否被保存定时任务
我们再来检查一遍,查验下我们添加的两条定时任务是否被自动插入库中(持久化)。
在上面的两个添加任务的过程中,我们不仅将定时任务配置在了代码中,并把将它们持久化到数据库中。因为我们就是希望在应用程序重启后仍然能够保持任务状态,而Quartz就在内部直接将它们持久化到数据库中。
在Quartz中,定时任务数据是存储在三张表中的:
- QRTZ_JOB_DETAILS:保存JobDetail信息
- QRTZ_TRIGGERS:保存Trigger信息
- QRTZ_CRON_TRIGGERS:保存CronTrigger信息
当我们插入定时任务时,Quartz会自动将任务数据存储到这三张表中。我们插入了两个task定时任务后,我们完全不需要自主手插入定时任务,它会自动帮我们保存入库,同学们请看:
qrtz_cron_triggers表:
qrtz_job_details表:
qrtz_triggers表:
这样一来,哪怕是项目重启,定时任务也照常执行,不会因为没有被触发而停止执行定时任务。可以看到再次重启后的项目task1与task2都自动执行,保持了任务状态。
3.9.5 暂停定时任务
具体请求参数附上:
{
"jobGroup": "job_group_task1_jg",
"jobName": "job_name_task1_jn"
}
切记,我是为了区分便在参数pojo值上手动拼接后缀,比如:
可以看到数据入库的都被添加了对应的后缀名。
所以同学们在测试接口的时候,别忘了把后缀加上,或者你们也可以把我的这段去掉。
控制台查验是否被暂停了,大家请看:
经多次测试,task1任务确实暂停执行了。
3.9.6 继续定时任务
我们能暂停任务,就能把被暂停的任务恢复执行,这么我们来测试一下。
具体请求参数附上:
{
"jobGroup": "job_group_task1_jg",
"jobName": "job_name_task1_jn"
}
控制台查验被暂停的task1是否被恢复执行了,大家请看:
经5min观察,task1任务确实又重新执行了。你也可以调用【正在执行中的任务】接口作为辅助验证,不过就是要多次调用,毕竟在非执行间隔有点久,毕竟task1任务间隔10s执行一次,task2任务间隔20s执行一次。
3.9.7 查询正在执行的定时任务
同上述测试步骤,我们直接通过Swagger接口文档作为测试请求:
再次测试请求:
3.9.8 查看所有定时任务
这里我们可以查询出所有的定时任务,不区分是否正在被执行状态。所以始终会返回job表中的总条数。
同上述测试步骤,我们直接通过Swagger接口文档作为测试请求:
3.9.9 删除定时任务
终于写到最后一个删除模拟了,同上述测试步骤,我们直接通过Swagger接口文档作为测试请求:
具体请求参数附上:
{
"jobGroup": "job_group_task1_jg",
"jobName": "job_name_task1_jn"
}
我们从三个地方来验证,分别是接口查询所有任务数,控制台执行打印,及数据库job任务条数。
重新请求【接口查询所有定时任务数】截图如下:
数据库表qrtz_job_details中task1成功被移除了,证明task1这条定时任务确实被删除了。
好啦,以上针对Quartz的一系列操作及测试演示了,所任何不清楚的地方都可以通过评论区或私信我,我将第一时间给予你解惑。
4. 总结
本文介绍了SpringBoot集成quartz可以使定时任务的管理变得更加方便和可靠。通过将定时任务的信息保存到数据库中,我们可以轻松地对任务进行增删改查等操作。下面是一些主要的总结:
配置数据源和quartz的相关属性:需要在配置文件中指定数据源的连接信息和quartz的属性,包括JobStore类型、表前缀、是否自动启动等。
创建Job和Trigger:通过继承Quartz提供的Job接口,实现execute方法,即可定义一个Job。同时,通过指定Trigger的cron表达式或简单的时间间隔,可以定义一个Trigger,并将其与Job绑定起来。
通过schedulerFactoryBean创建Scheduler:在配置类中,通过注入schedulerFactoryBean,我们可以获取到一个Scheduler实例,然后可以通过Scheduler的API实现任务的管理,包括启动、暂停、恢复、删除等操作。
配置JobDetail、Trigger、CronTrigger的持久化:通过将JobDetail、Trigger、CronTrigger的信息保存到数据库中,我们可以实现对定时任务的持久化,即使应用程序重新启动或服务器崩溃,我们也可以恢复定时任务。在配置文件中,需要指定SchedulerFactoryBean的jobStore类型为JobStoreTX,同时配置DataSourceTransactionManager和QuartzTransactionManager。
使用QuartzJobBean和JobDataMap:在Job中,可以通过继承Quartz提供的QuartzJobBean,获取JobExecutionContext,并从中获取JobDataMap,通过JobDataMap可以传递参数,实现Job的可配置化。
总之,通过使用SpringBoot集成quartz,我们可以方便地管理定时任务,实现任务的持久化,同时对任务的管理变得更加灵活和可靠。
以上就是SpringBoot集成quartz实现定时任务的详细内容,更多关于SpringBoot定时任务的资料请关注脚本之家其它相关文章!