Java定时任务调度工具

什么是定时任务调度呢?

基于给定的时间点给定的时间间隔或者给定的执行次数自动执行的任务

Java 中最常见的两款定时任务调度工具就是 Timer 和 Quartz,一般来说 Timer 能解决 60% 的需求,解决不了的就交给大哥 Quartz 了,Timer 是 JDK 自带的,不需要其他依赖,而 Quartz 是开源软件。
需要注意的是:能用 Timer 实现的就用 Timer,因为大哥的出场费是很贵的….

Timer

简单定义:
有且只有一个后台线程多个业务线程进行定时定频率的调度
构件关系:
Timer (包含有一个队列和一个后台线程)定时调用 TimerTask

我记得在上篇文章中我是写过关于 Timer 的,具体内容可以点击这里回看,这里就简单复习或者补充下:

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
public class Main {
public static void main(String[] args) {
Timer timer = new Timer();

timer.schedule(new MyTask(), 0, 1000);
// timer.scheduleAtFixedRate(new MyTask(), 0, 1000);

// 取消所有任务
timer.cancel();

// 返回取消任务的个数
int s = timer.purge();
}

public class MyTask extends TimerTask {
@Override
public void run() {
System.out.println("task run!!");

// 获取最近一次任务执行的时间
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(scheduledExecutionTime()));

// 取消任务
// cancel();
}
}
}

这里还是要再次说明:使用 Timer 执行周期性任务时,出现异常后会自动退出(全部任务),因为它是基于单线程的。所以应该尽量使用 ScheduledExecutorService (支持周期任务的线程池)的方式来创建。

1
2
3
4
5
6
7
8
ScheduledExecutorService scheduledExecutorService = newScheduledThreadPool(3);
scheduledExecutorService.scheduleAtFixedRate(new MyTask, 0, 1, TimeUnit.SECONDS);

// 结束线程池正在执行的任务,不再接受新任务,等待当前任务完成
scheduledExecutorService.shutdown();

// 线程池里的任务是否全部完成
scheduledExecutorService.isTerminated();

通过上面,知道 Timer 主要的方法有两个 schedule 和 scheduleAtFixedRate;然后来说说他们最大的区别:

  • 首次执行时间早于当前时间
    也就是说规定 12:00 执行 task,但是 12:05 的时候才执行到 schedule / scheduleAtFixedRate;
    对于这种情况,schedule 会以当前时间为准,然后间隔指定时间重复执行;
    对于 scheduleAtFixedRate 它会尽可能的多执行几次以赶上落下的任务,比如说规定没 2 分钟执行一次,那么它会在执行 scheduleAtFixedRate 后连续执行两次 task 来弥补缺失的“工作量”
  • 执行耗时超过了间隔时间
    也就是说规定每隔 3 秒执行一次,但是 task 3 秒还没执行完的情况;
    对于 scheduleAtFixedRate,当 task 超时后,第二次会很快被执行,它的间隔计算方式是程序开始执行的时间;
    对于 schedule ,它的间隔计算方式是程序执行完后计时的,也就是说规定每隔 3 秒执行一次,task 耗时 5 秒,task 执行完后再等 3 秒才会执行第二次,从运行开始算的话也就是差了 8 秒

他们两个方法的主要区别就集中在这两种情况上了,缺点也可以看出来了,因为是单线程所以在处理并发时效果会非常的不理想,基本不会做到并发执行;还有就是抛出异常所有的任务都会终止了。
所以在对时效性要求较高的多任务并发作业对复杂任务的调度(可能抛出异常)Timer 是不适合的

Quartz

然后就到了介绍大哥的时候了,它的强大就不用说了,你想实现的它基本都能搞定,听说也是作为 Spring 默认的调度框架,并且它的分布式和集群能力也不错。
Quartz 的核心有三个:调度器、任务、触发器;可以对应为 Scheduler、Job、Trigger。
简单的流程就是:调度器根据触发器来执行任务

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
public class HelloScheduler {
public static void main(String[] args) throws SchedulerException {
// 1.创建 JobDetail 实例,和 job 类进行绑定
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
.withIdentity("myJob", "group1")
// 传入自定义参数
.usingJobData("msg","Loli")
.build();

// 2.创建一个 trigger 实例,来控制执行的规则
Trigger trigger = TriggerBuilder.newTrigger()
// 这里的组和上面的完全不一样,虽然名字一样,但不是在一个类
.withIdentity("myTrigger", "group1")
// 传入自定义参数
.usingJobData("age", 12)
// 立即执行
.startNow()

// 每隔两秒执行一次,直到永远,使用 SimpleSchedule
// .withSchedule(SimpleScheduleBuilder.simpleSchedule()
// .withIntervalInSeconds(2)
// .repeatForever())

// 使用 CronTrigger 和 Cron 表达式
.withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *"))

.build();

// 3.创建 Scheduler 实例
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
// 按照 trigger 指定的日期执行 jobDetail
// 它返回最近一次要执行的时间
scheduler.scheduleJob(jobDetail, trigger);

// 挂起,可以被重启
scheduler.standby();

// 终止,不能被重启,如果传入 true 表示会等待任务结束后才(标记为)终止,默认 false
scheduler.shutdown();
}
}

