Quartz完全开发手册(一篇学会Quartz所有知识点)

目录

一、Quartz概念

1.1、Quartz介绍

1.2、使用场景

1.3、特点

二、Quartz运行环境

三、Quartz设计模式

四、Quartz学习的核心概念

4.1、任务Job

4.2、触发器Trigger

4.3、调度器Scheduler

五、Quartz的体系结构与工作流程

5.1、体系结构

5.2、工作流程

六、Quartz的几个常用API

七、Quartz的使用步骤

7.1、准备工作

7.2、引入Quartz的jar包

7.3、入门案例

7.4、Job和JobDetail介绍

7.5、JobExecutionContext介绍

7.6、JobDataMap介绍

7.7、有状态的Job和无状态的Job

7.8、Trigger介绍

7.9、SimpleTrigger触发器

7.10、CronTrigger触发器

八、配置、资源SchedulerFactory

九、Quartz.properties

十、Quartz监听器

10.1、概念

10.2、JobListener介绍

10.3、TriggerListener介绍

10.4、SchedulerListener介绍

十一、JobStore 作业存储

11.1、RAMJobStore介绍

11.2、JDBCJobStore介绍

11.2.1、使用示例

11.2.2、注意事项

十二、springboot 集成

12.1、添加依赖

12.2、使用示例

12.2.1、手动执行

12.2.2、自动执行

十三、集群模式

13.1、集群模式介绍

13.2、配置集群

13.3、注意事项 


一、Quartz概念

1.1、Quartz介绍

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。

Quartz是开源且具有丰富特性的"任务调度库",能够集成于任何的java应用,小到独立的应用,大至电子商业系统。Quartz能够创建亦简单亦复杂的调度,以执行上十、上百,甚至上万的任务。任务job被定义为标准的java组件,能够执行任何你想要实现的功能。quartz调度框架包含许多企业级的特性,如JTA事务、集群的支持。

简而言之,Quartz就是基于java实现的任务调度框架,用于执行你想要执行的任何任务。

1.2、使用场景

持久性任务 - 就是保持调度定时的状态;

任务管理 - 对调度任务进行有效的管理;

当遇到以下问题时:

自动关闭30分钟未支付的订单

与第三方公司对账业务

数据统计,比如博客系统统计日粉丝数,日阅读量等

活动开始和结束通知;

想在每月25号,自动还款;

每周或者每月的提醒事项,比如周总结或者月总结;

像这种某个时间点执行任务,或者每隔一段时间重复执行任务,都可以用Quartz实现

1.3、特点

强大的调度功能,例如丰富多样的调度方法,可以满足各种常规和特殊需求;

灵活的应用方式,例如支持任务调度和任务的多种组合,支持调度数据的多种存储方式(DB,RAM等);

支持分布式集群,在被Terracotta收购之后,在原来基础上进行了进一步的改造。

二、Quartz运行环境

  • Quartz 可以运行嵌入在另一个独立式应用程序

  • Quartz 可以在应用程序服务器(或servlet容器)内被实例化,并且参与事务

  • Quartz 可以作为一个独立的程序运行(其自己的Java虚拟机内),可以通过RMI使用

  • Quartz 可以被实例化,作为独立的项目集群(负载平衡和故障转移功能),用于作业的执行

三、Quartz设计模式

  • Builder模式

  • Factory模式

  • 组件模式

  • 链式编程

四、Quartz学习的核心概念

4.1、任务Job

Job就是你想要实现的任务类,每一个Job必须实现org.quartz.job接口,且只需实现接口定义的execute()方法。

4.2、触发器Trigger

Trigger为你执行任务的触发器,比如你想每天定时3点发送一份统计邮件,Trigger将会设置3点进行执行该任务。Trigger主要包含两种SimpleTrigger和CronTrigger两种。关于二者的区别的使用场景,后续的课程会进行讨论。

4.3、调度器Scheduler

Scheduler为任务的调度器,它会将任务job及触发器Trigger整合起来,负责基于Trigger设定的时间来执行Job。

五、Quartz的体系结构与工作流程

5.1、体系结构

5.2、工作流程

如上所示,使用 Quartz 的工作流程也很简单,大致如下:

  1. 首页基于 Job 接口定义你的作业 JobDetail 实例和触发器 Trigger 实例对象

  2. 将定义的作业和触发器实例对象通过调度器 scheduleJob,开始调度执行

  3. 调度器启动工作线程开始执行 JobDetail 实例的 execute 方法内容

  4. 任务运行时所需信息通过,JobExecutionContext 对象传递到工作线程,也可以在多个工作线程中跨线程传递

示意图:

六、Quartz的几个常用API

以下是Quartz编程API几个重要接口,也是Quartz的重要组件

  • Scheduler 用于与调度程序交互的主程序接口。 Scheduler 调度程序-任务执行计划表,只有安排进执行计划的任务Job(通过scheduler.scheduleJob方法安排进执行计划),当它预先定义的执行时间到了的时候(任务触发trigger),该任务才会执行。

  • Job 我们预先定义的希望在未来时间能被调度程序执行的任务类,我们可以自定义。

  • JobDetail 使用JobDetail来定义定时任务的实例,JobDetail实例是通过JobBuilder类创建的。

  • JobDataMap 可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用 其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数 据的方法。

  • Trigger 触发器,Trigger对象是用来触发执行Job的。当调度一个job时,我们实例一个触发器然后调整它的属性来满足job执行的条件。表明任务在什么时候会执行。定义了一个已经被安排的任务将会在什么时候执行的时间条件,比如每2秒就执行一次。

  • JobBuilder -用于声明一个任务实例,也可以定义关于该任务的详情比如任务名、组名等,这个声明的实例将会作为一个实际执行的任务。

  • TriggerBuilder 触发器创建器,用于创建触发器trigger实例。

  • JobListener、TriggerListener、SchedulerListener监听器,用于对组件的监听。

七、Quartz的使用步骤

7.1、准备工作

建立Maven工程

7.2、引入Quartz的jar包

<dependencies>
		<dependency>
			<groupId>org.quartz-scheduler</groupId>
			<artifactId>quartz</artifactId>
			<version>2.3.0</version>
		</dependency>
        <dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.7.5</version>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.15</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.5.1</version>
				<configuration>
					<target>1.8</target>
					<source>1.8</source>
				</configuration>
			</plugin>
		</plugins>
	</build>

导入log4j.properties日志文件

### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=c:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### set log levels - for more verbose logging change 'info' to 'debug' ###

log4j.rootLogger=info, stdout

7.3、入门案例

(1)创建HelloJob任务类

HelloJob.java

// 定义任务类
public class HelloJob implements Job {

	@Override
	public void execute(JobExecutionContext arg0) throws JobExecutionException {
		// 定义时间
		Date date = new Date();
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateString = dateFormat.format(date);
		// 定义工作任务内容
		System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
	}
}

(2)创建任务调度类HelloSchedulerDemo

HelloSchedulerDemo.java

public class HelloSchedulerDemo {

	public static void main(String[] args) throws Exception {
		// 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("job1", "group1") // 定义该实例唯一标识
                .build();

        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 定义该实例唯一标识
                .startNow()  // 马上执行
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
        .repeatSecondlyForever(5)) // 每5秒执行一次   
                .build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
	}
}

(3)实现效果

