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

news2024/9/21 23:36:11

目录

一、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.coloradmin.cn/o/1536942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Mock|JS】Mock的get传参+获取参数信息

mockjs的get传参 前端请求 const { data } await axios("/video/childcomments", {params: {sort: 1,start: 2,count: 5,childCount: 6,commenIndex: 0,},});后端获取参数 使用正则匹配url /*** # 根据url获取query参数* param {Url} urlStr get请求获取参数 eg:…

鸿蒙Harmony应用开发—ArkTS-全局UI方法(时间滑动选择器弹窗)

以24小时的时间区间创建时间滑动选择器&#xff0c;展示在弹窗上。 说明&#xff1a; 该组件从API Version 8开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 本模块功能依赖UI的执行上下文&#xff0c;不可在UI上下文不明确的地方使用&…

vuecli创建vue3项目

第一步&#xff1a; 在文件夹中输入 vue create xxx 第二步&#xff1a; 勾选下面带有*号的&#xff0c;经验最好把Linter/Formatter勾掉&#xff0c;不然会出现eslint报错 第三步&#xff1a; 选择3.x 第四步&#xff1a; 意思为是否用history模式来创建路由&#xff0…

把 Taro 项目作为一个完整分包,Taro项目里分包的样式丢失

现象&#xff1a; 当我们把 Taro 项目作为原生微信小程序一个完整分包时&#xff0c;Taro项目里分包的样式丢失&#xff0c;示意图如下&#xff1a; 原因&#xff1a; 在node_modules/tarojs/plugin-indie/dist/index.js文件里&#xff0c;限制了只有pages目录下会被引入app.w…

The 2023 Guangdong Provincial Collegiate Programming Contest

I. Path Planning 嗯&#xff0c;怎么说呢&#xff0c;一般二维图&#xff0c;数据不是很大的比如n*m*log级别允许的&#xff0c;如果一眼不是bfs&#xff0c;可以考虑结合一下二分 本题可知&#xff0c;只能向下或者向右&#xff0c;那么我们就像如果答案为x&#xff0c;那么…

【重温设计模式】访问者模式及其Java示例

访问者模式的基本概念 访问者模式&#xff0c;一种行为型设计模式&#xff0c;其基本定义是&#xff1a;允许一个或者多个操作应用到一组对象上&#xff0c;解耦操作和对象的具体类&#xff0c;使得操作的添加可以独立于对象的类结构变化。在面向对象编程中&#xff0c;访问者…

sqllab第35-45关通关笔记

35关知识点&#xff1a; 宽字节注入数值型注入错误注入 payload:id1andextractvalue(1,concat(0x7e,database(),0x7e))0--联合注入 payload:id0unionselect1,database(),version()-- 36关知识点&#xff1a; 字符型注入宽字节注入错误注入 payload:id1%df%27andextractvalue(…

什么是浏览器指纹识别?Maskfog指纹浏览器有用吗?

浏览器指纹识别是好是坏&#xff1f;这现在确实是一个有争议的话题。83%的消费者经常或偶尔会根据浏览历史记录看到广告。其实这就是利用了浏览器指纹技术。 如果您想了解浏览器指纹识别是什么&#xff0c;那就看下去&#xff01; 一、什么是浏览器指纹识别 浏览器指纹是指无…

Linux学习之自定义协议

前言&#xff1a; 首先对于Tcp的socket的通信&#xff0c;我们已经大概了解了&#xff0c;但是其中其实是由一个小问题&#xff0c;我们目前是不得而知得&#xff0c;在我们客户端与服务端连接成功后&#xff0c;服务端读取来自客户端得消息&#xff0c;我们使用的是函数read,…

线程池相关详解

1.线程池的核心参数 线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数&#xff1a; corePoolSize核心线程数目 maximumPoolSize最大线程数目&#xff08;核心线程救急线程的最大数目&#xff09; keepAliveTime生存时间:救急线程的生存时间&#xff0c;生…

Pytorch入门实战 P3-天气识别

目录 一、前期准备 1、查看设备 2、导入本地数据 3、测试下获取到的天气数据 4、图像预处理 5、划分数据集 6、加载数据集 二、搭建简单的CNN网络&#xff08;特征提取分类&#xff09; 三、训练模型 1、设置超参数 2、编写训练函数 3、编写测试函数 4、正式训练 …

4、类加载器

2.4.1 什么是类加载器 类加载器&#xff08;ClassLoader&#xff09;是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术&#xff0c;类加载器只参与加载过程中的字节码获取并加载到内存这一部分。 类加载器会通过二进制流的方式获取到字节码文件的内容&#xff0c…

Visual Studio配置libtorch(cuda安装一步到位)

Visual Studio配置libtorch visual Studio安装cuDNN安装CUDAToolkit安装libtorch下载Visual Studio配置libtorch(cuda版本配置) visual Studio安装 visual Studio点击安装 具体的安装和配置过程这里就不进行细讲了&#xff0c;可以参考我这篇博客Visual Studio配置OpenCV(保姆…

【嵌入式学习】Qtday03.21

一、思维导图 二、练习 自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面。&#xff08;不要使用课堂上的图片和代码&#xff0c;自己发挥&#xff0c;有利于后面项目的完成&#xff09; 要求&#xff1a; 1. 需要使用Ui界面文件进行界面设计 2. ui界面上的组件…

vue.js制作学习计划表案例

通俗易懂&#xff0c;完成“学习计划表”用于对学习计划进行管理&#xff0c;包括对学习计划进行添加、删除、修改等操作。 一. 初始页面效果展示 二.添加学习计划页面效果展示 三.修改学习计划完成状态的页面效果展示 四.删除学习计划 当学习计划处于“已完成”状态时&…

栈——数据结构——day4

栈的定义 栈是限定仅在一段进行插入和删除操作的线性表。 我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表&#xff0c;简称LIFO结构。 栈的插入操作&#xff0c;叫作进栈&#…

开源项目ChatGPT-Next-Web的容器化部署(三)-- k8s deployment.yaml部署

一、说在前面的话 有了docker镜像&#xff0c;要把一个项目部署到K8S里&#xff0c;主要就是编写deployment.yaml。 你需要考虑的是&#xff1a; 环境变量服务的健康检测持久化启动命令程序使用的数据源程序使用的配置文件 因为本前端项目比较简单&#xff0c;这里只做一个…

重学SpringBoot3-Profiles介绍

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-Profiles介绍 Profiles简介如何在Spring Boot中使用Profiles定义Profiles激活ProfilesIDEA设置active profile使用Profile-specific配置文件 条件化Bean…

Python爬虫案例-爬取主题图片(可以选择自己喜欢的主题)

2024年了&#xff0c;你需要网络资源不能还自己再慢慢找吧&#xff1f; 跟着博主一块学习如何利用爬虫获取资源&#xff0c;从茫茫大海中寻找那个她到再妹子群中找妹子&#xff0c;闭着眼睛都可以找到合适的那种。文章有完整示例代码&#xff0c;拿过来就可以用&#xff0c;欢迎…

就业班 第二阶段 2401--3.18 day1 初识mysql

初识&#xff1a; 1、关系型数据库mysql、mariadb、sqlite 二维关系模型 2、非关系型数据库 redis、memcached sql 四个部分 DDL 数据库定义语言 创建数据库&#xff0c;创建用户&#xff0c;创建表 DML 数据库操作语言 增删改 DQL 数据库查询语言 查 DCL 数据库控制语言 授权 …