上面代码结合下面的解释看最好啦,这也仅仅只是一个简单的小例子

Job

实现业务逻辑的任务接口,没错它是个接口,非常容易实现,只有一个 execute 方法,相当于 TimerTask 的 run 方法;在里面编写逻辑即可,当任务执行失败时会抛出 JobExecutionException 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HelloJob implements Job {

public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(MessageFormat.format("当前时间:{0}", new Date()));

// System.out.println("Hello World!");

// jobExecutionContext.getJobDetail().getKey().getName();
// jobExecutionContext.getJobDetail().getKey().getGroup();

// jobExecutionContext.getTrigger().getKey().getName();
// jobExecutionContext.getTrigger().getKey().getGroup();

// 获取自定义的参数
String msg = jobExecutionContext.getJobDetail().getJobDataMap().getString("msg");
Integer age = jobExecutionContext.getTrigger().getJobDataMap().getInt("age");
System.out.println(msg + "::" + age);

// 合并后的大 Map,如果有相同的 key 以 Trigger 为准
jobExecutionContext.getMergedJobDataMap();
}
}

这是一个简单的 Job,只是展示部分常用的,结合下面的说明阅读比较好~

生命周期

每次调度器(Scheduler)调用执行 job 的 execute 方法前会会创建一个新的 job 实例,当调用完成后,关联的 job 实例会被释放,释放的实例就会被 GC 回收

JobExecutionContext

当调度器调用一个 Job ,就会将 JobExecutionContext 传递给 job 的 execute 方法;这样 Job 就可以通过 JobExecutionContext 来访问 Quartz 运行时候的环境以及 Job 本身的明细数据。
从另一方面 JobExecutionContext 就是为了解决不同 Job 需要不同参数的问题。
通过 JobExecutionContext 对象,可以获得 JobDetail 或者 Trigger 设置的自定义参数。

JobDataMap

在进行任务调度时,JobDataMap 存储在 JobExecutionContext 中,非常方便获取。
JobDataMap 可以用来装载任何可序列化的数据对象,当 job 实例执行时,这些参数对象会传递给它。
JobDataMap 实现了 JDK 的 Map 接口,并添加了一些非常方便的方法用来存取基本数据类型

关于获取,除了使用 JobExecutionContext ,还有另一种方式,那就是在 Job 的实现类里添加相应的成员变量,并且设置 setter 方法,默认的 JobFactory 实现类在初始化 Job 实例的时候会自动调用这些 setter 方法,把 JobDataMap 中相应的值放进去

JobDetail

JobDetail 为 job 实例提供了许多设置属性,以及 JobDataMap 成员变量属性,它用来存储特定的 job 实例的状态信息,调度器需要借助 JobDetail 对象来添加 Job 实例
简单说就是 JobDetail 是用来绑定 Job 实例的,并携带一些 Job 实例没有携带的状态信息。
下面看看它的一些重要属性:
name 和 group ,这两个是必须的,group 的默认值是 DEFAULT;还有 jobClass 也是必须的,就是绑定的 Job 类;此外还有一个 JobDataMap ,可简单理解为是来传递数据的。
查看可以使用:jd.getKey().getName()/getGroup();

Trigger

Quartz 中的触发器用来告诉调度程序作业什么时候触发。即 Trigger 对象是用来触发执行 Job 的。
触发器的通用属性:

  • JobKey
    表示 Job 实例的标识,触发器被触发时,该指定的 Job 实例会执行
  • StartTime
    触发器的时间表首次被触发的时间,类型是 Date,startAt()
  • EndTime
    指定触发器的不再被触发的时间,类型也是 Date,endAt(),它的优先级比设置的重复执行次数高

SimpleTrigger

首先来看它的作用:
在一个指定时间段内执行一次作业任务,或者是在指定的时间间隔多次执行作业任务。
我们通过 TriggerBuilder.newTrigger() 代码实际上就是生成了一个 SimpleTrigger

CronTrigger

它的作用:
基于日历的作业调度器,它不像 SimpleTrigger 那样精确的控制,但是更加常用。
说到 CronTrigger 就必须得说 Cron 表达式了,熟悉 Linux 的可能接触过,因为 Linux 有一个 crontab 命令来控制计划任务,也是 Cron 表达式。
简单说,Cron 表达式是由 7 个子表达式组成的字符串,描述了时间表的详细信息,格式:
[秒] [分] [小时] [日] [月] [周] [年]
* :表示“每”的意思,放在第一个位置就是每秒。
? :表示不确定,不指定值,就是不关心。
, :表示或的意思。
- :表示至的意思,就是范围啦。
/ :表示的也是每的意思,举个例子,如果出现在秒的位置 0/5 就是从 0 秒开始,每隔 5 秒钟。
# :表示第,例如在周的位置有 5#3 意思就是第三周的星期四
L :意思是 List,在周位置 3L 就表示最后一周的周二。
需要注意的是,时分秒范围是从 0 开始,日月周就是 1 开始了,年可以省略

