springboot整合Quartz持久化定时任务管理界面
此案例在天降风云博主的基础上进行了补充
Quartz提供两种基本作业存储类型。第一种类型叫做RAMJobStore,第二种类型叫做JDBC作业存储。在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失
比如我们希望安排一个执行100次的任务,如果执行到50次时系统崩溃了,系统重启时任务的执行计数器将从0开始。
如果确实需要持久化任务调度信息,Quartz允许你通过调整其属性文件,将这些信息保存到数据库中。使用数据库保存任务调度信息后,即使系统崩溃后重新启动,任务的调度信息将得到恢复。如前面所说的例子,执行50次崩溃后重新运行,计数器将从51开始计数。使用了数据库保存信息的任务称为持久化任务。
本文实例代码: https://github.com/haoxiaoyong1014/springboot-quartz
此案例是基于springboot2版本,相关依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.0.5.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
接下来是application.yml配置文件信息,主要针对数据库和mybatis进行的配置
server:
port: 12741
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:mappers/*Mapper.xml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
name: sv
url: jdbc:mysql://115.29.32.62:3306/quartz?useUnicode=true&characterEncoding=utf8
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
我们在 application.yml 同级目录下创建一个quartz.properties的文件,这个文件主要是对 Quartz 进行的配置,我把配置说明也写在了配置文件中,看着有点乱
# 固定前缀org.quartz
# 主要分为scheduler、threadPool、jobStore、plugin等部分
#
#
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
#如果您希望Quartz Scheduler通过RMI作为服务器导出本身,则将“rmi.export”标志设置为true。
#在同一个配置文件中为'org.quartz.scheduler.rmi.export'和'org.quartz.scheduler.rmi.proxy'指定一个'true'值是没有意义的,如果你这样做,'export '选项将被忽略
org.quartz.scheduler.rmi.export = false
#如果要连接(使用)远程服务的调度程序,则将“org.quartz.scheduler.rmi.proxy”标志设置为true。您还必须指定RMI注册表进程的主机和端口 - 通常是“localhost”端口1099。
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
# threadCount和threadPriority将以setter的形式注入ThreadPool实例
# 并发个数 如果你只有几个工作每天触发几次 那么1个线程就可以,如果你有成千上万的工作,每分钟都有很多工作 那么久需要50-100之间.
# 只有1到100之间的数字是非常实用的
org.quartz.threadPool.threadCount = 5
# 优先级 默认值为5
org.quartz.threadPool.threadPriority = 5
#可以是“true”或“false”,默认为false。
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
#在被认为“misfired”(失火)之前,调度程序将“tolerate(容忍)”一个Triggers(触发器)将其下一个启动时间通过的毫秒数。默认值(如果您在配置中未输入此属性)为60000(60秒)
org.quartz.jobStore.misfireThreshold = 5000
# 默认存储在内存中,RAMJobStore快速轻便,但是当进程终止时,所有调度信息都会丢失。
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#您需要为JobStore选择一个DriverDelegate才能使用。DriverDelegate负责执行特定数据库可能需要的任何JDBC工作。
# StdJDBCDelegate是一个使用“vanilla”JDBC代码(和SQL语句)来执行其工作的委托,用于完全符合JDBC的驱动程序
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#可以将“org.quartz.jobStore.useProperties”配置参数设置为“true”(默认为false),以指示JDBCJobStore将JobDataMaps中的所有值都作为字符串,
# 因此可以作为名称 - 值对存储而不是在BLOB列中以其序列化形式存储更多复杂的对象。从长远来看,这是更安全的,因为您避免了将非String类序列化为BLOB的类版本问题。
org.quartz.jobStore.useProperties=true
#表前缀
org.quartz.jobStore.tablePrefix = QRTZ_
#您需要设置JobStore应该使用哪个DataSource。
org.quartz.jobStore.dataSource = qzDS
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://115.29.32.62:3306/quartz?useUnicode=true&characterEncoding=utf8
org.quartz.dataSource.qzDS.user = root
org.quartz.dataSource.qzDS.password = root
org.quartz.dataSource.qzDS.maxConnections = 10
#设置为“true”以打开群集功能。如果您有多个Quartz实例使用同一组数据库表,则此属性必须设置为“true”,否则您将遇到破坏。
#org.quartz.jobStore.isClustered=false
如果这里看着有点乱或者不明白可以参考: https://www.w3cschool.cn/quartz_doc/quartz_doc-i7oc2d9l.html
着我们在com.example.quartz下新建一个名为SchedulerConfig.java的文件。在这个文件里,对刚才我们新建的quartz.properties文件进行读取
@Configuration
public class SchedulerConfig {
@Bean(name="SchedulerFactory")
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setQuartzProperties(quartzProperties());
return factory;
}
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
//在quartz.properties中的属性被读取并注入后再初始化对象
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
/**
* quartz初始化监听器
* 这个监听器可以监听到工程的启动,在工程停止再启动时可以让已有的定时任务继续进行。
* @return
*/
@Bean
public QuartzInitializerListener executorListener() {
return new QuartzInitializerListener();
}
/**
*
*通过SchedulerFactoryBean获取Scheduler的实例
*/
@Bean(name="Scheduler")
public Scheduler scheduler() throws IOException {
return schedulerFactoryBean().getScheduler();
}
}
实现
先来看Job类。首先设置一个BaseJob接口,用来继承Job类:
public interface BaseJob extends Job {
public void execute(JobExecutionContext context) throws JobExecutionException;
}
然后两个 Job继承 BaseJob
HelloJob
public class HelloJob implements BaseJob{
private static Logger log = LoggerFactory.getLogger(HelloJob.class);
public HelloJob() {
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("Hello Job执行时间: " + new Date());
}
}
NewJob
public class NewJob implements BaseJob{
private static Logger log = LoggerFactory.getLogger(NewJob.class);
public NewJob() {
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("Hello Job执行时间: " + new Date());
}
}
接下来我们看下 controller类
@RestController
@RequestMapping(value = "job")
public class JobController {
@Autowired
private IJobAndTriggerService iJobAndTriggerService;
//加入Qulifier注解,通过名称注入bean
@Autowired
@Qualifier("Scheduler")
private Scheduler scheduler;
@Autowired
private DateUnit dateUnit;
private static Logger log = LoggerFactory.getLogger(JobController.class);
/**
* 添加任务
*
* @param jobInfo
* @throws Exception
*/
@PostMapping(value = "/addjob")
public void addjob(@RequestBody JobInfo jobInfo) throws Exception {
if ("".equals(jobInfo.getJobClassName()) || "".equals(jobInfo.getJobGroupName()) || "".equals(jobInfo.getCronExpression())) {
return;
}
if (jobInfo.getTimeType() == null) {
addCronJob(jobInfo);
return;
}
addSimpleJob(jobInfo);
}
//CronTrigger
public void addCronJob(JobInfo jobInfo) throws Exception {
// 启动调度器
scheduler.start();
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(getClass(jobInfo.getJobClassName()).getClass()).
withIdentity(jobInfo.getJobClassName(), jobInfo.getJobGroupName())
.build();
//表达式调度构建器(即任务执行的时间)
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(jobInfo.getCronExpression());
//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().
withIdentity(jobInfo.getJobClassName(), jobInfo.getJobGroupName())
.withSchedule(scheduleBuilder)
.build();
try {
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
System.out.println("创建定时任务失败" + e);
throw new Exception("创建定时任务失败");
}
}
//Simple Trigger
public void addSimpleJob(JobInfo jobInfo) throws Exception {
// 启动调度器
scheduler.start();
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(getClass(jobInfo.getJobClassName()).getClass())
.withIdentity(jobInfo.getJobClassName(), jobInfo.getJobGroupName())
.build();
DateBuilder.IntervalUnit verDate = dateUnit.verification(jobInfo.getTimeType());
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity(jobInfo.getJobClassName(), jobInfo.getJobGroupName())
.startAt(futureDate(Integer.parseInt(jobInfo.getCronExpression()), verDate))
.forJob(jobInfo.getJobClassName(), jobInfo.getJobGroupName())
.build();
try {
scheduler.scheduleJob(jobDetail, simpleTrigger);
} catch (SchedulerException e) {
System.out.println("创建定时任务失败" + e);
throw new Exception("创建定时任务失败");
}
}
/**
* 暂停任务
*
* @param jobClassName
* @param jobGroupName
* @throws Exception
*/
@PostMapping(value = "/pausejob")
public void pausejob(@RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName) throws Exception {
jobPause(jobClassName, jobGroupName);
}
public void jobPause(String jobClassName, String jobGroupName) throws Exception {
scheduler.pauseJob(JobKey.jobKey(jobClassName, jobGroupName));
}
/**
* 恢复任务
*
* @param jobClassName
* @param jobGroupName
* @throws Exception
*/
@PostMapping(value = "/resumejob")
public void resumejob(@RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName) throws Exception {
jobresume(jobClassName, jobGroupName);
}
public void jobresume(String jobClassName, String jobGroupName) throws Exception {
scheduler.resumeJob(JobKey.jobKey(jobClassName, jobGroupName));
}
/**
* 更新任务
*
* @param jobClassName
* @param jobGroupName
* @param cronExpression
* @throws Exception
*/
@PostMapping(value = "/reschedulejob")
public void rescheduleJob(@RequestParam(value = "jobClassName") String jobClassName,
@RequestParam(value = "jobGroupName") String jobGroupName,
@RequestParam(value = "cronExpression") String cronExpression) throws Exception {
jobreschedule(jobClassName, jobGroupName, cronExpression);
}
public void jobreschedule(String jobClassName, String jobGroupName, String cronExpression) throws Exception {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
// 按新的trigger重新设置job执行
scheduler.rescheduleJob(triggerKey, trigger);
} catch (SchedulerException e) {
System.out.println("更新定时任务失败" + e);
throw new Exception("更新定时任务失败");
}
}
/**
* 删除任务
* 删除操作前应该暂停该任务的触发器,并且停止该任务的执行
*
* @param jobClassName
* @param jobGroupName
* @throws Exception
*/
@PostMapping(value = "/deletejob")
public void deletejob(@RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName) throws Exception {
jobdelete(jobClassName, jobGroupName);
}
public void jobdelete(String jobClassName, String jobGroupName) throws Exception {
scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName));
scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName));
scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName));
}
/**
* 查询任务
*
* @param pageNum
* @param pageSize
* @return
*/
@GetMapping(value = "/queryjob")
public Map<String, Object> queryjob(@RequestParam(value = "pageNum") Integer pageNum, @RequestParam(value = "pageSize") Integer pageSize) {
PageInfo<JobAndTrigger> jobAndTrigger = iJobAndTriggerService.getJobAndTriggerDetails(pageNum, pageSize);
Map<String, Object> map = new HashMap<String, Object>();
map.put("JobAndTrigger", jobAndTrigger);
map.put("number", jobAndTrigger.getTotal());
return map;
}
/**
* 根据类名称,通过反射得到该类,然后创建一个BaseJob的实例。
* 由于NewJob和HelloJob都实现了BaseJob,
* 所以这里不需要我们手动去判断。这里涉及到了一些java多态调用的机制
*
* @param classname
* @return
* @throws Exception
*/
public static BaseJob getClass(String classname) throws Exception {
Class<?> class1 = Class.forName(classname);
return (BaseJob) class1.newInstance();
}
}
你会看到addCronJob(jobInfo)
和addSimpleJob(jobInfo)
这两个方法,这就涉及到Simple Trigger和CronTrigger的区别
Simple Trigger:
SimpleTrigger可以满足的调度需求是:在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。比如,你有一个trigger,你可以设置它在2015年1月13日的上午11:23:54准时触发,或者在这个时间点触发,并且每隔2秒触发一次,一共重复5次。
根据描述,你可能已经发现了,SimpleTrigger的属性包括:开始时间、结束时间、重复次数以及重复的间隔。这些属性的含义与你所期望的是一致的,只是关于结束时间有一些地方需要注意。
重复次数,可以是0、正整数,以及常量SimpleTrigger.REPEAT_INDEFINITELY。重复的间隔,必须是0,或者long型的正数,表示毫秒。注意,如果重复间隔为0,trigger将会以重复次数并发执行(或者以scheduler可以处理的近似并发数)。
如果你还不熟悉DateBuilder,了解后你会发现使用它可以非常方便地构造基于开始时间(或终止时间)的调度策略。
endTime属性的值会覆盖设置重复次数的属性值;比如,你可以创建一个trigger,在终止时间之前每隔10秒执行一次,你不需要去计算在开始时间和终止时间之间的重复次数,只需要设置终止时间并将重复次数设置为REPEAT_INDEFINITELY(当然,你也可以将重复次数设置为一个很大的值,并保证该值比trigger在终止时间之前实际触发的次数要大即可)。
更多参见: https://www.w3cschool.cn/quartz_doc/quartz_doc-67a52d1f.html
CronTrigger
CronTrigger通常比Simple Trigger更有用,如果您需要基于日历的概念而不是按照SimpleTrigger的精确指定间隔进行重新启动的作业启动计划。
使用CronTrigger,您可以指定号时间表,例如“每周五中午”或“每个工作日和上午9:30”,甚至“每周一至周五上午9:00至10点之间每5分钟”和1月份的星期五“。
即使如此,和SimpleTrigger一样,CronTrigger有一个startTime,它指定何时生效,以及一个(可选的)endTime,用于指定何时停止计划。
Cron Expressions
Cron-Expressions用于配置CronTrigger的实例。Cron Expressions是由七个子表达式组成的字符串,用于描述日程表的各个细节。这些子表达式用空格分隔,并表示:
Seconds
Minutes
Hours
Day-of-Month
Month
Day-of-Week
Year (optional field)
一个完整的Cron-Expressions的例子是字符串“0 0 12?* WED“ - 这意味着”每个星期三下午12:00“。
Cron Expressions示例
CronTrigger示例1 - 创建一个触发器的表达式,每5分钟就会触发一次
“0 0/5 * * *?”
CronTrigger示例2 - 创建触发器的表达式,每5分钟触发一次,分钟后10秒(即上午10时10分,上午10:05:10等)。
“10 0/5 * * ?”
CronTrigger示例3 - 在每个星期三和星期五的10:30,11:30,12:30和13:30创建触发器的表达式。
“0 30 10-13? WED,FRI“
CronTrigger示例4 - 创建触发器的表达式,每个月5日和20日上午8点至10点之间每半小时触发一次。请注意,触发器将不会在上午10点开始,仅在8:00,8:30,9:00和9:30
“0 0/30 8-9 5,20 *?”
请注意,一些调度要求太复杂,无法用单一触发表示 - 例如“每上午9:00至10:00之间每5分钟,下午1:00至晚上10点之间每20分钟”一次。在这种情况下的解决方案是简单地创建两个触发器,并注册它们来运行相同的作业。表达式
你可以到 这里自动生成
更多参见: https://www.w3cschool.cn/quartz_doc/quartz_doc-lwuv2d2a.html
在 controller类中你会看到最后有这么一个方法:
public static BaseJob getClass(String classname) throws Exception {
Class<?> class1 = Class.forName(classname);
return (BaseJob) class1.newInstance();
}
注意这个方法,根据类名称,通过反射得到该类,然后创建一个BaseJob的实例。由于NewJob和HelloJob都实现了BaseJob,所以这里不需要我们手动去判断。这里涉及到了一些java多态调用的机制,
其他设计到的 dao层以及service层,工具类就不在这里展示了,因为篇幅有长,案例代码在我的github上.
我们可以看到static下的JobManager.html,这个是前端的一个简单的管理页面。Spring Boot的web工程中,静态页面可以放在static目录下。这里贴一下主要代码,完整代码在github上.
//从服务器读取数据
loadData: function(pageNum, pageSize){
this.$http.get('job/queryjob?' + 'pageNum=' + pageNum + '&pageSize=' + pageSize).then(function(res){
console.log(res)
this.tableData = res.body.JobAndTrigger.list;
this.totalCount = res.body.number;
},function(){
console.log('failed');
});
},
//单行删除
handleDelete: function(index, row) {
this.$http.post('job/deletejob',{"jobClassName":row.job_NAME,"jobGroupName":row.job_GROUP},{emulateJSON: true}).then(function(res){
this.loadData( this.currentPage, this.pagesize);
},function(){
console.log('failed');
});
},
//暂停任务
handlePause: function(index, row){
this.$http.post('job/pausejob',{"jobClassName":row.job_NAME,"jobGroupName":row.job_GROUP},{emulateJSON: true}).then(function(res){
this.loadData( this.currentPage, this.pagesize);
},function(){
console.log('failed');
});
},
//恢复任务
handleResume: function(index, row){
this.$http.post('job/resumejob',{"jobClassName":row.job_NAME,"jobGroupName":row.job_GROUP},{emulateJSON: true}).then(function(res){
this.loadData( this.currentPage, this.pagesize);
},function(){
console.log('failed');
});
},
//搜索
search: function(){
this.loadData(this.currentPage, this.pagesize);
},
//弹出对话框
handleadd: function(){
this.checkboxChange = true;
},
change: function(){
this.dialogFormVisibleChange = true;
},
//添加
add: function(){
this.$http.post('job/addjob',{"jobClassName":this.form.jobName,"jobGroupName":this.form.jobGroup,"cronExpression":this.form.cronExpression}).then(function(res){
this.loadData(this.currentPage, this.pagesize);
this.dialogFormVisibleChange = false;
this.checkboxChange = false;
},function(){
console.log('failed');
});
},
addSimTir: function () {
console.log(this.value4)
this.$http.post('job/addjob',{"jobClassName":this.form.jobName,"jobGroupName":this.form.jobGroup,"cronExpression":this.form.cronExpression,
"timeType":this.value4}).then(function(res){
this.loadData(this.currentPage, this.pagesize);
this.dialogFormVisibleChange = false;
this.checkboxChange = false;
},function(){
console.log('failed');
});
},
//更新
handleUpdate: function(index, row){
console.log(row)
this.updateFormVisible = true;
this.updateform.jobName = row.job_CLASS_NAME;
this.updateform.jobGroup = row.job_GROUP;
},
//更新任务
update: function(){
this.$http.post
('job/reschedulejob',
{"jobClassName":this.updateform.jobName,
"jobGroupName":this.updateform.jobGroup,
"cronExpression":this.updateform.cronExpression
},{emulateJSON: true}
).then(function(res){
this.loadData(this.currentPage, this.pagesize);
this.updateFormVisible = false;
},function(){
console.log('failed');
});
},
//每页显示数据量变更
handleSizeChange: function(val) {
this.pagesize = val;
this.loadData(this.currentPage, this.pagesize);
},
//页码变更
handleCurrentChange: function(val) {
this.currentPage = val;
this.loadData(this.currentPage, this.pagesize);
},
},
});
//载入数据
vue.loadData(vue.currentPage, vue.pagesize);
最终展示效果:
-
启动项目
-
输入 http://localhost:12741/JobManager.html 会看到这个页面,在这里可以看到你的所有任务
- 添加任务
在添加任务的时候 任务名称
指定类名即可,通过反射得到该类, 由于NewJob和HelloJob都实现了BaseJob,
所以这里不需要我们手动去判断。这里涉及到了一些java多态调用的机制
任务名称
例如: HelloJob
当然持久化也是将任务添加到数据库的,sql脚本
在项目的跟目录下 quartz (官方提供的sql脚本),
在添加任务中的表达式
你可以到 这里自动生成
增加 SimpleTrigger
点击确定:
参考:
https://blog.****.net/u012907049/article/details/73801122
https://www.w3cschool.cn/quartz_doc/quartz_doc-1xbu2clr.html