7.4、Job和JobDetail介绍

  • Job:工作任务调度的接口,任务类需要实现该接口。该接口中定义execute方法,类似JDK提供的TimeTask类的run方法。在里面编写任务执行的业务逻辑。

  • Job实例在Quartz中的生命周期:每次调度器执行Job时,它在调用execute方法前会创建一个新的Job实例,当调用完成后,关联的Job对象实例会被释放,释放的实例会被垃圾回收机制回收。

  • JobDetail:JobDetail为Job实例提供了许多设置属性,以及JobDetaMap成员变量属性,它用来存储特定Job实例的状态信息,调度器需要借助JobDetail对象来添加Job实例。

  • JobDetail重要属性:name、group、jobClass、jobDataMap

        

JobDetail job = JobBuilder.newJob(HelloJob.class)
        .withIdentity("job1", "group1") // 定义该实例唯一标识,并指定一个组。
        .build();
  
System.out.println("name:"+job.getKey().getName());
System.out.println("group:"+job.getKey().getGroup());
System.out.println("jobClass:"+job.getJobClass().getName());

7.5、JobExecutionContext介绍

  • 当Scheduler调用一个Job,就会将JobExecutionContext传递给Job的execute()方法;

  • Job能通过JobExecutionContext对象访问到Quartz运行时候的环境以及Job本身的明细数据。

7.6、JobDataMap介绍

(1)使用Map获取

  • 在进行任务调度时,JobDataMap存储在JobExecutionContext中 ,非常方便获取。

  • JobDataMap可以用来装载任何可序列化的数据对象,当job实例对象被执行时这些参数对象会传递给它。

  • JobDataMap实现了JDK的Map接口,并且添加了非常方便的方法用来存取基本数据类型。

HelloSchedulerDemo.java

// 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
    JobDetail job = JobBuilder.newJob(HelloJob.class)
    		.withIdentity("job1", "group1") // 定义该实例唯一标识
    		.usingJobData("message", "打印日志")
    		.build();
	
    // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
    Trigger trigger = TriggerBuilder.newTrigger()
    		.withIdentity("trigger1", "group1") // 定义该实例唯一标识
    		.startNow()  // 马上执行
    		//.startAt(triggerStartTime) // 针对某个时刻执行
    		.withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .repeatSecondlyForever(5)) // 每5秒执行一次   
    		.usingJobData("message", "simple触发器")
    		.build();

HelloJob.java

JobKey jobKey = context.getJobDetail().getKey();
System.out.println("工作任务名称:"+jobKey.getName()+";工作任务组:"+jobKey.getGroup());
System.out.println("任务类名称(带包名):"+context.getJobDetail().getJobClass().getName());
System.out.println("任务类名称:"+context.getJobDetail().getJobClass().getSimpleName());
System.out.println("当前任务执行时间:"+context.getFireTime());
System.out.println("下一任务执行时间:"+context.getNextFireTime());

TriggerKey triggerKey = context.getTrigger().getKey();
System.out.println("触发器名称:"+triggerKey.getName()+";触发器组:"+triggerKey.getGroup());

JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
String jobMessage = jobDataMap.getString("message");
System.out.println("任务参数消息值:"+jobMessage);

JobDataMap triggerDataMap = context.getTrigger().getJobDataMap();
String triggerMessage = triggerDataMap.getString("message");
System.out.println("触发器参数消息值:"+triggerMessage);

(2)Job实现类中添加setter方法对应JobDataMap的键值,Quartz框架默认的JobFactory实现类在初始化job实例对象时会自动地调用这些setter方法。

HelloJob.java

private String message;

public void setMessage(String message) {
	this.message = message;
}

这里注意:如果遇到同名的key,Trigger中的.usingJobData("message", "simple触发器")会覆盖JobDetail中的.usingJobData("message", "打印日志")。

7.7、有状态的Job和无状态的Job

@PersistJobDataAfterExecution注解的使用

有状态的Job可以理解为多次Job调用期间可以持有一些状态信息,这些状态信息存储在JobDataMap中,而默认的无状态job每次调用时都会创建一个新的JobDataMap。

(1)修改HelloSchedulerDemo.java。添加.usingJobData("count", 0),表示计数器。

JobDetail job = JobBuilder.newJob(HelloJob.class)
    		.withIdentity("job1", "group1") // 定义该实例唯一标识
    		.usingJobData("message", "打印日志")
    		.usingJobData("count", 0)
    		.build();

(2)修改HelloJob.java

添加count的setting和getting方法。

private Integer count;
public void setCount(Integer count) {
	this.count = count;
}

在public void execute(JobExecutionContext context) throws JobExecutionException的方法中添加。

++count;
System.out.println("count数量:"+count);
context.getJobDetail().getJobDataMap().put("count", count);

HelloJob类没有添加@PersistJobDataAfterExecution注解,每次调用时都会创建一个新的JobDataMap。不会累加;

HelloJob类添加@PersistJobDataAfterExecution注解,多次Job调用期间可以持有一些状态信息,即可以实现count的累加。

7.8、Trigger介绍

Quartz有一些不同的触发器类型,不过,用得最多的是SimpleTrigger和CronTrigger。

(1)jobKey

表示job实例的标识,触发器被触发时,该指定的job实例会被执行。

(2)startTime

表示触发器的时间表,第一次开始被触发的时间,它的数据类型是java.util.Date。

(3)endTime

指定触发器终止被触发的时间,它的数据类型是java.util.Date。

案例:

HelloJobTrigger.java

// 定义任务类
public class HelloJobTrigger implements Job {

	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		// 定义时间
		Date date = new Date();
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateString = dateFormat.format(date);
		
		// 定义工作任务内容
		System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
		
		// 获取jobKey、startTime、endTime
		Trigger trigger = context.getTrigger();
		System.out.println("jobKey的标识:"+trigger.getJobKey().getName()+";jobKey的组名称:"+trigger.getJobKey().getGroup());
		System.out.println("任务开始时间:"+dateFormat.format(trigger.getStartTime())+";任务结束时间:"+dateFormat.format(trigger.getEndTime()));
	}
}

HelloSchedulerDemoTrigger.java

public class HelloSchedulerDemoTrigger {

	public static void main(String[] args) throws Exception {
		// 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        
        // 定义日期
        Date startDate = new Date();
        // 启动任务,任务在当前时间3秒后执行
        startDate.setTime(startDate.getTime()+3000);
        // 定义日期
        Date endDate = new Date();
        // 结束任务,任务在当前时间10秒后停止
        endDate.setTime(endDate.getTime()+10000);

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJobTrigger.class)
        		.withIdentity("job1", "group1") // 定义该实例唯一标识
        		.usingJobData("message", "打印日志")
        		.build();

        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
        		.withIdentity("trigger1", "group1") // 定义该实例唯一标识
        		.startAt(startDate)
        		.endAt(endDate)
        		.withSchedule(SimpleScheduleBuilder.simpleSchedule()
                    .repeatSecondlyForever(5)) // 每5秒执行一次   
        		.usingJobData("message", "simple触发器")
        		.build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);
        
        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
	}
}

7.9、SimpleTrigger触发器

SimpleTrigger对于设置和使用是最为简单的一种 QuartzTrigger。

它是为那种需要在特定的日期/时间启动,且以一个可能的间隔时间重复执行 n 次的 Job 所设计的。