在西方,星期天是一个星期的第一天。所以星期天是 1 ,以此类推

掌握了表达式其他的就不是问题了:
cron表达式.png

月周可以使用英文单词的前三个字母,有在线生成 Cron 表达式的工具哦

Scheduler

调度器 Scheduler 可以说是 Quartz 的发动机,它是通过工厂模式来创建的,常用的两个是 StdSchedulerFactory 和 DirectSchedulerFactory。
常用的还是 StdSchedulerFactory 因为它允许使用声明式的配置,也就是可以配置到 XML/Properties 文件中,而 DirectSchedulerFactory 生成的只允许在代码中配置。
StdSchedulerFactory 默认会加载工程目录下的 quartz.properties ,如果不存在就会去读取自带的配置文件(jar 中),在配置文件中可以配置调度器属性、线程池属性(如线程数)、作业存储位置、插件配置等,可参考 jar 包里的配置。

Quartz与Spring整合

Quartz 与 Spring 能够进行完美的整合,用的也是比较多,下面就赶紧来学习一下,首先,必要的一些依赖:

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
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
</dependency>

然后是在 Spring 的配置文件中配置必要的 Bean 了,没办法,总要写配置文件的,或者使用 SpringBoot ?
在 Spring 中配置使用 Quartz 有两种方式:
MethodInvokingJobDetailFactoryBean 和 JobDetailFactoryBean,它们主要是来确定要执行的任务的,也就是 Job

MethodInvokingJobDetailFactoryBean

使用这种方式,只需要写一个普通的 Bean 即可

1
2
3
4
5
6
<!-- 相当于设置执行的 Job 为 myBean 的 printMessage 方法 -->
<bean id="simpleJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="myBean" />
<property name="targetMethod" value="printMessage" />
</bean>

对应执行的 Bean:

1
2
3
4
5
6
7
8
@Component("myBean")
public class MyBean {
public void printMessage() {
Date date = new Date();
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("MyBean Executes!" + sf.format(date));
}
}

这是一种相对简单的方法,然后都猜得出简单的方式定制性不太高,如果逻辑太复杂可能就无能为力了

JobDetailFactoryBean

当需要给作业传递数据,想要更加灵活的话就使用这种方式,配置文件:

1
2
3
4
5
6
7
8
9
10
11
<bean id="firstComplexJobDetail"
class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass"
value="com.imooc.springquartz.quartz.FirstScheduledJob" />
<property name="jobDataMap">
<map>
<entry key="anotherBean" value-ref="anotherBean" />
</map>
</property>
<property name="Durability" value="true"/>
</bean>

因为 firstComplexJobDetail 是通过代码的方式创建的并不是 Spring 容器注入的,启动时可能会报错,提示没有绑定触发器,这里使用的是 Spring 来配置的 trigger,它扫描不到,可以把 Durability 属性设置为 true 表示即使没有绑定触发器也会将其保存在任务容器中。
通过 jobClass 来指定 Job 类,对这个类的要求就是要继承 QuartzJobBean,实现 executeInternal 方法,这个还是比较熟悉的,和最开始我们学习时写法类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class FirstScheduledJob extends QuartzJobBean{
private AnotherBean anotherBean;

public void setAnotherBean(AnotherBean anotherBean){
this.anotherBean = anotherBean;
}

@Override
protected void executeInternal(JobExecutionContext arg0)
throws JobExecutionException {
Date date = new Date();
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("FirstScheduledJob Executes!" + sf.format(date));
this.anotherBean.printAnotherMessage();
}
}

/* -----分割线------ */
@Component("anotherBean")
public class AnotherBean {
public void printAnotherMessage() {
System.out.println("AnotherMessage");
}
}

这里加了点“难度”,设置自定参数时,我们可以设置对象,通过 value-ref 实现了自动注入,还是挺爽的。

配置Trigger和Scheduler

有了任务 Job,Quartz 还差两大核心,下面就来搞一下,完善 Spring 的配置文件:

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
<!-- 距离当前时间1秒之后执行,之后每隔两秒钟执行一次 -->
<bean id="mySimpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="jobDetail" ref="simpleJobDetail"/>
<property name="startDelay" value="1000"/>
<property name="repeatInterval" value="2000"/>
</bean>

<!-- 每隔5秒钟执行一次 -->
<bean id="myCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="firstComplexJobDetail"/>
<property name="cronExpression" value="0/5 * * ? * *"/>
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="jobDetails">
<list>
<ref bean="simpleJobDetail"/>
<ref bean="firstComplexJobDetail"/>
</list>
</property>
<property name="triggers">
<list>
<ref bean="mySimpleTrigger"/>
<ref bean="myCronTrigger"/>
</list>
</property>
</bean>

定义 Trigger 的时候也完全活用了上面学习的两种方式,一个使用 SimpleTrigger ,一个使用 CronTrigger;
这样当程序启动后就会执行配置的两个 Job 了,simpleJobDetail 使用方式一运行,firstComplexJobDetail 使用方式二运行,还是挺和谐的。
上面的也仅仅都是 Quartz 的初级使用,高级的并发、异常处理、持久化等没有涉及,以后有机会再补充吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~