Spring Quartz 定时器任务

时间:2022-04-23 23:29:52

Spring Quartz 定时器任务

本文介绍了Spring Quartz使用定时器任务的2种方式

  • 基于XML文件的方式
  • 基于注解的方式

除此之外,还较少了Cron表达式的基本使用

Spring Quartz部分有如下一些核心概念:

  1. Scheduler是一个计划调度器容器,容器里面可以很多的JobDetail和Trigger,当容器启动后,里面的每个JobDetail都会根据Trigger按部就班自动去执行。容器中有一个线程池,用来并行调度执行每个作业,这样可以提高容器效率

  2. JobDetail是一个可执行的工作,它本身可能是有状态的

  3. Trigger代表一个任务执行计划的配置,什么时候去启动一个Job。

  4. 当JobDetail和Trigger在Scheduler容器上注册后,形成了装配好的作业(JobDetail和Trigger所组成的一对儿),就可以伴随容器启动而调度执行了。

项目源码地址:https://github.com/upshi/spring-quartz

1. 环境介绍

Spring:4.3.6.RELEASE
Quzrtz:2.2.3

pom.xml文件部分相关配置如下:

<properties>
<spring.version>4.3.6.RELEASE</spring.version>
<quartz.version>2.2.3</quartz.version>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<slf4j.version>1.7.21</slf4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

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

<!-- ################################# QUARTZ #################################### -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>${quartz.version}</version>
</dependency>

<!-- ################################# LOG #################################### -->
<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>

2. 基于XML文件的方式

2.1 创建Job类

使用Quartz非常简单,只要写一个Job类,不需要继承或者实现任何类,包含定时器需要执行任务的代码即可,如下,我创建了一个类XMLBasedJob.java,这里的逻辑非常简单,只是定时在控制台输出一段话。

package cn.upshi.springquartz.job;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* spring-quartz XMLBasedJob
* 描述:
* 时间:2017-2-27 15:04.
*/


public class XMLBasedJob {

private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

//具体执行定时器任务逻辑的方法
public void execute() {
System.out.println(sdf.format(new Date()) + " 执行了定时任务 XMLBasedJob");
}

}

2.2 配置Spring的XML文件

要使上述的Job类中的定时器任务生效,需要在Spring的配置文件里配置4部门内容

1.配置Job类的Bean

<!-- 自定义的Job类 -->
<bean id="xmlBasedJob" class="cn.upshi.springquartz.job.XMLBasedJob" />

2.配置JobDetail

<!-- 一个可执行的定时器 -->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 指定Job类 -->
<property name="targetObject" ref="xmlBasedJob" />
<!-- 指定Job执行的方法 -->
<property name="targetMethod" value="execute" />
</bean>

3.配置Job触发器

<!-- Job的触发器 -->
<bean id="jobTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail" />
<!-- 每5秒运行一次 -->
<property name="cronExpression" value="0/5 * * * * ?" />
</bean>

4.配置Job调度器

<!-- Job调度器 -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="jobTrigger" />
</list>
</property>
</bean>

至此,Spring的配置文件就完成了。

spring.xml完整的配置文件如下:

<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd "
>


<!-- 自动扫描(自动注入) -->
<context:component-scan base-package="cn.upshi.springquartz"/>

<!-- 自定义的Job类 -->
<bean id="xmlBasedJob" class="cn.upshi.springquartz.job.XMLBasedJob"/>

<!-- 一个可执行的定时器 -->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 指定Job类 -->
<property name="targetObject" ref="xmlBasedJob"/>
<!-- 指定Job执行的方法 -->
<property name="targetMethod" value="execute"/>
</bean>
<!-- Job的触发器 -->
<bean id="jobTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail"/>
<!-- 每5秒运行一次 -->
<property name="cronExpression" value="0/5 * * * * ?"/>
</bean>

<!-- Job调度器 -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="jobTrigger"/>
</list>
</property>
</bean>
</beans>

2.3 编写测试类

编写一个测试类查看运行效果

package cn.upshi.springquartz;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* spring-quartz cn.upshi.springquartz
* 描述:
* 时间:2017-2-27 18:27.
*/


public class TestXMLBasedJob {

public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
}

}

执行结果如下:

2017-02-27 19:08:40 执行了定时任务 XMLBasedJob
2017-02-27 19:08:45 执行了定时任务 XMLBasedJob
2017-02-27 19:08:50 执行了定时任务 XMLBasedJob
2017-02-27 19:08:55 执行了定时任务 XMLBasedJob
2017-02-27 19:09:00 执行了定时任务 XMLBasedJob
2017-02-27 19:09:05 执行了定时任务 XMLBasedJob
2017-02-27 19:09:10 执行了定时任务 XMLBasedJob
...

3. 基于注解的方式

3.1 创建Job类

基于注解的方式比基于XML文件的方式更加简单,首先,还是需要创建一个Job类,如下,我创建了一个类AnnotationBasedJob.java