案例一:表示在一个指定的时间段内,执行一次作业任务;

HelloJobSimpleTrigger.java

// 定义任务类
public class HelloJobSimpleTrigger implements Job {

@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		// 定义时间
		Date date = new Date();
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateString = dateFormat.format(date);
		
		// 定义工作任务内容
		System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
	}
}

HelloSchedulerDemoSimpleTrigger.java

public class HelloSchedulerDemoSimpleTrigger {

	public static void main(String[] args) throws Exception {
		// 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        
        // 定义日期
        Date startDate = new Date();
        // 启动任务,任务在当前时间3秒后执行
        startDate.setTime(startDate.getTime()+3000);

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJobSimpleTrigger.class)
        		.withIdentity("job1", "group1") // 定义该实例唯一标识
        		.build();

        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
        		.withIdentity("trigger1", "group1") // 定义该实例唯一标识
        		.startAt(startDate)
        		.build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);
        
        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
	}
}

案例二:或在指定的时间间隔内多次执行作业任务。

修改HelloSchedulerDemoSimpleTrigger.java

// 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity("trigger1", "group1") // 定义该实例唯一标识
            .startAt(startDate)
            .withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5)
    		.withRepeatCount(2)) // 每5秒执行一次,连续执行3次后停止,默认值是0
            .build();

案例三:指定任务的结束时间。

修改HelloSchedulerDemoSimpleTrigger.java

// 定义日期
Date endDate = new Date();
// 启动结束,任务在当前时间10秒后停止
endDate.setTime(endDate.getTime()+10000);

// 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
JobDetail job = JobBuilder.newJob(HelloJobSimpleTrigger.class)
		.withIdentity("job1", "group1") // 定义该实例唯一标识
		.build();

// 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
Trigger trigger = TriggerBuilder.newTrigger()
		.withIdentity("trigger1", "group1") // 定义该实例唯一标识
		.startAt(startDate)
		.endAt(endDate)
		.withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5)
		.withRepeatCount(3)) // 每5秒执行一次,连续执行3次后停止
		.build();

需要注意的点

  • SimpleTrigger的属性有:开始时间、结束时间、重复次数和重复的时间间隔。

  • 重复次数属性的值可以为0、正整数、或常量 SimpleTrigger.REPEAT_INDEFINITELY。

  • 重复的时间间隔属性值必须为大于0或长整型的正整数,以毫秒作为时间单位,当重复的时间间隔为0时,意味着与Trigger同时触发执行。

  • 如果有指定结束时间属性值,则结束时间属性优先于重复次数属性,这样的好处在于:当我们需要创建一个每间隔10秒钟触发一次直到指定的结束时间的 Trigger,而无需去计算从开始到结束的所重复的次数,我们只需简单的指定结束时间和使用REPEAT_INDEFINITELY作为重复次数的属性 值即可。

7.10、CronTrigger触发器

如果你需要像日历那样按日程来触发任务,而不是像SimpleTrigger 那样每隔特定的间隔时间触发,CronTriggers通常比SimpleTrigger更有用,因为它是基于日历的作业调度器。

使用CronTrigger,你可以指定诸如“每个周五中午”,或者“每个工作日的9:30”或者“从每个周一、周三、周五的上午9:00到上午10:00之间每隔五分钟”这样日程安排来触发。甚至,象SimpleTrigger一样,CronTrigger也有一个startTime以指定日程从什么时候开始,也有一个(可选的)endTime以指定何时日程不再继续。

(1)Cron Expressions——Cron 表达式

Cron表达式被用来配置CronTrigger实例。Cron表达式是一个由7个子表达式组成的字符串。每个子表达式都描述了一个单独的日程细节。这些子表达式用空格分隔,分别表示:

  1. Seconds 秒

  2. Minutes 分钟

  3. Hours 小时

  4. Day-of-Month 月中的天

  5. Month 月

  6. Day-of-Week 周中的天

  7. Year (optional field) 年(可选的域)

取值:

单个子表达式可以包含范围或者列表。例如:前面例子中的周中的天这个域(这里是"WED")可以被替换为"MON-FRI", "MON, WED, FRI"或者甚至"MON-WED,SAT"。

所有的域中的值都有特定的合法范围,这些值的合法范围相当明显,例如:秒和分域的合法值为0到59,小时的合法范围是0到23,Day-of-Month中值得合法凡范围是1到31,但是需要注意不同的月份中的天数不同。月份的合法值是1到12。或者用字符串JAN,FEB MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV 及DEC来表示。Days-of-Week可以用1到7来表示(1=星期日)或者用字符串SUN, MON, TUE, WED, THU, FRI 和SAT来表示.

示例:

"0 0 10,14,16 * * ?" 每天上午10点,下午2点,4点
"0 0/30 9-17 * * ?"   朝九晚五工作时间内每半小时,从0分开始每隔30分钟发送一次
"0 0 12 ? * WED" 表示每个星期三中午12点 
"0 0 12 * * ?" 每天中午12点触发 
"0 15 10 ? * *" 每天上午10:15触发 
"0 15 10 * * ?" 每天上午10:15触发 
"0 15 10 * * ? *" 每天上午10:15触发 
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发 
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发 
"0 0/55 14 * * ?" 在每天下午2点到下午2:55期间,从0开始到55分钟触发 
"0 0/55 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的,从0开始到55分钟触发 
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发 
"0 15 10 15 * ?" 每月15日上午10:15触发 
"0 15 10 L * ?" 每月最后一日的上午10:15触发 
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发 

案例:

HelloJobCronTrigger.java

// 定义任务类
public class HelloJobCronTrigger implements Job {

@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		// 定义时间
		Date date = new Date();
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateString = dateFormat.format(date);
		
		// 定义工作任务内容
		System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
	}
}

HelloSchedulerDemoCronTrigger.java

public class HelloSchedulerDemoCronTrigger {

	public static void main(String[] args) throws Exception {
		// 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJobCronTrigger.class)
        		.withIdentity("job1", "group1") // 定义该实例唯一标识
        		.build();

        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
        		.withIdentity("trigger1", "group1") // 定义该实例唯一标识
        		.withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * 6 4 ?"))// 定义表达式
        		.build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);
        
        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
	}
}

小提示:

  • ‘L’和‘W’可以一起使用。(企业可用在工资计算)

  • ‘#’可表示月中第几个周几。(企业可用在计算母亲节和父亲节)

  • 周字段英文字母不区分大小写,例如MON==mon。

  • 利用工具,在线生成。

八、配置、资源SchedulerFactory

Quartz以模块方式构架,因此,要使它运行,几个组件必须很好的咬合在一起。幸运的是,已经有了一些现存的助手可以完成这些工作。

所有的Scheduler实例由SchedulerFactory创建

Quartz的三个核心概念:调度器、任务、触发器,三者之间的关系是:

大家都知道,一个作业,比较重要的三个要素就是Schduler,jobDetail,Trigger;而Trigger
对于job而言就好比一个驱动器;没有触发器来定时驱动作业,作业就无法运行;对于Job而言,
一个job可以对应多个Trigger,但对于Trigger而言,一个Trigger只能对应一个job;所以一
个Trigger 只能被指派给一个 Job;如果你需要一个更复杂的触发计划,你可以创建多个 Trigger并指派它们给同一个 Job。

