简介
Quartz
:任务调度框架。
官网:http://www.quartz-scheduler.org
所谓的任务调度,其实就是我们想在什么时候做什么事情?
- 我们想做的事情就是Job(任务)。
- 我们期望的时候就是Trigger(触发器)。
- 最后还需要一个Scheduler(调度器),将其整合起来。
案例
新建一个最简单的Maven工程,引入Quartz的Jar包。
1 2 3 4 5
| <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency>
|
有些资料会说还需要引入quartz-jobs
。
1 2 3 4 5
| <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.3.2</version> </dependency>
|
实际上,这个包不一定需要引用。
这个包内部,基本上是一些预设好的Job。

创建HelloJob任务类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
public class HelloJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println(LocalDateTime.now()); } }
|
创建主方法的类,在该类中定义Trigger(触发器)以及Scheduler(调度器)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class). withIdentity("job1", "group1"). build();
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).build();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start(); } }
|
运行任务调度类,运行结果:
1 2 3 4 5
| 2022-12-10T08:24:57.480 2022-12-10T08:25:02.385 2022-12-10T08:25:07.384
【部分运行结果略】
|
解释说明:
- 每一个
Job
必须实现org.quartz.job
接口,并实现execute()
方法。
JobDetail
,定义任务实例。在上文,JobDetail
实例是通过JobBuilder
创建的。
JobBuilder
,用于创建一个任务实例。
TriggerBuilder
,用于创建触发器实例。
scheduler
的方法除了start()
、shutdown()
,还有standby()
(暂停操作)。
除了上文的创建scheduler
的方法,我们还可以通过如下的方法创建:
1 2
| SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler();
|
上文的方法属于静态工厂,这种方法属于实例工厂。
关于静态工程和实例工厂,我们在《15.Spring Framework [1/2]》有过讨论。
生命周期
每次调度器执行Job时,在调用execute方法前都会创建一个新的Job实例,当调用完成后,关联的Job对象实例会被释放,释放的实例会被垃圾回收机制回收。
我们可以验证一下,修改HelloJob
类,新增构造方法,并在构造方法中打印HelloJob Constructor
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
public class HelloJob implements Job {
public HelloJob() { System.out.println("HelloJob Constructor"); }
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println(LocalDateTime.now()); } }
|
运行结果:
1 2 3 4 5 6 7 8
| HelloJob Constructor : 2022-12-10T21:20:09.829 2022-12-10T21:20:09.831 HelloJob Constructor : 2022-12-10T21:20:14.776 2022-12-10T21:20:14.776 HelloJob Constructor : 2022-12-10T21:20:19.780 2022-12-10T21:20:19.780
【部分运行结果略】
|
特别的,如果我们创建一个有参构造方法,并且我们不创建无参构造方法呢?
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.PersistJobDataAfterExecution;
import java.time.LocalDateTime;
@PersistJobDataAfterExecution public class HelloJob implements Job {
private String k1;
public HelloJob(String k1) { System.out.println("HelloJob Constructor"); this.k1 = k1; }
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(LocalDateTime.now()); } }
|
运行结果:
程序不会运行。
因为默认使用的是无参构造方法。
所以,我们在《2.面向对象》说:“无论是否使用,都手工书写无参数构造方法”。
JobExecutionContext
当Scheduler调用一个Job,就会将JobExecutionContext传递给Job的execute()方法。
获取Job的明细数据
Job能通过JobExecutionContext对象访问到Quartz运行时候的环境以及Job本身的明细数据。例如:Group
、Name
、Class
、当前任务执行时间(.getFireTime()
)、下一次任务执行时间(.getNextFireTime()
)等。
获取Group
、Name
、Class
,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.kakawanyifan.job;
import org.quartz.*;
import java.time.LocalDateTime;
public class HelloJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobKey jobKey = jobExecutionContext.getJobDetail().getKey(); System.out.println("jobKey.getGroup() : " + jobKey.getGroup()); System.out.println("jobKey.getName() : " + jobKey.getName()); System.out.println("jobKey.getClass() : " + jobKey.getClass());
TriggerKey triggerKey = jobExecutionContext.getTrigger().getKey(); System.out.println("triggerKey.getGroup() : " + triggerKey.getGroup()); System.out.println("triggerKey.getName() : " + triggerKey.getName()); System.out.println("triggerKey.getClass() : " + triggerKey.getClass());
System.out.println(LocalDateTime.now()); } }
|
运行结果:
1 2 3 4 5 6 7 8 9 10
| jobKey.getGroup() : group1 jobKey.getName() : job1 jobKey.getClass() : class org.quartz.JobKey triggerKey.getGroup() : group1 triggerKey.getName() : trigger1 triggerKey.getClass() : class org.quartz.TriggerKey 2022-12-10T21:32:20.901
【部分运行结果略】
|
获取当前任务执行时间(.getFireTime()
)、下一次任务执行时间(.getNextFireTime()
),示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.kakawanyifan.job;
import org.quartz.*;
import java.time.LocalDateTime; import java.util.Date;
public class HelloJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Date fireTime = jobExecutionContext.getFireTime(); System.out.println("当前任务执行时间:" + fireTime);
Date nextFireTime = jobExecutionContext.getNextFireTime(); System.out.println("下一次任务执行时间:" + nextFireTime);
System.out.println(LocalDateTime.now()); } }
|
运行结果:
1 2 3 4 5 6 7 8 9 10 11
| 当前任务执行时间:Tue Jan 10 21:55:57 CST 2023 下一次任务执行时间:Tue Jan 10 21:56:02 CST 2023 2022-12-10T21:55:57.939 当前任务执行时间:Tue Jan 10 21:56:02 CST 2023 下一次任务执行时间:Tue Jan 10 21:56:07 CST 2023 2022-12-10T21:56:02.880 当前任务执行时间:Tue Jan 10 21:56:07 CST 2023 下一次任务执行时间:Tue Jan 10 21:56:12 CST 2023 2022-12-10T21:56:07.880
【部分运行结果略】
|
传递参数
- 可以在定义JobDetail实例时,通过
.usingJobData(jobDataMap)
进行参数传递。
- 可以在Job中通过
jobExecutionContext.getJobDetail().getJobDataMap()
解析参数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
public class App { public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("jobK1","jjj-1"); jobDataMap.put("jobK2",2);
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("job1", "group1") .usingJobData(jobDataMap) .build();
JobDataMap triggerDataMap = new JobDataMap(); triggerDataMap.put("triggerK1","ttt-1"); triggerDataMap.put("triggerK2",2.0);
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .usingJobData(triggerDataMap) .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).build();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.kakawanyifan.job;
import org.quartz.*;
import java.time.LocalDateTime;
public class HelloJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap(); for (String jobDataMapKey: jobDataMap.keySet()) { System.out.println(jobDataMapKey + " : " + jobDataMap.get(jobDataMapKey)); }
JobDataMap triggerDataMap = jobExecutionContext.getTrigger().getJobDataMap(); for (String triggerDataMapKey: triggerDataMap.keySet()) { System.out.println(triggerDataMapKey + " : " + triggerDataMap.get(triggerDataMapKey)); }
System.out.println(LocalDateTime.now()); } }
|
运行结果:
1 2 3 4 5 6 7 8
| jobK2 : 2 jobK1 : jjj-1 triggerK1 : ttt-1 triggerK2 : 2.0 2022-12-10T21:42:37.001
【部分运行结果略】
|
除了上文的getJobDataMap,也可以采取如下的方式传递参数,解析方法不变。