package cn.upshi.springquartz.job;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* spring-quartz AnnotationBasedJob
* 描述:
* 时间:2017-2-27 15:04.
*/


@Component
public class AnnotationBasedJob {

private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

//执行计划,每5秒执行一次
@Scheduled(cron = "0/5 * * * * *")
public void execute() {
System.out.println(sdf.format(new Date()) + " 执行了定时任务 AnnotationBasedJob");
}

}

在以上代码中,使用@Component注解,spring会自动将该类加入Spring的容器中。
此外,最核心的一个注解是@Scheduled,它定义了一个执行计划,每5秒执行一次。关于cron表达式的含义,将在后面进行介绍。

3.2 配置Spring,以支持注解的方式

首先在spring.xml配置文件头部引入相应的命名空间
xmlns:

xmlns:task="http://www.springframework.org/schema/task"  

xsi:schemaLocation

http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd

开启使用注解的配置

<!-- 基于注解方式的定时器 -->
<task:annotation-driven/>

至此,Spring的配置文件就完成了。

spring.xml完整的配置文件如下:

<?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-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd"
>


<!-- 自动扫描(自动注入) -->
<context:component-scan base-package="cn.upshi.springquartz"/>

<!-- 基于注解方式的定时器 -->
<task:annotation-driven/>
</beans>

3.3 编写测试类

仍然使用2.3节的测试类查看运行效果

package cn.upshi.springquartz;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* spring-quartz cn.upshi.springquartz
* 描述:
* 时间:2017-2-27 18:27.
*/


public class TestXMLBasedJob {

public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
}

}

执行结果如下:

2017-02-27 19:20:20 执行了定时任务 AnnotationBasedJob
2017-02-27 19:20:25 执行了定时任务 AnnotationBasedJob
2017-02-27 19:20:30 执行了定时任务 AnnotationBasedJob
2017-02-27 19:20:35 执行了定时任务 AnnotationBasedJob
2017-02-27 19:20:40 执行了定时任务 AnnotationBasedJob
2017-02-27 19:20:45 执行了定时任务 AnnotationBasedJob
2017-02-27 19:20:50 执行了定时任务 AnnotationBasedJob
...

3.4 两个DEBUG级别的异常问题

在使用注解方式时,如果log4j的日志级别设置成DEBUG,将会看到控制台输出了两个DEBUG级别异常,
No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
如下:

[DEBUG] - [2017-02-27 19:29:05 775] - [org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.main:207] - Could not find default TaskScheduler bean
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:166)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:138)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:381)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:335)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:855)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
at cn.upshi.springquartz.TestXMLBasedJob.main(TestXMLBasedJob.java:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
[DEBUG] - [2017-02-27 19:29:05 778] - [org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.main:219] - Could not find default ScheduledExecutorService bean
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.scheduling.TaskScheduler] is defined
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:372)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:332)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:192)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:171)
at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:86)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:166)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:138)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:381)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:335)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:855)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
at cn.upshi.springquartz.TestXMLBasedJob.main(TestXMLBasedJob.java:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

但是,定时器完全可以正常运行,这是为什么呢?!!

仔细一看,这个异常的级别不是ERROR,也不是WARNING,竟然是DEBUG!!!

通过跟踪源码后发现,原来,Spring的定时任务调度器会尝试获取一个注册过的 Task Scheduler来做任务调度,它会尝试通过BeanFactory.getBean的方法来获取一个注册过的Scheduler Bean,获取的步骤如下:

  1. 尝试从配置中找到一个TaskScheduler Bean
  2. 寻找ScheduledExecutorService Bean
  3. 使用默认的scheduler

前两步,如果找不到的话,会在DefaultListableBeanFactory.java的372行抛出异常
Spring Quartz 定时器任务

在ScheduledAnnotationBeanPostProcessor.java类中会捕获异常进行处理:
Spring Quartz 定时器任务
logger.debug(“Could not find default TaskScheduler bean”, ex);
logger.debug(“Could not find default ScheduledExecutorService bean”, ex);

所以,日志中打印出来的两个异常,不是错误信息,也不会影响定时器的使用,只是Spring的自己打印的一些信息。

4. Cron表达式介绍

Cron的表达式被用来配置CronTrigger实例。 Cron的表达式是字符串,实际上是由七子表达式,描述个别细节的时间表。这些子表达式是分开的空白,代表:

  1. Seconds
  2. Minutes
  3. Hours
  4. Day-of-Month
  5. Month
  6. Day-of-Week
  7. Year (可选字段)

例:”0 0 9 ? * FRI” 在每星期五上午9:00 执行

名称 是否必须 允许值 特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W C
1-12 或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L C #
空 或 1970-2099 , - * /

注意:

  • 月份和星期的名称是不区分大小写的,FRI 和 fri 是一样的
  • 域之间有空格分隔 * * * ? * *
  • 这个表达会每秒钟(每分种的、每小时的、每天的)激发一个部署的 job

特殊字符的解释

1. * 星号

使用星号(*) 指示着你想在这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发这个 trigger。

例:0 * 9 * * ?

意义:每天从上午9点到上午9:59中的每分钟激发一次 trigger。它停在上午 9:59 是因为值 9 在小时域上,在上午 10 点时,小时变为 10 了,也就不再理会这个 trigger,直到下一天的上午 9 点。

在你希望 trigger 在该域的所有有效值上被激发时使用 * 字符。

2. 问号

? 号只能用在日和周域上,但是不能在这两个域上同时使用。你可以认为 ? 字符是 “我并不关心在该域上是什么值。” 这不同于星号,星号是指示着该域上的每一个值。? 是说不为该域指定值。

不能同时这两个域上指定值的理由是难以解释甚至是难以理解的。基本上,假定同时指定值的话,意义就会变得含混不清了:考虑一下,如果一个表达式在日域上有值11,同时在周域上指定了 WED。那么是要 trigger 仅在每个月的11号,且正好又是星期三那天被激发?还是在每个星期三的11号被激发呢?要去除这种不明确性的办法就是不能同时在这两个域上指定值。

只要记住,假如你为这两域的其中一个指定了值,那就必须在另一个字值上放一个 ?。

表达式样例:

0 10,44 14 ? 3 WEB

意义:在三月中的每个星期三的14:10 和 14:44 被触发。

3. , 逗号

逗号 (,) 是用来在给某个域上指定一个值列表的。例如,使用值 0,15,30,45 在秒域上意味着每15秒触发一个 trigger。

表达式样例:

0 0,15,30,45 * * * ?

意义:每刻钟触发一次 trigger。

4. / 斜杠

斜杠 (/) 是用于时间表的递增的。我们刚刚用了逗号来表示每15分钟的递增,但是我们也能写成这样 0/15。

表达式样例:

0/15 0/30 * * * ?

意义:在整点和半点时每15秒触发 trigger。

5. - 中划线

中划线 (-) 用于指定一个范围。例如,在小时域上的 3-8 意味着 “3,4,5,6,7 和 8 点。” 域的值不允许回卷,所以像 50-10 这样的值是不允许的。

表达式样例:

0 45 3-8 ? * *

意义:在上午的3点至上午的8点的45分时触发 trigger。

6. L 字母

L 说明了某域上允许的最后一个值。它仅被日和周域支持。当用在日域上,表示的是在月域上指定的月份的最后一天。例如,当月域上指定了 JAN 时,在日域上的 L 会促使 trigger 在1月31号被触发。假如月域上是 SEP,那么 L 会预示着在9月30号触发。换句话说,就是不管指定了哪个月,都是在相应月份的时最后一天触发 trigger。

表达式 0 0 8 L * ? 意义是在每个月最后一天的上午 8:00 触发 trigger。在月域上的 * 说明是 “每个月”。

当 L 字母用于周域上,指示着周的最后一天,就是星期六 (或者数字7)。所以如果你需要在每个月的最后一个星期六下午的 11:59 触发 trigger,你可以用这样的表达式 0 59 23 ? * L。

当使用于周域上,你可以用一个数字与 L 连起来表示月份的最后一个星期 X。例如,表达式 0 0 12 ? * 2L 说的是在每个月的最后一个星期一触发 trigger。

不要让范围和列表值与 L 连用,虽然你能用星期数(1-7)与 L 连用,但是不允许你用一个范围值和列表值与 L 连用。这会产生不可预知的结果。

7. W 字母

W 字符代表着平日 (Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最近的一个平日。大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。例如,日域中的 15W 意味着 “离该月15号的最近一个平日。” 假如15号是星期六,那么 trigger 会在14号(星期四)触发,因为距15号最近的是星期一,这个例子中也会是17号(译者Unmi注:不会在17号触发的,如果是15W,可能会是在14号(15号是星期六)或者15号(15号是星期天)触发,也就是只能出现在邻近的一天,如果15号当天为平日直接就会当日执行)。W 只能用在指定的日域为单天,不能是范围或列表值。

8. # 井号

#字符仅能用于周域中。它用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。另一个例子 2#1 意思是某月的第一个星期一 (2=星期一,#1意味着月份中的第一周)。注意,假如你指定 #5,然而月份中没有第 5 周,那么该月不会触发。

各种示例:

表达式 意义
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/5 14 * *? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 ** ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0-5 14 * *? 在每天下午2点到下午2:05期间的每1分钟触发
0 10,44 14 ? 3WED 每年三月的星期三的下午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 ? * 6L2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? *6#3 每月的第三个星期五上午10:15触发

参考:
http://blog.csdn.net/evankaka/article/details/45365051
http://www.voidcn.com/blog/oarsman/article/p-6233372.html