Scheduler的创建方式:

(1)StdSchedulerFactory:

Quartz默认的SchedulerFactory

  • 使用一组参数(java.util.Properties)来创建和初始化Quartz调度器

  • 配置参数一般存储在quartz.properties文件中

  • 调用getScheduler方法就能创建和初始化调度器对象

SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();

用法一:输出调度器开始的时间(重要:使得任务和触发器进行关联):

Date scheduleJob(JobDetail jobDetail, Trigger trigger)

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("调度器开始的时间是:"+dateFormat.format(scheduler.scheduleJob(job, trigger)));

用法二:启动任务调度:

void start();

scheduler.start();	

用法三:任务调度挂起,即暂停操作

void standby()

// Scheduler执行2秒后自动挂起
Thread.sleep(2000L);
scheduler.standby();	
// Scheduler执行5秒后自动开启
Thread.sleep(5000L);
scheduler.start();	

用法四:关闭任务调度

void shutdown()

shutdown(true):表示等待所有正在执行的job执行完毕之后,再关闭Scheduler; shutdown(false):表示直接关闭Scheduler

测试一:

// Scheduler执行2秒后自动挂起
Thread.sleep(2000L);
scheduler.shutdown();
// Scheduler执行5秒后自动开启
Thread.sleep(5000L);
scheduler.start();

测试二:

// Scheduler执行2秒后自动挂起
Thread.sleep(2000L);
/**
 * shutdown(true):表示等待所有正在执行的job执行完毕之后,再关闭Scheduler;
 * shutdown(false):表示直接关闭Scheduler
 */
scheduler.shutdown(false);
System.out.println("scheduler是否被关闭:"+scheduler.isShutdown());

同时修改:HelloJobScheduler.java

任务调度延迟5秒执行

// 延迟任务执行的时间,推迟5秒向后执行
try {
	Thread.sleep(5000L);
} catch (InterruptedException e) {
	e.printStackTrace();
}

(2)DirectSchedulerFactory(了解):

DirectSchedulerFactory是对SchedulerFactory的直接实现,通过它可以直接构建Scheduler、threadpool 等

DirectSchedulerFactory directSchedulerFactory = DirectSchedulerFactory.getInstance();
Scheduler scheduler = directSchedulerFactory.getScheduler();

九、Quartz.properties

默认路径:quartz-2.3.0中的org.quartz中的quartz.properties

我们也可以在项目的资源下添加quartz.properties文件,去覆盖底层的配置文件。

组成部分

  • 调度器属性

org.quartz.scheduler.instanceName属性用来区分特定的调度器实例,可以按照功能用途来给调度器起名。

org.quartz.scheduler.instanceId属性和前者一样,也允许任何字符串,但这个值必须在所有调度器实例中是唯一的,尤其是在一个集群环境中,作为集群的唯一key。假如你想Quartz帮你生成这个值的话,可以设置为AUTO。

  • 线程池属性

threadCount

处理Job的线程个数,至少为1,但最多的话最好不要超过100,在多数机器上设置该值超过100的话就会显得相当不实用了,特别是在你的 Job 执行时间较长的情况下

threadPriority

线程的优先级,优先级别高的线程比级别低的线程优先得到执行。最小为1,最大为10,默认为5

org.quartz.threadPool.class

一个实现了 org.quartz.spi.ThreadPool 接口的类,Quartz 自带的线程池实现类是 org.quartz.smpl.SimpleThreadPool

  • 作业存储设置

描述了再调度器实例的生命周期中,Job和Trigger信息是如何被存储的。

  • 插件配置

满足特定需求用到的Quartz插件的配置。

例子:

#===============================================================     
#Configure Main Scheduler Properties     调度器属性
#===============================================================  
#调度器的实例名     
org.quartz.scheduler.instanceName = QuartzScheduler     
#调度器的实例ID,大多数情况设置为auto即可  
org.quartz.scheduler.instanceId = AUTO     
 
#===============================================================     
#Configure ThreadPool     线程池属性
#===============================================================   
#处理Job的线程个数,至少为1,但最多的话最好不要超过100,在多数机器上设置该值超过100的话就会显得相当不实用了,特别是在你的 Job 执行时间较长的情况下
org.quartz.threadPool.threadCount =  5     
#线程的优先级,优先级别高的线程比级别低的线程优先得到执行。最小为1,最大为10,默认为5
org.quartz.threadPool.threadPriority = 5 
#一个实现了 org.quartz.spi.ThreadPool 接口的类,Quartz 自带的线程池实现类是 org.quartz.smpl.SimpleThreadPool      
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool     
 
#===============================================================     
#Configure JobStore 作业存储设置
#===============================================================      
#要使 Job 存储在内存中需通过设置  org.quartz.jobStrore.class 属性为 org.quartz.simpl.RAMJobStore 
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore     
 
#===============================================================     
#Configure Plugins    插件配置 
#===============================================================       
org.quartz.plugin.jobInitializer.class =       
org.quartz.plugins.xml.JobInitializationPlugin       
      
org.quartz.plugin.jobInitializer.overWriteExistingJobs = true      
org.quartz.plugin.jobInitializer.failOnFileNotFound = true      
org.quartz.plugin.jobInitializer.validating=false 

也可以编写程序代码操作quartz.properties文件的内容:

public class QuartzProperties {

	public static void main(String[] args) {
        // 创建工厂实例
        StdSchedulerFactory factory = new StdSchedulerFactory();
        
        // 创建配置工厂的属性对象
        Properties props = new Properties();
        props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool"); // 线程池定义
        props.put("org.quartz.threadPool.threadCount", "5"); // 默认Scheduler的线程数
        
        try {
            // 使用定义的属性初始化工厂
            factory.initialize(props);
            
            Scheduler scheduler = factory.getScheduler();
            
            scheduler.start();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

 通过Properties设置工厂属性的缺点在用硬编码,假如需要修改例子中线程数量,将不得不修改代码,然后重新编译。我们这里不推荐使用。

十、Quartz监听器

10.1、概念

Quartz的监听器用于当任务调度中你所关注事件发生时,能够及时获取这一事件的通知。类似于任务执行过程中的邮件、短信类的提醒。Quartz监听器主要有JobListener、TriggerListener、SchedulerListener三种,顾名思义,分别表示任务、触发器、调度器对应的监听器。三者的使用方法类似,在开始介绍三种监听器之前,需要明确两个概念:全局监听器与非全局监听器,二者的区别在于:

全局监听器能够接收到所有的Job/Trigger的事件通知,

而非全局监听器只能接收到在其上注册的Job或Trigger的事件,不在其上注册的Job或Trigger则不会进行监听。

10.2、JobListener介绍

任务调度过程中,与任务Job相关的事件包括:job开始要执行的提示; job执行完成的提示。

public interface JobListener {
  	String getName();
  	void jobToBeExecuted(JobExecutionContext context);
  	void jobExecutionVetoed(JobExecutionContext context);
    void jobWasExecuted(JobExecutionContext context,JobExecutionException jobException);
}

其中:

  1. getName方法:用于获取该JobListener的名称。

  2. jobToBeExecuted方法:Scheduler在JobDetail将要被执行时调用这个方法。

  3. jobExecutionVetoed方法:Scheduler在JobDetail即将被执行,但又被TriggerListerner否决时会调用该方法

  4. jobWasExecuted方法:Scheduler在JobDetail被执行之后调用这个方法

示例:

HelloJobListener.java

// 定义任务类
public class HelloJobListener implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 定义时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateString = dateFormat.format(date);

        // 定义工作任务内容
        System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
    }
}

创建自定义的JobListener

MyJobListener.java

public class MyJobListener implements JobListener{
    @Override
    public String getName() {
        String name = getClass().getSimpleName();
        System.out.println("监听器的名称是:"+name);
        return name;
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:"+jobName+"     Scheduler在JobDetail将要被执行时调用这个方法");
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:"+jobName+"     Scheduler在JobDetail即将被执行,但又被TriggerListerner否决时会调用该方法");
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:"+jobName+"     Scheduler在JobDetail被执行之后调用这个方法");
    }
}

执行调度器

HelloSchedulerDemoJobListener.java

public class HelloSchedulerDemoJobListener {
	public static void main(String[] args) throws Exception {
	// 1:从工厂中获取任务调度的实例
    Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

    // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
    JobDetail job = JobBuilder.newJob(HelloJobListener.class)
    		.withIdentity("job1", "group1") // 定义该实例唯一标识
    		.build();
    // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
      Trigger trigger = TriggerBuilder.newTrigger()
              .withIdentity("trigger1", "group1") // 定义该实例唯一标识
              .startNow()  // 马上执行
              //.startAt(triggerStartTime) // 针对某个时刻执行
              .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                  .repeatSecondlyForever(5)) // 每5秒执行一次 
              .build();

