quartz原理
quartz
Quartz调度核心元素
Scheduler:任务调度器,是实际执行任务调度的控制器。在spring中通过SchedulerFactoryBean封装起来。
Trigger:触发器,用于定义任务调度的时间规则,有SimpleTrigger,CronTrigger,DateIntervalTrigger和NthIncludedDayTrigger,其中CronTrigger用的比较多,本文主要介绍这种方式。CronTrigger在spring中封装在CronTriggerFactoryBean中。
Calendar:它是一些日历特定时间点的集合。一个trigger可以包含多个Calendar,以便排除或包含某些时间点。JobDetail:用来描述Job实现类及其它相关的静态信息,如Job名字、关联监听器等信息。在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用。
Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。实现Job接口的任务,默认是无状态的,若要将Job设置成有状态的,在quartz中是给实现的Job添加@DisallowConcurrentExecution注解(以前是实现StatefulJob接口,现在已被Deprecated),在与spring结合中可以在spring配置文件的job detail中配置concurrent参数。
Quartz 集群
quartz集群是通过数据库表来感知其他的应用的,各个节点之间并没有直接的通信。只有使用持久的JobStore才能完成Quartz集群。
数据库表:有11张表,表信息如下
QRTZ_LOCKS就是Quartz集群实现同步机制的行锁表,包括以下几个锁:CALENDAR_ACCESS 、JOB_ACCESS、MISFIRE_ACCESS 、STATE_ACCESS 、TRIGGER_ACCESS
数据存储
Quartz 中的 trigger 和 job 需要存储下来才能被使用。Quartz 中有两种存储方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是将 trigger 和 job 存储在内存中,而 JobStoreSupport 是基于 jdbc 将 trigger 和 job 存储到数据库中。RAMJobStore 的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在通常应用中,都是使用 JobStoreSupport。
在 Quartz 中,JobStoreSupport 使用一个驱动代理来操作 trigger 和 job 的数据存储:StdJDBCDelegate。StdJDBCDelegate 实现了大部分基于标准 JDBC 的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展 StdJDBCDelegate 以实现这些特殊处理。
在Hap中使用的就是JobStoreSupport持久化存储,下面简单介绍一下常见几张表对应的关系:
这个界面的数据,存放在 qrtz_job_details 表中,但是这里还是有一些需要注意的地方。并不是所有的参数都在这个表中,比如 上次执行时间、下次执行时间、corn表达式等信息,实际上是存在别的表中,这个视 任务的类型来判断。当简单任务的时候,存在 qrtz_simple_triggers 表中;当 corn 任务的时候,存在 qrtz_triggers 表中。
不知道大家有没有想过这个问题,任务中的参数是放在哪里?我一开始以为会放到一张表里,最后发现参数其实是存在 qrtz_job_details 表里的一个字段上的。
是一个blob类型。
其实Hap在Job这一块封装的东西不算是很多。我们在开发时主要用到的就是一个 AbstractJob 类,之后会介绍到。还有就是任务运行记录,在Hap中是将任务执行记录存在 sys_job_running_info 表中,其实也是在 quratz 的监听器基础上做到的,之后会有介绍。
配置
applicationContext-job.xml
quartz.properties
#============================================================================
# Configure Scheduler
#============================================================================
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.instanceId = AUTO
#org.quartz.scheduler.rmi.export = false
#org.quartz.scheduler.rmi.proxy = false
#org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
org.quartz.scheduler.skipUpdateCheck = true
#============================================================================
# Configure ThreadPool
#============================================================================
#org.quartz.threadPooln.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
#org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
#============================================================================
# Configure JobStore
#============================================================================
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.maxMisfiresToHandleAtATime=1
#org.quartz.jobStore.misfireThreshold = 60000
#org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#org.quartz.scheduler.classLoadHelper.class=org.quartz.simpl.CascadingClassLoadHelper
#org.quartz.jobStore.useProperties = true
#============================================================================
# Microsoft SQL Server
#============================================================================
#org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.MSSQLDelegate
#org.quartz.jobStore.selectWithLockSQL=SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?
#============================================================================
# Configure Plugins
#============================================================================
#org.quartz.plugin.triggHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin
org.quartz.plugin.runningListener.class=com.hand.hap.job.plugin.RunningListenerPlugin
org.quartz.plugin.runningListener.LogRunningInfo=true
org.quartz.plugin.runningListener.mailTemplate=email_job_running_notification
quratz默认是使用RAMJobStore存储,这是将信息存储再内存中,这种方式有一个好处就是速度快,单缺点也很明显:不能持久化存储,如果系统出问题 就得从头开始,所以在项目上我没一般是采用持久化存储这种方式。如果需要数据库持久化存储,一般需要在quartz.properties配置文件中将存储方式改成jdbcjobstore,例如:
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
但在Hap中并没有直接在quartz.properties配置文件中定义数据库得连接方式,而是在 applicationContext-job.xml 配置文件中将数据源注入进去了。
因为spring 不能在quartz中注入bean的,所以这里自定义了 一个 jobFactory
package com.hand.hap.job;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
/**
* 支持 AutoWired.
*
*/
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
@Override
public Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}
applicationContextSchedulerContextKey 是 org.springframework.scheduling.quartz.SchedulerFactoryBean这个类中把spring上下 文以key/value的方式存放在了quartz的上下文中了,可以用applicationContextSchedulerContextKey所定义的key得到对应的spring上下文。
Listeners
这是Hap中Job模块的关键部分。Listeners是您创建的对象,用于根据调度程序中发生的事件执行操作。
TriggerListeners和JobListeners
您可能猜到,TriggerListeners接收到与触发器(trigger)相关的事件,JobListeners 接收与jobs相关的事件。
与触发相关的事件包括:触发器触发,触发失灵(在本文档的“触发器”部分中讨论),并触发完成(触发器关闭的作业完成)。
org.quartz.TriggerListener接口
public interface TriggerListener {
public String getName();
public void triggerFired(Trigger trigger, JobExecutionContext context);
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context);
public void triggerMisfired(Trigger trigger);
public void triggerComplete(Trigger trigger, JobExecutionContext context,
int triggerInstructionCode);
}
job相关事件包括:job即将执行的通知,以及job完成执行时的通知。
org.quartz.JobListener接口
public interface JobListener {
public String getName();
public void jobToBeExecuted(JobExecutionContext context);
public void jobExecutionVetoed(JobExecutionContext context);
public void jobWasExecuted(JobExecutionContext context,
JobExecutionException jobException);
}
使用TriggerListeners和JobListeners
要创建一个listener,只需创建一个实现org.quartz.TriggerListener和/或org.quartz.JobListener接口的对象。然后,listener在运行时会向调度程序注册,并且必须给出一个名称(或者,他们必须通过他们的getName()方法来宣传自己的名字)。
为了方便起见,实现这些接口,您的类也可以扩展JobListenerSupport类或TriggerListenerSupport类,并且只需覆盖您感兴趣的事件。
listener与调度程序的ListenerManager一起注册,并配有描述listener希望接收事件的job/触发器的Matcher。
Listener在运行时间内与调度程序一起注册,并且不与jobs和触发器一起存储在JobStore中。这是因为听众通常是与应用程序的集成点。因此,每次运行应用程序时,都需要重新注册该调度程序。
添加对特定job感兴趣的JobListener
scheduler.getListenerManager().addJobListener(myJobListener,KeyMatcher.jobKeyEquals(new JobKey(“myJobName”,“myJobGroup”)));
这将上面的例子变成这样:
scheduler.getListenerManager().addJobListener(myJobListener, jobKeyEquals(jobKey("myJobName", "myJobGroup")));
添加对特定组的所有job感兴趣的JobListener:
scheduler.getListenerManager().addJobListener(myJobListener, jobGroupEquals("myJobGroup"));
添加对两个特定组的所有job感兴趣的JobListener:
scheduler.getListenerManager().addJobListener(myJobListener, or(jobGroupEquals("myJobGroup"), jobGroupEquals("yourGroup")));
添加对所有job感兴趣的JobListener:
scheduler.getListenerManager().addJobListener(myJobListener, allJobs());
注册TriggerListeners的工作原理相同。
Quartz的大多数用户并不使用Listeners,但是当应用程序需求创建需要事件通知时不需要Job本身就必须明确地通知应用程序,这些用户就很方便。
SchedulerListeners
SchedulerListeners非常类似于TriggerListeners和JobListeners,除了它们在Scheduler本身中接收到事件的通知 (不一定与特定触发器(trigger)或job相关的事件)
与计划程序相关的事件包括:添加job/触发器,删除job/触发器,调度程序中的严重错误,关闭调度程序的通知等。
org.quartz.SchedulerListener接口:
public interface SchedulerListener {
public void jobScheduled(Trigger trigger);
public void jobUnscheduled(String triggerName, String triggerGroup);
public void triggerFinalized(Trigger trigger);
public void triggersPaused(String triggerName, String triggerGroup);
public void triggersResumed(String triggerName, String triggerGroup);
public void jobsPaused(String jobName, String jobGroup);
public void jobsResumed(String jobName, String jobGroup);
public void schedulerError(String msg, SchedulerException cause);
public void schedulerStarted();
public void schedulerInStandbyMode();
public void schedulerShutdown();
public void schedulingDataCleared();
}
SchedulerListeners注册到调度程序的ListenerManager。SchedulerListeners几乎可以实现任何实现org.quartz.SchedulerListener接口的对象。
添加SchedulerListener:
scheduler.getListenerManager().addSchedulerListener(mySchedListener);
删除SchedulerListener:
scheduler.getListenerManager().removeSchedulerListener(mySchedListener);
Hap中的Listener
Hap中得Listener如下:
主要包括三类:JobListener、TriggerListener、SchedulerListener ;
前面那三个 Default开头得Listener,没有什么实质性得内容,只是分别单纯得实现了 JobListener、TriggerListener、SchedulerListener 这个三个接口,这样接下来得哪几个Listener分别继承Default开头得那几个Listener,就不用是实现JobListener、TriggerListener、SchedulerListener 接口中所有得方法了。这其实就是适配器模式中的 接口适配器模式。至于这几个适配器里面的逻辑,就不过多说明了。可以简单看看 SchedulerRunningListener:
/*
* #{copyright}#
*/
package com.hand.hap.job.listener;
import com.hand.hap.job.service.IJobRunningInfoService;
import org.quartz.JobKey;
import org.quartz.listeners.SchedulerListenerSupport;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import com.hand.hap.job.dto.JobRunningInfoDto;
/**
* @author shiliyan
*
*/
public class SchedulerRunningListener extends SchedulerListenerSupport {
private static final String JOB_INFO_HAS_DELETED = "Job Info [{}.{}] has deleted.";
private static final String JOB_WAS_DELETED_FROM_SCHEDULER = "Job [{}.{}] was deleted from Scheduler.";
private final ApplicationContext applicationContext;
public SchedulerRunningListener(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/*
* (non-Javadoc)
*
* @see DefaultSchedulerListener#jobDeleted(org.quartz.JobKey)
*/
@Override
public void jobDeleted(JobKey jobKey) {
JobRunningInfoDto dto = new JobRunningInfoDto();
String group = jobKey.getGroup();
String name = jobKey.getName();
logInfo(JOB_WAS_DELETED_FROM_SCHEDULER, group, name);
dto.setJobName(name);
dto.setJobGroup(group);
deleteJobInfo(dto);
logInfo(JOB_INFO_HAS_DELETED, group, name);
}
private void deleteJobInfo(JobRunningInfoDto jobCreateDto) {
IJobRunningInfoService jobRunningInfoService = applicationContext.getBean(IJobRunningInfoService.class);
jobRunningInfoService.delete(jobCreateDto);
}
protected void logInfo(String info, Object... para) {
if (getLog().isInfoEnabled()) {
getLog().info(info, para);
}
}
protected void logInfo(String info) {
if (getLog().isInfoEnabled()) {
getLog().info(info);
}
}
}
有个 jobDeleted 方法,根据代码可以看出,在删除job的时候,会删除 sys_job_running_info 表中对应Job的执行记录。
插件SchedulerPlugin
在Hap 中的 quartz.properties 配置文件中,有这样一行配置
org.quartz.plugin.runningListener.class=com.hand.hap.job.plugin.RunningListenerPlugin
这其实就是quartz的插件功能。
在上面我没已经提到了Hap中对应Listener类型分别定义了几个Listener,但是发现还少了点什么。并没有在哪个地方看到JobRunningListener添加了需要监听的Job。其实这些动作都在 RunningListenerPlugin 中。
DefaultSchedulerPlugin 实现了 SchedulerPlugin, 没什么实质的内容
RunningListenerPlugin 继承了 DefaultSchedulerPlugin,并重写了 start 方法,然后将所有 Job和 JobRunningListener关联起来
至此,有关于Hap中Job的内容到此结束。下一章节将介绍 quartz 在 soringboot2 中的应用。
整合Springboot
上一章节介绍了Quartz在Hap中的应用(其实就是在spring 中的应用)。本章节介绍 Quartz 在 springboot2中的应用。
添加依赖
org.quartz-scheduler
quartz
org.quartz-scheduler
quartz-jobs
有可莪能会报一个 c3p0的错误,这时候需要添加一个依赖
com.mchange
c3p0
0.9.5.2
配置
///QuartzConfigure
package com.hand.sxy.config;
import org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
import javax.sql.DataSource;
/**
* @author pavan.solapure
*/
@Configuration
@EnableScheduling
public class QuartzConfigure {
/**
* 继承org.springframework.scheduling.quartz.SpringBeanJobFactory
* 实现任务实例化方式
*/
public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
/**
* 将job实例交给spring ioc托管
* 我们在job实例实现类内可以直接使用spring注入的调用被spring ioc管理的实例
*
* @param bundle
* @return
* @throws Exception
*/
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
/**
* 将job实例交付给spring ioc
*/
beanFactory.autowireBean(job);
return job;
}
}
/**
* 配置任务工厂实例
*
* @param applicationContext spring上下文实例
* @return
*/
@Bean
public JobFactory jobFactory(ApplicationContext applicationContext) {
/**
* 采用自定义任务工厂 整合spring实例来完成构建任务
* see {@link AutowiringSpringBeanJobFactory}
*/
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
/**
* 配置任务调度器
* 使用项目数据源作为quartz数据源
*
* @param jobFactory 自定义配置任务工厂
* @param dataSource 数据源实例
* @return
* @throws Exception
*/
@Bean(destroyMethod = "destroy", autowire = Autowire.NO)
public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
//将spring管理job自定义工厂交由调度器维护
schedulerFactoryBean.setJobFactory(jobFactory);
//设置覆盖已存在的任务
schedulerFactoryBean.setOverwriteExistingJobs(true);
//项目启动完成后,等待2秒后开始执行调度器初始化
schedulerFactoryBean.setStartupDelay(2);
//设置调度器自动运行
schedulerFactoryBean.setAutoStartup(true);
//设置数据源,使用与项目统一数据源
schedulerFactoryBean.setDataSource(dataSource);
//设置上下文spring bean name
schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
//设置配置文件位置
schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
return schedulerFactoryBean;
}
}
//quartz.properties
# 固定前缀org.quartz
# 主要分为scheduler、threadPool、jobStore、plugin等部分
#
#
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
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实例
# 并发个数
org.quartz.threadPool.threadCount = 5
# 优先级
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
org.quartz.jobStore.misfireThreshold = 5000
# 默认存储在内存中
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties = true
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = qzDS
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/vue?characterEncoding=utf8&autoReconnect=true&useSSL=false
org.quartz.dataSource.qzDS.user = root
org.quartz.dataSource.qzDS.password = root
org.quartz.dataSource.qzDS.maxConnections = 10
使用
//BasalJob
package com.hand.sxy.job;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
* @author spilledyear
*/
public interface BasalJob extends Job {
/**
* Job执行入口
*
* @param context
* @throws JobExecutionException
*/
@Override
void execute(JobExecutionContext context) throws JobExecutionException;
}
//HelloJob
package com.hand.sxy.job.example;
import com.hand.sxy.job.BasalJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
/**
* @author spilledyear
*/
public class HelloJob implements BasalJob {
private static Logger logger = LoggerFactory.getLogger(HelloJob.class);
public HelloJob() {
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
logger.info("Hello Job执行时间: " + new Date());
}
}
//JobController
package com.hand.sxy.job.controller;
import com.hand.sxy.job.BasalJob;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author spilledyear
*/
@RestController
@RequestMapping(value = "/job")
public class JobController {
private static Logger log = LoggerFactory.getLogger(JobController.class);
@Autowired
private Scheduler scheduler;
@RequestMapping(value = "/addjob", method = RequestMethod.GET)
public void addjob(@RequestParam(value = "jobGroup") String jobGroup,
@RequestParam(value = "jobName") String jobName,
@RequestParam(value = "jobClassName") String jobClassName,
@RequestParam(value = "cronExpression") String cronExpression) throws Exception {
addJob(jobGroup, jobName, jobClassName, cronExpression);
}
public void addJob(String jobGroup, String jobName, String jobClassName, String cronExpression) throws Exception {
// 启动调度器
scheduler.start();
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(jobGroup, jobName).build();
//表达式调度构建器(即任务执行的时间)
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobGroup, jobName)
.withSchedule(scheduleBuilder).build();
try {
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
System.out.println("创建定时任务失败" + e);
throw new Exception("创建定时任务失败");
}
}
.....
}
在前端请求 /job/addJob接口(其实已经集成到了那两个前后端分离的应用中,前端可视化管理Job)
后台日志
数据库数据
前端可视化管理