Setter方法
如果我们在Job类中添加成员变量及其Setter方法对应JobDataMap的键值,Quartz默认的JobFactory实现类在初始化JobDetail对象时会自动地调用这些Setter方法。
这样,解析参数会更方便一些,因为这些参数都作为了Job类的成员变量。
例子
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
public class HelloJob implements Job {
private String k1; private Integer k2;
public void setK1(String k1) { System.out.println("setK1 : " + k1); this.k1 = k1; }
public void setK2(Integer k2) { System.out.println("setK2 : " + k2); this.k2 = k2; }
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(k1); System.out.println(k2);
System.out.println(LocalDateTime.now()); } }
|
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("k1","kkk-1"); jobDataMap.put("k2",2);
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("job1", "group1") .usingJobData(jobDataMap) .build();
JobDataMap triggerDataMap = new JobDataMap(); triggerDataMap.put("k1","kkk-vvv-1"); triggerDataMap.put("k2",100);
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .usingJobData(triggerDataMap) .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).build();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start(); } }
|
运行结果:
1 2 3 4 5
| setK1 : kkk-vvv-1 setK2 : 100 kkk-vvv-1 100 2022-12-11T08:00:57.275
|
覆盖规则
如果遇到同名的key
,Trigger
中的.usingJobData()
会覆盖JobDetail
中的.usingJobData()
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDataMap triggerDataMap = new JobDataMap(); triggerDataMap.put("k1","kkk-vvv-1"); triggerDataMap.put("k2",100);
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .usingJobData(triggerDataMap) .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).build();
JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("k1","kkk-1"); jobDataMap.put("k2",2);
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("job1", "group1") .usingJobData(jobDataMap) .build();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start(); } }
|
运行结果:
1 2 3 4 5
| setK1 : kkk-vvv-1 setK2 : 100 kkk-vvv-1 100 2022-12-11T08:03:24.843
|
有状态的Job
所谓的有状态的Job是指多次Job调用期间可以持有一些状态信息,这些状态信息存储在JobDataMap中。
在实现方法上,在Job类上加上@PersistJobDataAfterExecution
注解即可。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.PersistJobDataAfterExecution;
import java.time.LocalDateTime;
@PersistJobDataAfterExecution public class HelloJob implements Job {
private String k1; private Integer k2;
public void setK1(String k1) { System.out.println("setK1 : " + k1); this.k1 = k1; }
public void setK2(Integer k2) { System.out.println("setK2 : " + k2); this.k2 = k2; }
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("k1 :" + k1); System.out.println("k2 :" + k2);
k2 = k2 + 1;
jobExecutionContext.getJobDetail().getJobDataMap().put("k2", k2);
System.out.println(LocalDateTime.now()); } }
|
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("job1", "group1") .usingJobData("k1","kv1") .usingJobData("k2",1) .build();
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).build();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start(); } }
|
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| setK1 : kv1 setK2 : 1 k1 :kv1 k2 :1 2022-12-11T08:07:33.029 setK1 : kv1 setK2 : 2 k1 :kv1 k2 :2 2022-12-11T08:07:37.933 setK1 : kv1 setK2 : 3 k1 :kv1 k2 :3 2022-12-11T08:07:42.934
|
有些资料说,添加@PersistJobDataAfterExecution
注解后,不会每次调用都创建一个新的实例,这个说法是错误的。实际上,还是每次都创建了新的实例。
(通过注解名字,我们也能知道,被持久化的,是JobData
,而不是JobDetail
。)
我们可以创建一个无参的构造方法,并打印一行内容,进行验证。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.PersistJobDataAfterExecution;
import java.time.LocalDateTime;
@PersistJobDataAfterExecution public class HelloJob implements Job {
public HelloJob() { System.out.println("HelloJob..."); }
private String k1; private Integer k2;
public void setK1(String k1) { System.out.println("setK1 : " + k1); this.k1 = k1; }
public void setK2(Integer k2) { System.out.println("setK2 : " + k2); this.k2 = k2; }
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("k1 :" + k1); System.out.println("k2 :" + k2);
k2 = k2 + 1;
jobExecutionContext.getJobDetail().getJobDataMap().put("k2", k2);
System.out.println(LocalDateTime.now()); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("job1", "group1") .usingJobData("k1","kv1") .usingJobData("k2",1) .build();
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).build();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start(); } }
|
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| HelloJob... setK1 : kv1 setK2 : 1 k1 :kv1 k2 :1 2022-12-11T08:10:17.520 HelloJob... setK1 : kv1 setK2 : 2 k1 :kv1 k2 :2 2022-12-11T08:10:22.459 HelloJob... setK1 : kv1 setK2 : 3 k1 :kv1 k2 :3 2022-12-11T08:10:27.458
|
Trigger
Quartz有多种触发器,使用最多的是SimpleTrigger和CronTrigger。
Trigger的属性
我们可以在Trigger中定义startTime
(开始时间)和endTime
(结束时间)。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.Trigger;
import java.time.LocalDateTime;
public class HelloJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(LocalDateTime.now());
Trigger trigger = jobExecutionContext.getTrigger(); System.out.println("name : " +trigger.getJobKey().getName()); System.out.println("group : " + trigger.getJobKey().getGroup()); System.out.println("任务开始时间 : " + trigger.getStartTime()); System.out.println("任务结束时间 : " + trigger.getEndTime()); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
Date startDate = new Date(); startDate.setTime(startDate.getTime() + 3000); Date endDate = new Date(); endDate.setTime(endDate.getTime() + 10000);
JobDetail job = JobBuilder.newJob(HelloJob.class) .withIdentity("job1", "group1") // 定义该实例唯一标识 .usingJobData("message", "打印日志") .build();
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startAt(startDate) .endAt(endDate) .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) .usingJobData("message", "simple触发器") .build();
scheduler.scheduleJob(job, trigger);
scheduler.start(); } }
|
运行结果:
1 2 3 4 5
| 2022-12-11T08:35:50.408 name : job1 group : group1 任务开始时间 : Wed Jan 11 08:35:50 CST 2023 任务结束时间 : Wed Jan 11 08:35:57 CST 2023
|
SimpleTrigger
SimpleTrigger的使用场景:在指定的时间启动,且以指定的间隔时间重复执行若干次。
例子一:在指定的时间启动,只执行一次。 示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
public class HelloJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(LocalDateTime.now());
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
Date startDate = new Date(); startDate.setTime(startDate.getTime() + 10000);
JobDetail job = JobBuilder.newJob(HelloJob.class) .build();
Trigger trigger = TriggerBuilder.newTrigger() .startAt(startDate) .build();
scheduler.scheduleJob(job, trigger);
scheduler.start(); } }
|
运行结果:
例子二:在指定的时间启动,每次间隔5秒,永久执行。 示例代码:
1 2 3 4
| Trigger trigger = TriggerBuilder.newTrigger() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) .startAt(startDate) .build();
|
例子三:在指定的时间启动,每次间隔5秒,永久执行。并在指定的时间结束 示例代码:
1 2 3 4 5
| Trigger trigger = TriggerBuilder.newTrigger() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) .startAt(startDate) .endAt(endDate) .build();
|
CronTrigger
CronTriggers:基于日历的作业调度器。
使用CronTrigger,你可以指定诸如"每个周五12:00"、"每周一、周三、周五的上午9:00到上午10:00之间每隔五分钟"等,这样日程安排来触发。
Cron表达式
这不是我们第一次讨论Cron表达式,在《Linux操作系统使用入门:2.命令》,我们也讨论过Cron表达式。
Linux中的Cron表达式一共5位,从左到右,分别是:
- 分钟
- 小时
- 日期
- 月份
- 星期
QuartZ中的Cron表达式,有7位,从左到右,分别是
- 秒
- 分钟
- 小时
- 日期
- 月份
- 星期
- 年(可选)
(粗体字标识的,是相较于Linux中的Cron表达式,多的两位。)
注意:
- 星期:用1到7来表示(1代表星期天),或者用字符串
SUN
、MON
、TUE
、WED
、THU
、FRI
和SAT
来表示。
- 这部分与在Linux中不同,Linux中1代表星期一。
- 月份,用1到12来表示,或者用字符串
JAN
、FEB
、MAR
、APR
、MAY
、JUN
、JUL
、AUG
、SEP
、OCT
、NOV
和DEC
来表示。
在《Linux操作系统使用入门:2.命令》,我们还讨论了一些特殊值:
特殊字符 |
含义 |
示例 |
* |
所有可能的值 |
在月域中,* 表示每个月;在星期域中,* 表示星期的每一天。 |
, |
列出枚举值 |
在分钟域中,5,20 表示分别在5分钟和20分钟触发一次。 |
- |
范围 |
在分钟域中,5-20 表示从5分钟到20分钟之间每隔一分钟触发一次。 |
/ |
指定数值的增量 |
在分钟域中,0/15 表示从第0分钟开始,每15分钟,3/20 表示从第3分钟开始,每20分钟。 |
在Cron中有更多的特殊值:
特殊字符 |
含义 |
示例 |
? |
表示不指定值 |
例如,"月份中的日期"和"星期中的日期"这两个元素是互斥的,因此应该通过设置一个问号? 来表明不想设置某个字段 |
L |
最后一个 |
在day-of-month中,L 表示这个月的最后一天,例如一月的31日、二月的28日。在day-of-week中,表示7 或者SAT 。但是如果在day-of-week中,这个字符跟在别的值后面,则表示"当月的最后一个的周XXX",例如:6L 或者FRIL ,表示当月的最后一个周五。注意,使用L 选项时,不要指定列表或者值范用。 |
W |
指定离给定日期最近的工作日(周一到周五) |
在日期域中5W ,如果5日是星期六,则将在最近的工作日星期五,即4日触发。如果5日是星期天,则将在最近的工作日星期一,即6日触发;如果5日在星期一到星期五中的一天,则就在5日触发。 |
# |
确定每个月第几个星期几,仅星期域支持该字符。 |
在星期域中,4#2 表示某月的第二个星期四。 |
0 0 10,14,16 * * ?
每天10:00、14:00、16:00
最近10次运行时间
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 10:00:00 2022-12-12 14:00:00 2022-12-12 16:00:00 2022-12-13 10:00:00 2022-12-13 14:00:00 2022-12-13 16:00:00 2022-12-14 10:00:00 2022-12-14 14:00:00 2022-12-14 16:00:00 2022-12-15 10:00:00
|
0 0/30 9-12 * * ?
每天9时到12时,0分0秒开始每隔30分钟发送一次。
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 09:00:00 2022-12-12 09:30:00 2022-12-12 10:00:00 2022-12-12 10:30:00 2022-12-12 11:00:00 2022-12-12 11:30:00 2022-12-12 12:00:00 2022-12-12 12:30:00 2022-12-13 09:00:00 2022-12-13 09:30:00
|
0 0 12 ? * WED
每个星期三,12:00:00
1 2 3 4 5 6 7 8 9 10
| 2022-12-18 12:00:00 2022-12-25 12:00:00 2023-02-01 12:00:00 2023-02-08 12:00:00 2023-02-15 12:00:00 2023-02-22 12:00:00 2023-03-01 12:00:00 2023-03-08 12:00:00 2023-03-15 12:00:00 2023-03-22 12:00:00
|
0 0 12 * * ?
每天,12:00:00
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 12:00:00 2022-12-13 12:00:00 2022-12-14 12:00:00 2022-12-15 12:00:00 2022-12-16 12:00:00 2022-12-17 12:00:00 2022-12-18 12:00:00 2022-12-19 12:00:00 2022-12-20 12:00:00 2022-12-21 12:00:00
|
0 15 10 ? * *
每天,10:15:00
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 10:15:00 2022-12-13 10:15:00 2022-12-14 10:15:00 2022-12-15 10:15:00 2022-12-16 10:15:00 2022-12-17 10:15:00 2022-12-18 10:15:00 2022-12-19 10:15:00 2022-12-20 10:15:00 2022-12-21 10:15:00
|
0 15 10 * * ?
每天,10:15:00
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 10:15:00 2022-12-13 10:15:00 2022-12-14 10:15:00 2022-12-15 10:15:00 2022-12-16 10:15:00 2022-12-17 10:15:00 2022-12-18 10:15:00 2022-12-19 10:15:00 2022-12-20 10:15:00 2022-12-21 10:15:00
|
0 15 10 * * ? *
每天,10:15:00
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 10:15:00 2022-12-13 10:15:00 2022-12-14 10:15:00 2022-12-15 10:15:00 2022-12-16 10:15:00 2022-12-17 10:15:00 2022-12-18 10:15:00 2022-12-19 10:15:00 2022-12-20 10:15:00 2022-12-21 10:15:00
|
0 15 10 * * ? 2099
2099年,10:15:00
1 2 3 4 5 6 7 8 9 10
| 2099-01-01 10:15:00 2099-01-02 10:15:00 2099-01-03 10:15:00 2099-01-04 10:15:00 2099-01-05 10:15:00 2099-01-06 10:15:00 2099-01-07 10:15:00 2099-01-08 10:15:00 2099-01-09 10:15:00 2099-01-10 10:15:00
|
0 * 14 * * ?
每天,14点,的每一分钟的0秒。
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 14:00:00 2022-12-12 14:01:00 2022-12-12 14:02:00 2022-12-12 14:03:00 2022-12-12 14:04:00 2022-12-12 14:05:00 2022-12-12 14:06:00 2022-12-12 14:07:00 2022-12-12 14:08:00 2022-12-12 14:09:00
|
0 0/55 14 * * ?
每天,14点,0分0秒。55分钟后。
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 14:00:00 2022-12-12 14:55:00 2022-12-13 14:00:00 2022-12-13 14:55:00 2022-12-14 14:00:00 2022-12-14 14:55:00 2022-12-15 14:00:00 2022-12-15 14:55:00 2022-12-16 14:00:00 2022-12-16 14:55:00
|
0 0/55 14,18 * * ?
每天,14点,0分0秒。55分钟后。以及18点,0分0秒。55分钟后。
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 14:00:00 2022-12-12 14:55:00 2022-12-12 18:00:00 2022-12-12 18:55:00 2022-12-13 14:00:00 2022-12-13 14:55:00 2022-12-13 18:00:00 2022-12-13 18:55:00 2022-12-14 14:00:00 2022-12-14 14:55:00
|
0 0-5 14 * * ?
每天,14点,0分到5分,0秒。
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 14:00:00 2022-12-12 14:01:00 2022-12-12 14:02:00 2022-12-12 14:03:00 2022-12-12 14:04:00 2022-12-12 14:05:00 2022-12-13 14:00:00 2022-12-13 14:01:00 2022-12-13 14:02:00 2022-12-13 14:03:00
|
0 10 14 ? 3 WED
每年三月的星期三的,14:10
1 2 3 4 5 6 7 8 9 10
| 2023-03-01 14:10:00 2023-03-08 14:10:00 2023-03-15 14:10:00 2023-03-22 14:10:00 2023-03-29 14:10:00 2024-03-06 14:10:00 2024-03-13 14:10:00 2024-03-20 14:10:00 2024-03-27 14:10:00 2025-03-05 14:10:00
|
0 15 10 ? * MON-FRI
周一至周五的,10:15触发
1 2 3 4 5 6 7 8 9 10
| 2022-12-12 10:15:00 2022-12-13 10:15:00 2022-12-16 10:15:00 2022-12-17 10:15:00 2022-12-18 10:15:00 2022-12-19 10:15:00 2022-12-20 10:15:00 2022-12-23 10:15:00 2022-12-24 10:15:00 2022-12-25 10:15:00
|
0 15 10 15 * ?
每月15日,10:15:00
1 2 3 4 5 6 7 8 9 10
| 2022-12-15 10:15:00 2023-02-15 10:15:00 2023-03-15 10:15:00 2023-04-15 10:15:00 2023-05-15 10:15:00 2023-06-15 10:15:00 2023-07-15 10:15:00 2023-08-15 10:15:00 2023-09-15 10:15:00 2023-10-15 10:15:00
|
0 15 10 L * ?
每月最后一日的上午10:15触发
1 2 3 4 5 6 7 8 9 10
| 2022-12-31 10:15:00 2023-02-28 10:15:00 2023-03-31 10:15:00 2023-04-30 10:15:00 2023-05-31 10:15:00 2023-06-30 10:15:00 2023-07-31 10:15:00 2023-08-31 10:15:00 2023-09-30 10:15:00 2023-10-31 10:15:00
|
0 15 10 ? * 6L
每月的最后一个星期五上午10:15触发
1 2 3 4 5 6 7 8 9 10
| 2022-12-27 10:15:00 2023-02-24 10:15:00 2023-03-31 10:15:00 2023-04-28 10:15:00 2023-05-26 10:15:00 2023-06-30 10:15:00 2023-07-28 10:15:00 2023-08-25 10:15:00 2023-09-29 10:15:00 2023-10-27 10:15:00
|
0 15 10 ? * 6#3
每月的第三个星期五上午10:15触发
1 2 3 4 5 6 7 8 9 10
| 2022-12-20 10:15:00 2023-02-17 10:15:00 2023-03-17 10:15:00 2023-04-21 10:15:00 2023-05-19 10:15:00 2023-06-16 10:15:00 2023-07-21 10:15:00 2023-08-18 10:15:00 2023-09-15 10:15:00 2023-10-20 10:15:00
|
应用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package com.kakawanyifan;
import com.kakawanyifan.job.HelloJob; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory;
public class HelloSchedule { public static void main(String[] args) throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail job = JobBuilder.newJob(HelloJob.class) .build();
Trigger trigger = TriggerBuilder.newTrigger() .withSchedule(CronScheduleBuilder.cronSchedule("0/30 * * * * ?")) .build();
scheduler.scheduleJob(job, trigger);
scheduler.start(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.kakawanyifan.job;
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
public class HelloJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(LocalDateTime.now());
} }
|
运行结果:
1 2 3 4 5
| 2022-12-12T08:32:00.054 2022-12-12T08:32:30.003 2022-12-12T08:33:00.002 2022-12-12T08:33:30.015 2022-12-12T08:34:00.018
|
Quartz.properties
在quartz-2.3.2.jar
的内部,org/quartz
目录下默认的配置文件quartz.properties
,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| org.quartz.scheduler.instanceName: DefaultQuartzScheduler org.quartz.scheduler.rmi.export: false org.quartz.scheduler.rmi.proxy: false org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount: 10 org.quartz.threadPool.threadPriority: 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
|
调度器属性:
org.quartz.scheduler.instanceName
,调度器的实例名。用来区分特定的调度器实例,可以按照功能用途来给调度器起名。
org.quartz.scheduler.instanceId
,调度器的实例ID。和前者一样,也允许任何字符串,但这个值必须在所有调度器实例中是唯一的,尤其是在一个集群环境中,作为集群的唯一key。假如你想Quartz帮你生成这个值的话,可以设置为AUTO,大多数情况设置为AUTO即可。
线程池属性:
org.quartz.threadPool.threadCount
,处理Job的线程个数,至少为1,但最多的话最好不要超过100。
org.quartz.threadPool.threadPriority
,线程的优先级,优先级别高的线程比级别低的线程优先得到执行。最小为1,最大为10,默认为5
org.quartz.threadPool.class
,线程池的实现类。
作业存储设置:
org.quartz.jobStore.class
,Job存储实现类,在本文中,Job存储在内存里。
我们也可以在项目的资源下添加quartz.properties
文件,去覆盖底层的配置文件。
整合Spring
spring-context-support
需要引入Jar包spring-context-support
:
1 2 3 4 5
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.3.24</version> </dependency>
|
Job类
新建Job类,继承QuartzJobBean
。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.kakawanyifan.job;
import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.scheduling.quartz.QuartzJobBean;
import java.time.LocalDateTime;
public class QuartzJob extends QuartzJobBean {
@Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { System.out.println(LocalDateTime.now()); } }
|
配置类
新建一个配置类,配置类中做的事情,其实就是我们上文的App类中的Main方法做的事情。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.kakawanyifan.config;
import com.kakawanyifan.job.QuartzJob; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; import org.springframework.scheduling.quartz.JobDetailFactoryBean; import org.springframework.scheduling.quartz.SchedulerFactoryBean;
public class QuartzJobConfig { @Bean public JobDetailFactoryBean jobDetail(){ JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean(); jobDetailFactoryBean.setName("n1"); jobDetailFactoryBean.setGroup("g1"); jobDetailFactoryBean.setJobClass(QuartzJob.class); return jobDetailFactoryBean; }
@Bean public CronTriggerFactoryBean cronTrigger(JobDetailFactoryBean jobDetail){ CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); cronTriggerFactoryBean.setName("n1"); cronTriggerFactoryBean.setGroup("g1"); cronTriggerFactoryBean.setJobDetail(jobDetail.getObject()); cronTriggerFactoryBean.setCronExpression("0/5 * * * * ?"); return cronTriggerFactoryBean; }
@Bean public SchedulerFactoryBean scheduler(CronTriggerFactoryBean cronTrigger){ SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); schedulerFactoryBean.setTriggers(cronTrigger.getObject()); return schedulerFactoryBean; } }
|
当然,该配置类需要被Import到Spring的配置类中。
@Import({JdbcConfig.class, MyBatisConfig.class, QuartzJobConfig.class})
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package com.kakawanyifan.config;
import org.springframework.context.annotation.*; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration @ComponentScan(value = "com.kakawanyifan", excludeFilters = @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = Controller.class )) @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class, MyBatisConfig.class, QuartzJobConfig.class}) @EnableTransactionManagement public class SpringConfig {
}
|
XML方式
新建一个XML文件quartzConfig.xml
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"> <property name="name" value="n1" /> <property name="group" value="g1" /> <property name="jobClass" value="com.kakawanyifan.job.QuartzJob" /> </bean>
<bean id="trigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="name" value="n1" /> <property name="group" value="g1" /> <property name="jobDetail" ref="jobDetail" /> <property name="cronExpression" value="0/5 * * * * ?" /> </bean>
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="trigger" /> </list> </property> </bean>
</beans>
|
同样的,该配置文件需要被Import到Spring的配置文件中。
<import resource="classpath:quartzConfig.xml" />
有些资料会介绍另一种方法,将Job类的Bean也交给Spring管理,然后在"JobDetailFactoryBean"指定"TargetObject"。
在5版本的Spring中已经找不到这种方法。
在上文我们讨论过Job的生命周期,每次执行都会实例化,而在Spring中默认Bean是单例的。
这样的设计,存在冲突,这或许是这种方法被弃用的原因。
Spring中的定时任务
除了Quartz,Spring中也自带了定时任务。
@EnableScheduling
@EnableScheduling
注解,开启定时任务设置。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.kakawanyifan.config;
import org.springframework.context.annotation.*; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration @ComponentScan(value = "com.kakawanyifan", excludeFilters = @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = Controller.class )) @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class, MyBatisConfig.class, QuartzJobConfig.class}) @EnableTransactionManagement @EnableScheduling public class SpringConfig {
}
|
@Scheduled
@Scheduled
注解,定义定时任务。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.kakawanyifan.job;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component public class SpringJob {
@Scheduled(cron = "0/10 * * * * ?") public void runJob(){ System.out.println("SpringJob : " + LocalDateTime.now()); } }
|
@Scheduled
注解的参数有:
cron
:cron表达式进行执行。
@Scheduled(cron = "0/10 * * * * ?")
fixedDelay
,上次调用结束后,指定毫秒后再执行。
@Scheduled(fixedDelay = 5000)
:上次调用结束后5秒再执行。
fixedDelayString
,与fixedDelay
含义相同。只是使用字符串的形式,可以支持占位符。
@Scheduled(fixedDelayString = "${time.fixedDelay}")
fixedRate
,无论上次是否结束,指定毫秒之后都会再次执行。
@Scheduled(fixedRate = 5000)
:上次开始无论是否结束5秒钟之后会再次执行
fixedRateString
,与fixedRate
含义相同。只是使用字符串的形式,可以支持占位符。
initialDelay
,初次执行延时时间。
initialDelayString
,与initialDelay
含义相同。只是使用字符串的形式,支持占位符。
XML方式
xmlns:task="http://www.springframework.org/schema/task"
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
【部分代码略】
<bean id="springJob" class="com.kakawanyifan.job.SpringJob"></bean> <task:scheduler id="springJobScheduler"/> <task:scheduled-tasks scheduler="springJobScheduler"> <task:scheduled ref="springJob" method="runJob" cron="0/10 * * * * *"/> </task:scheduled-tasks>
</beans>
|
除了上文的配置方式,也可以开启注解<task:annotation-driven/>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
【部分代码略】
<task:annotation-driven/>
</beans>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.kakawanyifan.job;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component public class SpringJob {
@Scheduled(cron = "0/10 * * * * *") public void runJob(){ System.out.println("SpringJob : " + LocalDateTime.now()); } }
|
在Quartz
中,还有一部分是监听器。有JobListener
(任务监听器)、TriggerListener
(触发监听器)和SchedulerListener
(调度监听器)三种。
在实际工作中,监听器的应用并不多。
我们不讨论。