      // 4:使用触发器调度任务的执行
      scheduler.scheduleJob(job, trigger);

      // 创建并注册一个全局的Job Listener
      scheduler.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs());

      // 创建并注册一个指定任务的Job Listener
      // scheduler.getListenerManager().addJobListener(new MyJobListener(), 			  KeyMatcher.keyEquals(JobKey.jobKey("job1", "group1")));
      // 5:开启
      scheduler.start();
      // 关闭
      // scheduler.shutdown();
  }
}

10.3、TriggerListener介绍

任务调度过程中,与触发器Trigger相关的事件包括:触发器触发、触发器未正常触发、触发器完成等。

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);
}

其中:

  1. getName方法:用于获取触发器的名称

  2. triggerFired方法:当与监听器相关联的Trigger被触发,Job上的execute()方法将被执行时,Scheduler就调用该方法。

  3. vetoJobExecution方法:在 Trigger 触发后,Job 将要被执行时由 Scheduler 调用这个方法。TriggerListener 给了一个选择去否决 Job 的执行。假如这个方法返回 true,这个 Job 将不会为此次 Trigger 触发而得到执行。

  4. triggerMisfired方法:Scheduler 调用这个方法是在 Trigger 错过触发时。你应该关注此方法中持续时间长的逻辑:在出现许多错过触发的 Trigger 时,长逻辑会导致骨牌效应。你应当保持这上方法尽量的小。

  5. triggerComplete方法:Trigger 被触发并且完成了 Job 的执行时,Scheduler 调用这个方法。

示例:

下面的例子简单展示了TriggerListener的使用,其中创建并注册TriggerListener与JobListener几乎类似。

HelloJobListener.java

// 定义任务类
public class HelloJobListener implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 定义时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateString = dateFormat.format(date);

        // 定义工作任务内容
        System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
    }
}

MyTriggerListener.java

public class MyTriggerListener implements TriggerListener{
	private String name;

	public MyTriggerListener(String name) {
   		 this.name = name;
	}

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        String triggerName = trigger.getKey().getName();
        System.out.println(triggerName + " 被触发");
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        String triggerName = trigger.getKey().getName();
        System.out.println(triggerName + " 没有被触发");
        return true; // true:表示不会执行Job的方法
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        String triggerName = trigger.getKey().getName();
        System.out.println(triggerName + " 错过触发");
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
            CompletedExecutionInstruction triggerInstructionCode) {
        String triggerName = trigger.getKey().getName();
        System.out.println(triggerName + " 完成之后触发");
    }
}

任务调度类HelloSchedulerDemoTriggerListener.java

public class HelloSchedulerDemoTriggerListener {
    public static void main(String[] args) throws Exception {
        // 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJobListener.class)
                .withIdentity("job1", "group1") // 定义该实例唯一标识
                .build();
        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 定义该实例唯一标识
                .startNow()  // 马上执行
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                    .repeatSecondlyForever(5)) // 每5秒执行一次 
                .build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

        // 创建并注册一个全局的Trigger Listener
        scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), EverythingMatcher.allTriggers());

        // 创建并注册一个局部的Trigger Listener
        // scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), KeyMatcher.keyEquals(TriggerKey.triggerKey("trigger1", "group1")));

        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
    }
 }

10.4、SchedulerListener介绍

SchedulerListener会在Scheduler的生命周期中关键事件发生时被调用。与Scheduler有关的事件包括:增加一个job/trigger,删除一个job/trigger,scheduler发生严重错误,关闭scheduler等。

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();
}

其中:

  1. jobScheduled方法:用于部署JobDetail时调用

  2. jobUnscheduled方法:用于卸载JobDetail时调用

  3. triggerFinalized方法:当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。

  4. triggersPaused方法:Scheduler 调用这个方法是发生在一个 Trigger 或 Trigger 组被暂停时。假如是 Trigger 组的话,triggerName 参数将为 null。

  5. triggersResumed方法:Scheduler 调用这个方法是发生成一个 Trigger 或 Trigger 组从暂停中恢复时。假如是 Trigger 组的话,假如是 Trigger 组的话,triggerName 参数将为 null。参数将为 null。

  6. jobsPaused方法:当一个或一组 JobDetail 暂停时调用这个方法。

  7. jobsResumed方法:当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。

  8. schedulerError方法:在 Scheduler 的正常运行期间产生一个严重错误时调用这个方法。

  9. schedulerStarted方法:当Scheduler 开启时,调用该方法

  10. schedulerInStandbyMode方法: 当Scheduler处于StandBy模式时,调用该方法

  11. schedulerShutdown方法:当Scheduler停止时,调用该方法

  12. schedulingDataCleared方法:当Scheduler中的数据被清除时,调用该方法。

示例:

下面的代码简单描述了如何使用SchedulerListener方法:

HelloJobListener.java

public class HelloJobListener implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 定义时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateString = dateFormat.format(date);

        // 定义工作任务内容
        System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
    }
}

MySchedulerListener.java

public class MySchedulerListener implements SchedulerListener{
    @Override
    public void jobScheduled(Trigger trigger) {
        String jobName = trigger.getJobKey().getName();
        System.out.println(jobName + " 完成部署");
    }

    @Override
    public void jobUnscheduled(TriggerKey triggerKey) {
        System.out.println(triggerKey + " 完成卸载");
    }

    @Override
    public void triggerFinalized(Trigger trigger) {
        System.out.println("触发器被移除 " + trigger.getJobKey().getName());
    }

    @Override
    public void triggerPaused(TriggerKey triggerKey) {
        System.out.println(triggerKey + " 正在被暂停");
    }

    @Override
    public void triggersPaused(String triggerGroup) {
        System.out.println("触发器组 "+triggerGroup + " 正在被暂停");
    }

