-
quartz的学习与使用
- 概念与应用场景描述
- quartz的核心概念
- quartz的入门基础使用小demo
- quartz为JobDetail和Trigger自定义数据,并在Job中获取
- quartz的配置文件
- quartz整合Spring完成任务的调度(基于配置的方式)
- Spring整合quartz(基于MethodInvokingJobDetailFactoryBean配置JobDetail)
- Spring整合quartz完成持久化
- spring整合quartz完成集群的配置
- Job的并发执行问题以及Job状态问题
- misfire失火策略
- 当执行的Job任务出现异常
- quartz的任务中断,类似于Thread的中断
- quartz的日期排除
- 监听器
- quartz中插件的使用
- Quartz的其他一些细节
quartz的学习与使用
概念与应用场景描述
quartz的概念
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由java语言开发,支持分布式、集群部署,且具有丰富的调度方式。
quartz应用场景描述
quartz主要是用于进行定时任务的执行。
- 例如哔哩哔哩的视频播放量需要每隔30秒刷新一次。则30秒执行一次刷新播放量的定时任务(这是打个比方)
- 服务器需要每隔1天,发送一个邮件给开发者,内容为当前服务器的日志信息。这个发送邮件的操作,在该场景下就也是一个定时任务。
quartz的核心概念
Job对象
实现该接口后,重写其中的execute方法,该方法就是我们需要执行的任务(业务逻辑代码)。
JobDetail对象
该对象的作用是绑定一个Job对象,然后对其进行描述,主要有如下的几个属性
- name 任务的名称,默认是一串随机的字符串
- group 任务所在的组,默认为字符串:deafult
- description 任务描述
- jobClass 任务类,用于指定Job对象
- jobDataMap 用于传递一些自定义的任务参数
Tigger对象
该对象是一个触发器,在绑定任务后,设置任务的执行时间,结束时间,执行间隔,执行频率等。
该触发器主要有4中指定触发规则的Tigger。
- SimpleTrigger 简单的Tigger触发器
- CronTrigger 使用cron表达式的触发器
- DataIntervalTrigger 按照时间间隔进行任务触发的一个触发器。可以解决misfired失火问题。
- NthIncludedTrigger 已经不建议使用了,具体细节可以自行查阅资料
Scheduler对象
该对象用于绑定JobDetail和Tigger,以此来进行任务的调度(执行)。
quartz官方提供了2种创建Scheduler对象的工厂类
- DirectSchedulerFactory(硬编码方式创建Scheduler对象,不推荐)
- StdSchedulerFactory(使用配置文件来创建Scheduler对象,推荐)
quartz的入门基础使用小demo
创建一个jar类型的maven项目,并引入如下的依赖
quartz使用的是slf4j日志门面,但是还没有具体的实现。
所以这里引入Logback的日志实现,在pom文件中新增依赖。
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
在resources目录下创建一个logback.xml文件(设置一下日志的输出格式)
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false" >
<!-- 日志级别 -->
<property name="logLevel" value="INFO"/>
<!-- 配置输出格式 -->
<property name="logPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger - %msg%n"/>
<!-- 控制台打印日志的相关配置 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志格式 -->
<encoder>
<charset>UTF-8</charset>
<pattern>${logPattern}</pattern>
</encoder>
</appender>
<root level="${logLevel}">
<appender-ref ref="STDOUT" />
</root>
</configuration>
创建一个Job任务,输出当前时间
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String currentDate = LocalDateTime.now().format(dtf);
System.out.println(currentDate);
}
}
创建一个QuartzTest测试类。
import com.it.job.DateJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
// 创建任务详情JobDetail,绑定任务,并且指定任务的名称和组名
JobDetail jobDetail = JobBuilder.newJob(DateJob.class)
.withIdentity("job1","g1")
.build();
// 创建一个触发器,绑定JobDetail
// 设置了触发器的名称、组名、以及立即开始、并且指定触发规则为5秒执行一次,无限重复
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("t1","g1")
.forJob(jobDetail)
.startNow()
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
.build();
// 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
}
运行结果
本次小demo的几个需要注意的事项
-
Scheduler每次进行调度任务时,会通过jobDetail来找到对应的Job,每次执行任务时,都会new一个新的Job实例。所以Job默认是无状态的。
-
Scheduler对象进行任务调度时,可以只指定一个Trigger触发器,而不指定JobDetail,前提是JobDetail已经交由Scheduler管理
quartz为JobDetail和Trigger自定义数据,并在Job中获取
修改QuartzTest文件中的代码,存储自定义数据
在Job任务中获取到对应的自定义数据的第一种方式,通过JobExecutionContext
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 获取当前的JobDetail中的自定义数据
String JobDetailMyKey1 = jobExecutionContext.getJobDetail().getJobDataMap().getString("myKey1");
System.out.println("JobDetail中的Key1:" + JobDetailMyKey1);
// 获取当前的JobDetail中的自定义数据
String TriggerMyKey1 = jobExecutionContext.getTrigger().getJobDataMap().getString("myKey1");
System.out.println("Trigger中的Key1:" + TriggerMyKey1);
// 获取JobDetail与Trigger合并后的自定义数据,如果两者key有冲突,则会使用Trigger的自定义数据覆盖JobDetail的自定义数据
String myKey1 = jobExecutionContext.getMergedJobDataMap().getString("myKey1");
System.out.println("合并后出现重复的key的数据:" + myKey1);
}
在Job任务中获取到对应的自定义数据的第二种方式,依赖注入
运行效果
quartz的配置文件
-
quartz应用程序默认在启动时,会在类路径查找quartz.properties文件作为配置文件的加载。
-
但是如果类路径下没有该配置文件,则会默认加载在org/quartz/文件夹下的quartz.properties文件
-
默认的配置文件如下,我只是加了个不检查版本更新的配置。
# 如果使用集群模式,则该实例名则是区分集群的唯一标识
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
# 如果希望Quartz Scheduler通过RMI作为服务器导出本身,则为true。
org.quartz.scheduler.rmi.export = false
# 如果要连接(使用)远程服务的调度程序,则为true。还必须指定RMI注册表进程的主机和端口 - 通常是“localhost”端口1099
org.quartz.scheduler.rmi.proxy = false
# 设置这项为true使我们在调用job的execute()之前能够开始一个UserTransaction。
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
# 程序运行期间,跳过quartz版本更新的检查
org.quartz.scheduler.skipUpdateCheck=true
# 指定的线程池
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
# 线程的数量
org.quartz.threadPool.threadCount = 10
# 线程优先级
org.quartz.threadPool.threadPriority = 5
# 自创建父线程
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
# 最大能忍受的触发超时时间
org.quartz.jobStore.misfireThreshold = 60000
# 数据保存方式为内存中
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
quartz整合Spring完成任务的调度(基于配置的方式)
创建一个jar类型的maven项目,引入spring与quartz的jar包
<properties>
<spring.version>5.2.3.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring ioc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 注意别少了这个包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<!-- 提供一些quartz的支持工具,例如发送邮件的定时任务 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>
</dependencies>
创建一个Job任务
- 其实这里也可以将实现Job接口替换成继承QuartzJobBean
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;
public class MySimpleJob implements Job{
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 获取到Spring容器
ApplicationContext ac = (ApplicationContext) jobExecutionContext.getMergedJobDataMap().get("applicationContext");
System.out.println(ac);
// 获取到自定义的参数
String key1 = jobExecutionContext.getJobDetail().getJobDataMap().getString("key1");
System.out.println("key1的值为:" + key1);
}
}
在类路径下配置Spring的配置文件
- 创建JobDetail,绑定任务Job,注意,这里使用的是JobDetailFactoryBean
- 创建Trigger触发器
- 创建Scheduler调度器,对任务进行调度
<!-- 定义JobDetail ,这里使用JobDetailFactoryBean,也可以使用MethodInvokingJobDetailFactoryBean ,配置类似-->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<!-- 指定job的名称 -->
<property name="name" value="job1"/>
<!-- 指定job的分组 -->
<property name="group" value="group1"/>
<!-- 指定具体的job类 -->
<property name="jobClass" value="com.it.job.MySimpleJob"/>
<!-- 必须设置为true,如果为false,当没有活动的触发器与之关联时会在调度器中会删除该任务 -->
<property name="durability" value="true"/>
<!-- 指定spring容器的key,如果不设定在job中的jobDataMap中是获取不到spring容器的 -->
<property name="applicationContextJobDataKey" value="applicationContext"/>
<!-- 定义自定义的数据 -->
<property name="jobDataAsMap">
<map>
<entry key="key1" value="value1"/>
</map>
</property>
</bean>
<!-- 配置Trigger触发器 -->
<!-- 第一种 SimpleTriggerBean,只支持按照一定频度调用任务,如每隔30分钟运行一次。配置方式如下: -->
<!-- <bean id="simpleTrigger"
class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail"></property>
<property name="startDelay" value="3000"></property>
<property name="repeatInterval" value="2000"></property>
</bean> -->
<!-- 第二种 CronTriggerBean,支持到指定时间运行一次,每隔2秒执行一次。配置方式如下: -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail" />
<!-- <!—每天12:00运行一次 —> -->
<property name="cronExpression" value="0/2 * * * * ?" />
</bean>
<!-- 配置调度工厂scheduler,进行任务的调度 -->
<bean id="schedulerFactoryBean" lazy-init="true"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<!-- <ref bean="testTrigger"></ref> -->
<ref bean="cronTrigger" />
</list>
</property>
</bean>
解决在Job示例中使用@Autowired注解,注入失败的问题
创建一个Spring的工具类,实现ApplicationContextAware接口
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringContextUtil.applicationContext == null) {
SpringContextUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过bean的id属性获取bean
* @param name bean的id
* @param <T> 强制转换的类型
* @return
*/
public static <T> T getBean(String name) {
return (T) getApplicationContext().getBean(name);
}
/**
* 通过类型获取到bean
* @param clazz 该类型的Class
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过bean的id以及类型获取到bean
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
Spring的配置文件记得添加context:component-scan 扫描该类
使用方式(在JOB中)
- 前提是这个id为user的bean已经加入到了spring的容器中
public User user = SpringContextUtil.getBean("user");
Test测试类(其实就是加载一下Spring的配置文件)
public class MyTest {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
}
}
Spring整合quartz(基于MethodInvokingJobDetailFactoryBean配置JobDetail)
首先随意修改一下Job类为如下,无需再实现Job
import com.it.entity.User;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
//@Component ,由于在spring配置文件中配置过了,所以不用配置该注解
public class MySimpleJob2{
@Autowired
private User user;
@Autowired
public Scheduler scheduler;
public void show() {
// 这是自动注入的User类,我这里随便的User类为: class User{}
System.out.println("依赖注入的User对象: " + user);
// 当前项目的Scheduler对象
System.out.println("Scheduler对象:" + scheduler);
}
}
修改Spring的配置文件,主要修改如下内容
<bean id="user" class="com.it.entity.User"/>
<!-- 定义需要执行的job类 -->
<bean id="myJob" class="com.it.job.MySimpleJob2" />
<!-- 定义JobDetail 使用MethodInvokingJobDetailFactoryBean -->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 指定job的名称 -->
<property name="name" value="job1"/>
<!-- 指定job的分组 -->
<property name="group" value="group1"/>
<!-- 指定具体的job类 -->
<property name="targetObject" ref="myJob"/>
<!-- 指定job类中的方法为定时任务 -->
<property name="targetMethod" value="show" />
</bean>
Spring整合quartz完成持久化
创建一个jar类型的maven项目,引入如下的依赖
<dependencies>
<!-- spring ioc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 注意别少了这个包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!-- 数据库连接 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<!-- 提供一些quartz的支持工具,例如发送邮件的定时任务 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>
<!-- 非必须,如果想要查看quartz日志,可以引入,然后在类路径下配置一个logback.xml文件-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
在类路径下添加spring的配置文件,主要是为了配置数据源以及Scheduler
<!-- 引入配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<context:component-scan base-package="com.it"/>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置调度工厂scheduler,进行任务的调度 -->
<bean name="quartzScheduler" lazy-init="false"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 可以通过scheduler.getContext().get("applicationContextKey") 获取到Spring容器对象 -->
<property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
<!-- 是否自动启动 -->
<property name="autoStartup" value="true" />
<!-- 其实也可以直接指定quartz的配置文件,然后在配置文件中配置,本质是一样的 -->
<property name="quartzProperties">
<props>
<prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
<prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop>
</props>
</property>
</bean>
编写一个简单的job任务
public class MyJob1 extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("你好呀,我很好,是的");
}
}
编写一个测试类(我这里是通过spring加载bean的机制进行的任务调度,其实可以通过使用junit整合spring来进行单元测试)
@Component
public class JobInit {
@Autowired
private Scheduler scheduler;
@PostConstruct
public void init() throws Exception {
// 创建任务详情JobDetail,绑定任务,并且指定任务的名称和组名
JobDetail jobDetail = JobBuilder.newJob(MyJob1.class)
.withIdentity("job4","g1")
.build();
// 创建一个触发器,绑定JobDetail
// 设置了触发器的名称、组名、以及立即开始、并且指定触发规则为5秒执行一次,无限重复
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("t3","g1")
.forJob(jobDetail)
.startNow()
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
.build();
// 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
}
spring整合quartz完成集群的配置
首先创建一个jar类型的maven项目,引入如上spring整合quartz进行持久化的依赖
在类路径下添加spring的配置文件,内容和刚刚spring整合quartz持久化差不多,只是修改了如下片段
<!-- 配置调度工厂scheduler,进行任务的调度 -->
<bean name="quartzScheduler" lazy-init="false"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 可以通过scheduler.getContext().get("applicationContextKey") 获取到Spring容器对象 -->
<property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
<!-- 是否自动启动 -->
<property name="autoStartup" value="true" />
<!-- 其实也可以直接指定quartz的配置文件,然后在配置文件中配置,本质是一样的 -->
<property name="quartzProperties">
<props>
<!-- 持久化配置-->
<prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
<prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop>
<!-- 集群配置 -->
<prop key="org.quartz.jobStore.isClustered">true</prop>
<!-- 注意这个相当于集群的名字,而quartz的集群是通过所连接的数据库来判定的 -->
<prop key="org.quartz.scheduler.instanceName">myCluster</prop>
<prop key="org.quartz.scheduler.instanceId">AUTO</prop>
</props>
</property>
</bean>
创建一个任务类
public class MyJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 获取当前系统时间
String thisDateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
System.out.println(thisDateTime + " 我是一个任务,任务名为:" + jobExecutionContext.getJobDetail().getKey().getName());
}
}
创建一个Test测试类,用于加载任务(只运行一次)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
public class JTest {
@Autowired
private Scheduler scheduler;
@Test
public void addJob() throws Exception{
JobDetail job1 = JobBuilder.newJob(MyJob.class).withIdentity("job1").build();
JobDetail job2 = JobBuilder.newJob(MyJob.class).withIdentity("job2").build();
JobDetail job3 = JobBuilder.newJob(MyJob.class).withIdentity("job3").build();
Trigger trigger1 = TriggerBuilder.newTrigger().forJob(job1).withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)).build();
Trigger trigger2 = TriggerBuilder.newTrigger().forJob(job2).withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)).build();
Trigger trigger3 = TriggerBuilder.newTrigger().forJob(job3).withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)).build();
scheduler.scheduleJob(job1, trigger1);
scheduler.scheduleJob(job2, trigger2);
scheduler.scheduleJob(job3, trigger3);
scheduler.start();
}
}
启动项目(对了,记得把当前项目复制一份,然后2台都启动)
然后就会看到集群环境下任务的执行了。至于时间,大概是因为刚好卡点。
- 可以看出一台集群的机器执行了job1和job3,那么另一台肯定就只会在同一时间执行了job2
值得一提的quartz集群分布式问题。
想一下,我们配置redis集群时,是不是需要指定集群的ip地址,而我们使用quartz却并没有指定。
- 原因其实是因为,quartz的集群是根据连接的数据库来分配的。
- 也就是说,只要不同机器的quartz连接的是同一个数据库,就会根据数据库中的信息来确认集群关系。
Job的并发执行问题以及Job状态问题
@DisallowConcurrentExecution禁止并发执行的注解作用
- 作用:顾名思义,禁止并发执行,也就是说一个Trigger在触发任务时,如果上一次任务没有执行完,则等待上一次任务执行完毕后再进行触发执行。
示例(不添加该注解时)
- 任务类
import org.quartz.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class StatusJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
// 演示并发问题,主要是任务执行的时间为3秒,但是间隔执行时间为2秒,这个时候会出现的问题有哪些
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(nowTime + " 我是job任务,嘿嘿");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 测试方法
import com.it.job.StatusJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class MyTest {
public static void main(String[] args) throws Exception {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
JobDetail jobDetail = JobBuilder
.newJob(StatusJob.class)
.build();
Trigger trigger = TriggerBuilder
.newTrigger()
.startNow()
.forJob(jobDetail)
.withSchedule(
SimpleScheduleBuilder.repeatSecondlyForever(2)
)
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
}
- 执行结果,从结果可以看出,我们一个任务需要执行3秒,但是执行的间隔依然是2秒一次。
- 问题分析:如果我们第二次任务的执行,需要用到第一次任务执行后传递的数据,那么就会出现数据不一致问题。所以quartz提供了该注解解决此问题。
- 问题分析:如果我们第二次任务的执行,需要用到第一次任务执行后传递的数据,那么就会出现数据不一致问题。所以quartz提供了该注解解决此问题。
示例(添加该注解时)
- 只是在Job任务类上添加上@DisallowConcurrentExecution注解,其他的未作改动
- 执行结果,可以看出任务是挨个执行的,没有并发现象
@PersistJobDataAfterExecution注解实现Job的有状态(其实就是任务之间的数据传递)
前提概要,一些注意事项
-
由于Scheduler在帮助执行我们的任务时,每一次通过Trigger触发任务时,都会初始化一个Job,所以导致Job任务之间的数据不共享。
-
虽然我们可以通过静态变量来解决此问题,但是quartz为我们提供了@PersistJobDataAfterExecution注解来帮我们实现了Job任务之间的数据共享。主要是通过JobDetail的JobDataMap来完成数据的共享。
-
在Job类上加上该注解时,JobDetail中的JobDataMap中的数据就可以传递给下一个任务,否则无法传递
-
注意一件事:Trigger中的JobDataMap数据并不会进行传递。
示例(未添加注解)
- Job任务类
import org.quartz.*;
public class StatusJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 演示数据共享问题
// 获取自定义的count数据
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
int count = jobDataMap.getInt("count");
System.out.println("当前Job中的count的值为:" + count);
// 将JobDataMap中的count数据 加1
jobDataMap.put("count", ++count);
}
}
- 运行测试类(侧重JobDetail中添加的自定义数据count)
import com.it.job.StatusJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class MyTest {
public static void main(String[] args) throws Exception {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
JobDetail jobDetail = JobBuilder
.newJob(StatusJob.class)
.usingJobData("count",1)
.build();
Trigger trigger = TriggerBuilder
.newTrigger()
.startNow()
.forJob(jobDetail)
.withSchedule(
SimpleScheduleBuilder.repeatSecondlyForever(2)
)
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
}
- 运行结果,可以看出数据是不共享的
示例(添加了该注解在Job类上时)
- 运行结果
2个注解配合使用的注意点
- 如果使用了@PersistJobDataAfterExecution注解,那么就要考虑是否使用@DisallowConcurrentExecution注解。
- 因为@PersistJobDataAfterExecution注解只有在当前任务执行完毕后数据才会生效。
- 例如:如果第一个任务还没执行完(数据还没重新更新),这个时候并发执行了第二次该任务,那么就会出现取不到第一次任务触发时更新的数据。
- 因此使用@PersistJobDataAfterExecution时需要考虑搭配@DisallowConcurrentExecution
misfire失火策略
在讲失火策略之前,需要讲一个quartz的配置
# 最大能忍受的触发超时时间,单位为毫秒,默认为60000(60秒)
# 当任务执行时间到了时,由于某些原因导致的任务没有按时执行,并且超过了最大的触发超时时间,则认定为该任务失火
# 例如任务本应该在 09:00执行,但是却超过了默认的失火时间60秒都没有触发执行,也就是在09:01时还没有执行,则该任务失火
# 这里我为了演示效果改为1000毫秒 (1秒)
org.quartz.jobStore.misfireThreshold = 1000
注意一点: 如果没有超过失火时间(也就是任务没有失火),那么scheduler则会在可触发任务的同一时间直接执行未触发(多个)的任务。
创建一个Job
public class SimpleJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(nowTime + " 这是一条普通的任务");
}
}
创建测试类(使用默认)
package com.it.test;
import com.it.job.SimpleJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;
import java.util.Calendar;
/**
* @author dengqixing
* @date 2021/8/28
*/
public class MyTest {
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
JobDetail jobDetail = JobBuilder.newJob(SimpleJob.class).build();
// 使任务在前3秒执行。
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.SECOND,calendar.get(Calendar.SECOND) - 5);
Date date = calendar.getTime();
System.out.println(date);
System.out.println(new Date());
Trigger trigger = TriggerBuilder
.newTrigger()
.startAt(date)
// 每2秒执行一次,重复3次,所以就是执行4次,
// 由于这里每2秒执行一次,所以按照如上设 置的前5秒为开始时间,则会失火3次
.withSchedule(
SimpleScheduleBuilder.repeatSecondlyForever(2).withRepeatCount(3)
)
.forJob(jobDetail)
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
}
-- 默认失火策略(不理睬任务的失火,从当前时间重新执行任务)
记住一点,失火策略的分类,SimpleScheduleBuilder中有6种,CronScheduleBuilder有3种
SimpleScheduleBuilder中有6种(简单讲几种)
withMisfireHandlingInstructionFireNow 不管任务失火几次,只补一次,然后正常执行
withMisfireHandlingInstructionIgnoreMisfires 有机会就一瞬间执行完所有失火的任务(忽略掉失火,直接执行)
withMisfireHandlingInstructionNextWithExistingCount 只执行未失火的任务,失火的任务直接抛弃掉了
withMisfireHandlingInstructionNowWithExistingCount 与默认一致。(不理睬任务的失火,从当前时间重新执行任务)
当执行的Job任务出现异常
默认处理情况
默认即便任务抛出异常了,那么也会当作一次正确的任务触发,不会影响接下来的任务触发
当任务出现异常时,解决问题并重新调度一次任务
首先编写一个Job任务
@PersistJobDataAfterExecution
public class SimpleJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
try {
// 获取自定义数据boolean类型的flag
if (context.getJobDetail().getJobDataMap().getBoolean("flag")) {
System.out.println(nowTime + " 这是一条普通的任务");
} else {
throw new Exception("flag不为true");
}
} catch (Exception e) {
// 解决掉异常问题,记得别忘了修改job的状态(添加注解)
context.getJobDetail().getJobDataMap().put("flag", true);
// 重新调用一次任务
JobExecutionException jobExecutionException = new JobExecutionException(e);
// 立即执行一次
jobExecutionException.setRefireImmediately(true);
// 抛出该quartz提供的异常
throw jobExecutionException;
}
}
}
Test测试类
public class MyTest {
public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
JobDetail jobDetail = JobBuilder
.newJob(SimpleJob.class)
.usingJobData("flag",false)
.build();
Trigger trigger = TriggerBuilder
.newTrigger()
.startNow()
// 每2秒执行一次,一共4次
.withSchedule(
SimpleScheduleBuilder.repeatSecondlyForever(2).withRepeatCount(3)
)
.forJob(jobDetail)
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
}
运行结果
当任务出现异常时,取消之后任务的执行
// 取消所有关联了该Job任务的Trigger的触发
jobExecutionException.setUnscheduleFiringTrigger(true);
// 取消正在触发该任务的Trigger的触发
jobExecutionException.setUnscheduleFiringTrigger(true);
quartz的任务中断,类似于Thread的中断
首先编写一个job,实现InterruptableJob接口
public class MyInterruptJob implements InterruptableJob {
private boolean interruptFlag;
@Override
public void interrupt() throws UnableToInterruptJobException {
interruptFlag = true;
System.out.println("该任务已经被中断");
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
for (int i = 0; i < 5; i++) {
if (!interruptFlag) {
System.out.println("i等于: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
编写一个测试类
public class InterruptTest {
public static void main(String[] args) throws SchedulerException, InterruptedException {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
JobDetail jobDetail = JobBuilder
.newJob(MyInterruptJob.class)
.build();
Trigger trigger = TriggerBuilder
.newTrigger()
.startNow()
.forJob(jobDetail)
.build();
scheduler.scheduleJob(jobDetail, trigger);
// 注意这里睡了2秒,然后中断了任务
Thread.sleep(2000);
// 在这儿的作用就是中断第一次任务触发
scheduler.interrupt(jobDetail.getKey());
}
}
运行结果
注意了,interrupt方法只能中断当前正在触发的任务
quartz的日期排除
为什么需要进行日期排除?因为有些时间我们不需要进行任务的触发,例如节假日不上班呐之类的。
首先需要一个Job任务
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("Job任务执行了");
}
}
编写测试类,对了,这里提醒一下,进行日期排除日历是可以相互关联的,通过setBaseCalendar方法
记住步骤
- 创建日历
- 添加日历的排除规则
- 将日历添加到scheduler中
- 在trigger创建时指定日历
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
// 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
// 创建一个日历,这个是quartz提供的,包含月和日,不包含年,还有另外几个就不做阐述了
AnnualCalendar annualCalendar = new AnnualCalendar();
// 创建需要被排除的日历 第一个参数为年,其实可以随意填,因为AnnualCalendar 不包含年,这里主要是想要表示为8月28日
Calendar instance = new GregorianCalendar(2005,Calendar.AUGUST,28);
// 设置需要排除的日历
annualCalendar.setDayExcluded(instance,true);
// 将该日历添加到当前的Scheduler中
scheduler.addCalendar("holiday",annualCalendar,true,false);
// 创建任务详情JobDetail,绑定任务,并且指定任务的名称和组名
JobDetail jobDetail = JobBuilder.newJob(MyJob.class).build();
// 创建一个触发器,绑定JobDetail
// 设置了触发器的名称、组名、以及立即开始、并且指定触发规则为5秒执行一次,无限重复
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.startNow()
// 每10秒执行一次
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10))
.modifiedByCalendar("holiday")
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
}
可以实现日期排除的5种日历
监听器
监听器的分类
-
JobListener
-
public void jobToBeExecuted() 在job任务触发前执行
-
public void jobExecutionVetoed() 在job任务被Trigger的监听器拒绝执行时触发,后续会讲到,当然,执行该方法的时候,就不会再执行该监听器的另外2个方法。
-
public void jobWasExecuted() Job任务执行后执行
-
-
TriggerListener
-
public void triggerFired()当与监听器相关联的Trigger被触发,Job上的execute()方法将被执行时,Scheduler就调用该方法。该方法在vetoJobExecution方法之后。
-
public boolean vetoJobExecution() 如果返回true,则当前触发任务的execute方法不执行
-
public void triggerMisfired() 当该触发任务失火时,调用该方法
-
public void triggerComplete() 当任务的execute方法执行完毕后调用该方法
-
-
SchedulerListener
-
jobScheduled方法:用于添加部署一个Trigger
-
jobUnscheduled方法:用于卸载一个Trigger触发器
-
triggerFinalized方法:当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。
-
triggersPaused方法:Scheduler 调用这个方法是发生在一个 Trigger 或 Trigger 组被暂停时。假如是 Trigger 组的话,triggerName 参数将为 null。
-
triggersResumed方法:Scheduler 调用这个方法是发生成一个 Trigger 或 Trigger 组从暂停中恢复时。假如是 Trigger 组的话,假如是 Trigger 组的话,triggerName 参数将为 null。参数将为 null。
-
jobsPaused方法:当一个或一组 JobDetail 暂停时调用这个方法。
-
jobsResumed方法:当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。
-
schedulerError方法:在 Scheduler 的正常运行期间产生一个严重错误时调用这个方法。
-
schedulerStarted方法:当Scheduler 开启时,调用该方法
-
schedulerInStandbyMode方法: 当Scheduler处于StandBy模式时,调用该方法
-
schedulerShutdown方法:当Scheduler停止时,调用该方法
-
schedulingDataCleared方法:当Scheduler中的数据被清除时,调用该方法。
-
监听器的基础使用,这里以JobListener做演示
定义一个job
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("Job任务执行了");
}
}
编写一个监听器
public class MyJobListener implements JobListener {
@Override
public String getName() {
return "我是个假的监听器哦";
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
System.out.println("job执行前");
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
System.out.println("job被listener拒绝执行时执行");
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
System.out.println("job执行后");
}
}
测试类
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
// 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job2","group1").build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.startNow()
// 每10秒执行一次
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10))
.build();
// 添加监听器
JobListener jobListener = new MyJobListener();
scheduler.getListenerManager().addJobListener(jobListener);
// 部署到scheduler中
scheduler.scheduleJob(jobDetail, trigger);
// 开启任务调度
scheduler.start();
}
}
提醒一下,添加监听器的时候,可以设置匹配的规则,在添加监听器的时有第二个参数Matcher。
例如:
-
NameMatcher 根据名称匹配,其中有根据job的名称,或是trigger的名称
-
GroupMacher 根据组进行匹配,同样有job和trigger
-
EverythingMatcher 匹配全部的job或是匹配全部的trigger
-
AndMatcher 将两个Matcher相结合,并且是and条件
-
OrMatcher 将两个Matcher相结合,并且是or条件
-
NotMatcher 表示 非 的意思,也就是要求匹配不满足该条件的
quartz中插件的使用
在配置文件中配置插件即可,配置的插件必须是实现了SchedulerPlugin接口的类
quartz为我们提供了几个
使用插件的方法
在quartz的配置文件中进行如下配置,这个插件是quartz提供的,用job的日志
# 配置插件,这个myJobHistory名称是自定义的
org.quartz.plugin.myJobHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin
# 配置该插件的其他属性,至于配置嘛,可以在具体的插件类中找到(成员属性)
org.quartz.plugin.triggerHistory.name=heiheihei
对了,有一个XMLSchedulingDataProcessorPlugin插件,可以通过读取配置文件完成Trigger和JobDetail的装载。
Quartz的其他一些细节
如果需要进行线程的隔离,例如有3个监控任务,而其他的都是正常任务
3个监控任务肯定是要定时执行的。
但是如果其他的正常任务占用了所有的线程,或者是执行时间过长。可能会导致监控任务的失火。
那么这个时候,就可以创建2个Scheduler。每个Scheduler使用一个配置文件
JobDetail中的requestRecovery()方法
1、必须在持久化的方式下运行。
2、效果其实就是如果本次任务的执行,并没有执行完毕,而服务器宕机了,那么当服务器启动后,是否继续执行之前没有执行完毕的任务。
Trigger的优先级
想一个场景,如果只有3个线程,而同一时间有5个任务需要被触发。那么quartz该如何选择先执行谁。
答:按照当前Trigger的优先级。
源码中有一句话:
比较触发器的下一次触发时间的比较器,换句话说,根据最早的下一次触发时间对它们进行排序。 如果触发次数相同,则触发器按优先级排序(最高值在前),如果优先级相同,则按key排序。
quartz的服务端与客户端
服务端配置
服务端:负责执行任务
添加如下配置
# 开启远程方法调用
org.quartz.scheduler.rmi.export: true
# 暴露的地址
org.quartz.scheduler.rmi.registryHost:localhost
# 暴露的端口
org.quartz.scheduler.rmi.registryPort: 1099
# 是否要注册
org.quartz.scheduler.rmi.createRegistry: true
启动服务的测试类
public class ServerTest {
public static void main(String[] args) throws SchedulerException {
// 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
Scheduler scheduler = new StdSchedulerFactory("server.properties").getScheduler();
// 开启任务调度
scheduler.start();
}
}
客户端的配置
客户端:负责分配任务
添加如下配置
# 开启远程方法代理
org.quartz.scheduler.rmi.proxy: true
# 需要注册到的远程地址
org.quartz.scheduler.rmi.registryHost:localhost
# 需要注册到的远程地址中暴露的端口
org.quartz.scheduler.rmi.registryPort: 1099
- 服务启动类,这里的这个Job类自己随便写一个就好
public class ClientTest {
public static void main(String[] args) throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(MyJob.class).build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.startNow()
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
.build();
Scheduler scheduler = new StdSchedulerFactory("client.properties").getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
}