    @Override
    public void triggerResumed(TriggerKey triggerKey) {
        System.out.println(triggerKey + " 正在从暂停中恢复");
    }

    @Override
    public void triggersResumed(String triggerGroup) {
        System.out.println("触发器组 "+triggerGroup + " 正在从暂停中恢复");
    }

    @Override
    public void jobAdded(JobDetail jobDetail) {
        System.out.println(jobDetail.getKey()+" 添加工作任务");
    }

    @Override
    public void jobDeleted(JobKey jobKey) {
        System.out.println(jobKey+" 删除工作任务");
    }

    @Override
    public void jobPaused(JobKey jobKey) {
        System.out.println(jobKey+" 工作任务正在被暂停");
    }

    @Override
    public void jobsPaused(String jobGroup) {
        System.out.println("工作任务组 "+jobGroup+" 正在被暂停");
    }

    @Override
    public void jobResumed(JobKey jobKey) {
        System.out.println(jobKey+" 正在从暂停中恢复");
    }

    @Override
    public void jobsResumed(String jobGroup) {
        System.out.println("工作任务组 "+jobGroup+" 正在从暂停中恢复");
    }

    @Override
    public void schedulerError(String msg, SchedulerException cause) {
        System.out.println("产生严重错误时调用:   "+msg+"  "+cause.getUnderlyingException());
    }

    @Override
    public void schedulerInStandbyMode() {
        System.out.println("调度器在挂起模式下调用");
    }

    @Override
    public void schedulerStarted() {
        System.out.println("调度器 开启时调用");
    }

    @Override
    public void schedulerStarting() {
        System.out.println("调度器 正在开启时调用");
    }

    @Override
    public void schedulerShutdown() {
        System.out.println("调度器 已经被关闭 时调用");
    }

    @Override
    public void schedulerShuttingdown() {
        System.out.println("调度器 正在被关闭 时调用");
    }

    @Override
    public void schedulingDataCleared() {
        System.out.println("调度器的数据被清除时调用");
    }
}

HelloSchedulerDemoSchedulerListener.java

public class HelloSchedulerDemoSchedulerListener {
    public static void main(String[] args) throws Exception {
        // 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJobListener.class)
                .withIdentity("job1", "group1") // 定义该实例唯一标识
                .build();
        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 定义该实例唯一标识
                .startNow()  // 马上执行
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                    .repeatSecondlyForever(5)) // 每5秒执行一次 
                .build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

        // 创建SchedulerListener
        scheduler.getListenerManager().addSchedulerListener(new MySchedulerListener());

        // 移除对应的SchedulerListener
        // scheduler.getListenerManager().removeSchedulerListener(new MySchedulerListener());

        // 5:开启
        scheduler.start();
        // 延迟7秒后关闭
        Thread.sleep(7000);
        // 关闭
        scheduler.shutdown();
    }
 }

十一、JobStore 作业存储

JobStore 属性在 Quartz 配置文件中声明,用于定义 Quartz 所有运行时任务的存储方式,目前主要有两种方式

11.1、RAMJobStore介绍

RAMJobStore 是基于内存的存储模式,其特点如下:

  • 优点:

    • 使用和配置简单

    • 将所有数据保留在RAM中,性能最高

  • 缺点

    • 当您的应用程序结束(或崩溃)时,所有调度信息都将丢失-

    • 这意味着RAMJobStore无法接受JobTrigger上的duriable设置。

    • 对于某些应用程序,这是可以接受的,甚至是所需的行为,但是对于其他应用程序,这可能是灾难性的。

配置方式 :

要使用RAMJobStore(并假设您正在使用StdSchedulerFactory),只需将类名称org.quartz.simpl.RAMJobStore指定为用于配置到quartz的JobStore类属性:

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

11.2、JDBCJobStore介绍

JDBCJobStore 是基于数据的存储模式,其特点如下:

  • 优点:支持常见的数据库,可以持久化保存任务信息

  • 缺点:配置繁琐,性能不高(取决于数据库)

11.2.1、使用示例

使用 JDBCJobStore 需要以下 3 步完成:

第一步:在项目中添加相关数据库依赖:

<!-- 添加数据库依赖 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>

第二步:在数据库执行 Quartz 官方的 SQL DDL 脚本,创建数据库表结构,Quartz 核心的表结构如下:

Table NameDescription
QRTZ_CALENDARS存储Quartz的Calendar信息
QRTZ_CRON_TRIGGERS存储CronTrigger,包括Cron表达式和时区信息
QRTZ_FIRED_TRIGGERS存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
QRTZ_PAUSED_TRIGGER_GRPS存储已暂停的Trigger组的信息
QRTZ_SCHEDULER_STATE存储少量的有关Scheduler的状态信息,和别的Scheduler实例
QRTZ_LOCKS存储程序的悲观锁的信息
QRTZ_JOB_DETAILS存储每一个已配置的Job的详细信息
QRTZ_JOB_LISTENERS存储有关已配置的JobListener的信息
QRTZ_SIMPLE_TRIGGERS存储简单的Trigger,包括重复次数、间隔、以及已触的次数
QRTZ_BLOG_TRIGGERSTrigger作为Blob类型存储
QRTZ_TRIGGERS存储已配置的Trigger的信息

第三步:配置文件修改为 JDBCJobStore 模式,配置数据源,并且将 `jobStore` 指定为该数据源,如下

# quartz scheduler config
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 3
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = myDS

# dataSource
org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://127.0.0.1:3306/quartz_demo
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = test123456
org.quartz.dataSource.myDS.maxConnections = 30

 最后运行 QuartzTest 后可以看到数据库 QRTZ_JOB_DETAILS 表已经添加数据,如下:

11.2.2、注意事项

在使用 JDBCJobStore 时,需要注意以下事项:

  • Quartz 的 JobStoreTX 默认是独立示例,如果需要和其他事务一起工作(例如 J2EE 服务器),可以选择 JobStoreCMT

  • 默认表前缀是 QRTZ_,可进行配置,使用多个不同的前缀有助于实现同一数据库的任务调度多组表结构

  • JDBC 委托驱动

    StdJDBCDelegate

    适用于大多数数据库,目前只针对测试

    StdJDBCDelegate

    时出现问题的类型进行特定的委托

    • DB2v6Delegate:适用于 DB2 版本 6 及更早版本

    • HSQLDBDelegate:适用于 HSQLDB 数据库

    • MSSQLDelegate:适用于 Microsoft SQLServer 数据库

    • PostgreSQLDelegate:适用于 PostgreSQL 数据库

    • WeblogicDelegate:由 Weblogic 制作的驱动程序

    • OracleDelegate:适用于 Oracle 数据库

    • …………

  • org.quartz.jobStore.useProperties 设置为 True,避免将非基础类型数据存储到数据库的 BLOB 字段

十二、springboot 集成

Quartz 整合 Springboot 非常普遍的场景,整合 Spring 可以带来好处:

  • 更加简洁的配置,开箱即用

  • 和 Spring 的 IOC 容器融合,使用更便捷

12.1、添加依赖

可以在现有项目上添加 springboot 官方提供的 starter-quartz 依赖,如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

如果是新项目,可以直接在 [Spring Initializr](https://start.spring.io/) 添加 Quartz Schduler 如下:

启动 Springboot 会发现,无需任何配置就已经整合 Quartz 模块了:

12.2、使用示例

现在基于整合模式实现刚才的 Demo 示例,首先定义任务,这里不再是实现 Job 类:

public class HelloJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        String name = jobDataMap.getString("name");
        System.out.println("Hello :" + name);
    }
}

这里实现由 Springboot 提供的 QuartzJobBean,实现 executerInternal() 方法,这是一个经过 Spring 容器包装后的任务类,可以在任务类使用 Spring 容器的实例

在 Demo 示例里面,我们调度启动都是在 Main 方法启动,在本地测试没有问题,但在生产环境就不建议了,和 springboot 整合后关于任务执行,现在可以有 2 中选项:

  1. 在控制层 Controller 提供接口,手动接收任务指定

  2. 监听 Spring 容器,在容器启动后,自动加载任务,并且注册为 Bean

12.2.1、手动执行

我们先看看第一种实现方式,我们创建控制器,然后接收参数,创建任务,如下:

@RestController
public class HelloController {
    
    @Autowired
    private Scheduler scheduler;

    @GetMapping("/hello")
    public void helloJob(String name) throws SchedulerException {
        // 定义一个的任务
        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("job11", "group1")
                .usingJobData("name", name)
                .build();

        // 定义一个简单的触发器: 每隔 1 秒执行 1 次,任务永不停止
        SimpleTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .withSchedule(SimpleScheduleBuilder
                        .simpleSchedule()
                        .withIntervalInSeconds(1)
                        .repeatForever()
                ).build();

        // 开始调度
        scheduler.scheduleJob(job, trigger);
    }
}

然后启动服务器,访问接口传入参数:

$curl --location --request GET 'http://localhost:8080/hello?name=phoenix'

然后控制台会输出:

2023-01-21 22:03:03.213  INFO 23832 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-01-21 22:03:03.213  INFO 23832 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-01-21 22:03:03.214  INFO 23832 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
Hello :phoenix
Hello :phoenix
#....
12.2.2、自动执行

将 JobDetail 注册 Bean,任务就会随 Spring 启动自动触发执行,这对于需要随程序启动执行的作业非常有效,配置如下:

先创建一个配置类:

@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail jobDetail() {
        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("job11", "group1")
                .usingJobData("name", "springboot")
                .storeDurably()
                .build();

        return job;
    }

    @Bean
    public Trigger trigger() {
        SimpleTrigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail())
                .withIdentity("trigger1", "group1")
                .withSchedule(SimpleScheduleBuilder
                        .simpleSchedule()
                        .withIntervalInSeconds(1)
                        .repeatForever()
                ).build();

        return trigger;
    }
}

然后在 springboot 启动后,任务就自动执行:

2023-01-21 22:29:51.962  INFO 46376 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler quartzScheduler_$_NON_CLUSTERED started.
Hello :springboot
Hello :springboot
Hello :springboot
# ....

十三、集群模式

13.1、集群模式介绍

为什么需要集群?
1、防止单点故障,减少对业务的影响
2、减少节点的压力,例如在 10 点要触发 1000 个任务,如果有 10 个节点,则每个节点之需要执行 100 个任务

集群需要解决的问题
1、任务重跑,因为节点部署的内容是一样的,到 10 点的时候,每个节点都会执行相同的操作,引起数据混乱。比如跑批,绝对不能执行多次。
2、任务漏跑,假如任务是平均分配的,本来应该在某个节点上执行的任务,因为节点故障,一直没有得到执行。
3、水平集群需要注意时间同步问题
4、Quartz 使用的是随机的负载均衡算法,不能指定节点执行

在 Quartz 中,提供了一种简单的方式,基于数据库共享任务执行信息。也就是说,一个节点执行任务的时候,会操作数据库,其他的节点查询数据库,便可以感知到了。使用系统自带的 11 张表即可。

对于生产环境来说,高可用,负载均衡,故障恢复,这些分布式的能力是必不可少的,Quartz 天生支持基于数据库的分布式:

要启用集群模式,需要注意以下事项:

1. 需要启用 JDBCStore 或者 TerracottaJobStore 运行模式
2. 需要将 `jobStore.isClustered` 属性设置为 True
3. 每个单独实例需要设置唯一的 `instanceId` (Quartz 提供参数让这点很容易实现)

13.2、配置集群

下面看看 springboot 集成的模式下如何配置 quartz 集群模式:

在 `application.yml` 添加 quartz 集群配置信息:

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/quartz_demo
    username: root
  quartz:
    job-store-type: jdbc
    properties:
      org:
        quartz:
          scheduler:
            instanceName: ClusteredScheduler   # 集群名,若使用集群功能,则每一个实例都要使用相同的名字
            instanceId: AUTO    # 若是集群下,每个 instanceId 必须唯一,设置 AUTO 自动生成唯一 Id
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 25
            threadPriority: 5
          jobStore:
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            useProperties: true   # 使用字符串参数,避免了将非 String 类序列化为 BLOB 的类版本问题
            isClustered: true     # 打开集群模式
            clusterCheckinInterval: 5000     # 集群存活检测间隔
            misfireThreshold: 60000 # 最大错过触发事件时间

使用集群模式需要添加数据库依赖,如下:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

然后创建 `SchedulerConfig` 配置类,将相关的配置信息加载到 `SchedulerFactoryBean` 中才能生效:

@Configuration
public class SchedulerConfig {

	@Autowired
	private DataSource dataSource;

	@Autowired
	private QuartzProperties quartzProperties;

	@Bean
	public SchedulerFactoryBean schedulerFactoryBean() {
		Properties properties = new Properties();
		properties.putAll(quartzProperties.getProperties());

		SchedulerFactoryBean factory = new SchedulerFactoryBean();
		factory.setOverwriteExistingJobs(true);
		factory.setDataSource(dataSource);
		factory.setQuartzProperties(properties);
		return factory;
	}
}

最后在启动日志内,可以看到 Quartz 启动集群模式运行:

13.3、注意事项 

使用集群模式,需要注意以下事项:

  • 不要在单机模式下使用集群模式,不然会出现时钟同步问题,具体参考 NIST Internet Time Service (ITS) | NIST

  • 不要在集群示例中,运行单机示例,不然会出现数据混乱和不稳定的情况

  • 关于任务的运行节点是随机的(哪个节点抢到锁就可以执行),尤其对大量情人的情况

  • 如果不想依赖 JDBC 数据库实现集群,可以看看 TerracottaJobStore 模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/479092.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python Flask框架 -- 模版继承

一个网站中&#xff0c;大部分网页的模块是重复的&#xff0c;比如顶部的导航栏&#xff0c;底部的备案信息。如果在每个页面中都重复的去写这些代码&#xff0c;会让项目变得臃肿&#xff0c;提高后期维护成本。比较好的做法是&#xff0c;通过模板继承&#xff0c;把一些重复…

SpringBoot-04 | spring-boot-starter-logging原理原理

SpringBoot-04 | spring-boot-starter-logging原理原理 第一步&#xff1a;springboot加载factories文件第二步&#xff1a;构造监听器第三步&#xff1a;注册监听器到Spring中第四步&#xff1a;开始加载日志框架第五步&#xff1a;加载日志框架logback-spring.xml第六步&…

全域电商数据实现高效稳定大批量采集♀

全域电商&#xff0c;是近几年的新趋势&#xff0c;几乎所有商家都在布局全域&#xff0c;追求全域增长。但商家发现&#xff0c;随着投入成本的上涨&#xff0c;利润却没有增加。 其中最为突出的是——商家为保证全域数据的及时更新&#xff0c;通过堆人头的方式完成每日取数任…

【应用笔记】LAT1305+使用STM32+TT类型IO的注意事项

1. 概述 在 STM32 系列 MCU 中&#xff0c; 除了一些特殊管脚外&#xff0c;绝大多数管脚都可以分类为 FT (兼容5V 信号)或 TT&#xff08;兼容 3V3 信号&#xff09;类型的 IO&#xff0c;由于 MCU 内部设计的不同&#xff0c; TT IO 相比 5V IO 有更多的限制&#xff0c;下面…

I2C协议

一.硬件连接 I2C必须使用开漏&#xff08;或集电极开路&#xff09;的引脚&#xff0c;其引脚框图如下所示。 SCL0对应78K0的P6.0引脚&#xff0c;SDA0对应78K0的P6.1引脚。 在使用开漏引脚通信时&#xff0c;需注意如下事项&#xff1a; 1&#xff09;两条总线须外接…

国产之光?Kimichat大模型200万字超长上下文突破

Kimi Chat简介 Kimi是AI大模型初创企业月之暗面&#xff08;Moonshot&#xff09;推出的AI产品。近日月之暗面宣布Kimi 智能助手在长上下文窗口技术上再次取得突破&#xff0c;无损上下文长度提升了一个数量级到200万字。 月之暗面&#xff08;Moonshot AI&#xff09;&#…

保姆级系列教程-玩转Fiddler抓包教程(1)-HTTP和HTTPS基础知识

2024软件测试面试刷题&#xff0c;这个小程序&#xff08;永久刷题&#xff09;&#xff0c;靠它快速找到工作了&#xff01;&#xff08;刷题APP的天花板&#xff09;【持续更新最新版】-CSDN博客 1.简介 有的小伙伴或者童鞋们可能会好奇地问&#xff0c;不是讲解和分享抓包…

CI/CD实战-jenkins部署 3

安装 软件下载地址&#xff1a;Index of /jenkins/redhat/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 启动服务 安装推荐插件 不新建用户&#xff0c;使用admin账号登录 修改一下初始密码 新建项目测试 安装git命令 生成密钥 在gitlab中上传公钥 修改ssh 创建中…

babel主要内容

定义 babel是一个编译工具 &#xff0c;用于把JSX等编译成浏览器可执行的javascript。 主要内容是几个包babel/parser 这个包主要是用于解析代码到AST树babel/types 这个包中有一堆API&#xff0c;用于手动创建ASTbabel/traverse 这个包主要是为了遍历AST树&#xff0c;结合具体…

C/C++代码性能优化——编程实践

1. 编程实践 在一些关键的地方&#xff0c;相应的编程技巧能够给性能带来重大提升。 1.1. 参数传递 传递非基本类型时&#xff0c;使用引用或指针&#xff0c;这样可以避免传递过程中发生拷贝。参数根据是否需要返回&#xff0c;相应加上const修饰&#xff0c;代码更安全&am…

硬盘、内存、缓存(CPU)和寄存器 空间大小与存取速度的区别及设计原理

一、寄存器和存储器是不同的 很多人会将 寄存器 与 存储器 二者混淆&#xff0c;认为它们是同一个东西。但并不是&#xff01;&#xff01; 寄存器是CPU上的一个模块 存储器是 内存硬盘的统称 二、存取速度的比较 CPU(包含寄存器&#xff0c;缓存) > 内存 > 硬盘 内…

浅谈Postman与Jmeter的区别、用法

前阶段做了一个小调查&#xff0c;发现软件测试行业做功能测试和接口测试的人相对比较多。在测试工作中&#xff0c;有高手&#xff0c;自然也会有小白&#xff0c;但有一点我们无法否认&#xff0c;就是每一个高手都是从小白开始的&#xff0c;所以今天我们就来谈谈一大部分人…

【TD3思路及代码】【自用笔记】

1 组成&#xff08;Target Network Delayed Training&#xff09; Actor网络&#xff1a;这个网络负责根据当前的状态输出动作值。在训练过程中&#xff0c;Actor网络会不断地学习和优化&#xff0c;以输出更合适的动作。Critic网络&#xff1a;TD3中有两个Critic网络&#xff…

2024Postman中变量的使用!

Postman中可设置的变量类型有全局变量&#xff0c;环境变量&#xff0c;集合变量&#xff0c;数据变量及局部变量。区别则是各变量作用域不同&#xff0c;全局变量适用于所有集合&#xff0c;环境变量适用于当前所选环境&#xff08;所有集合中均可使用不同环境变量&#xff09…

重磅|国家能源局开展配电网安全风险管控重点行动

据国家能源局3月21日消息&#xff0c;为紧扣新形势下电力保供和转型目标&#xff0c;聚焦配电网安全运行、供电保障、防灾减灾和坚强可靠等方面安全风险&#xff0c;推动解决城乡配电网发展薄弱等问题&#xff0c;全面提升配电网供电保障和综合承载能力&#xff0c;国家能源局决…

Mysql数据库:索引管理

目录 一、索引的概述 1、索引的概念 2、索引的作用 3、索引的副作用 4、创建索引的原则依据 5、索引优化 6、索引的分类 7、数据文件与索引文件 二、管理数据库索引 1、查询索引 2、创建索引 2.1 创建普通索引 2.2 创建唯一索引 2.3 创建主键索引 2.4 创建组合…

Xinstall让App推广变得高效而简单

随着移动互联网的迅猛发展&#xff0c;App已成为人们生活中不可或缺的一部分。然而&#xff0c;对于众多开发者和广告主来说&#xff0c;如何高效地推广自己的App&#xff0c;却一直是一个令人头疼的问题。今天&#xff0c;我们要为大家介绍的&#xff0c;正是国内专业的App全渠…

AI大模型学习在当前技术环境下的重要性与发展前景

目录 前言1 学科基础与技能要求1.1 数学基础的深厚性1.2 编程能力的必要性1.3 对特定领域业务场景的了解 2 模型结构与算法的优化2.1 模型结构的不断演进2.2 算法优化的重要性2.3 准确性与效率的提升 3 AI大模型学习的应用场景3.1 自然语言处理3.2 计算机视觉3.3 推荐系统 结语…

独家发布! 10个2024年新算法跑10个测试集!

前言&#xff1a;独家发布&#xff01; 10个2024年新算法跑10个测试集&#xff01;每个算法都是独立.m文件&#xff0c;高效管理&#xff0c;所有结果均可一键运行自动保存&#xff0c;可用于算法对比、学习、改进等等&#xff0c;趁现在知道的人少&#xff0c;先用先发&#x…

DMA的设置

DMA&#xff08;Direct Memory Access&#xff0c;直接内存访问&#xff09;是一种用于提高数据传输效率的重要技术&#xff0c;在现代计算机系统中被广泛应用。DMA的设置涉及到配置DMA控制器、分配内存缓冲区、设置传输模式等多个方面。本文将介绍DMA的设置过程及相关注意事项…
最新文章