Spring Quartz 定时器任务
本文介绍了Spring Quartz使用定时器任务的2种方式
- 基于XML文件的方式
- 基于注解的方式
除此之外,还较少了Cron表达式的基本使用
Spring Quartz部分有如下一些核心概念:
Scheduler是一个计划调度器容器,容器里面可以很多的JobDetail和Trigger,当容器启动后,里面的每个JobDetail都会根据Trigger按部就班自动去执行。容器中有一个线程池,用来并行调度执行每个作业,这样可以提高容器效率
JobDetail是一个可执行的工作,它本身可能是有状态的
Trigger代表一个任务执行计划的配置,什么时候去启动一个Job。
当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,获取的步骤如下:
- 尝试从配置中找到一个TaskScheduler Bean
- 寻找ScheduledExecutorService Bean
- 使用默认的scheduler
前两步,如果找不到的话,会在DefaultListableBeanFactory.java的372行抛出异常
在ScheduledAnnotationBeanPostProcessor.java类中会捕获异常进行处理:
logger.debug(“Could not find default TaskScheduler bean”, ex);
logger.debug(“Could not find default ScheduledExecutorService bean”, ex);
所以,日志中打印出来的两个异常,不是错误信息,也不会影响定时器的使用,只是Spring的自己打印的一些信息。
4. Cron表达式介绍
Cron的表达式被用来配置CronTrigger实例。 Cron的表达式是字符串,实际上是由七子表达式,描述个别细节的时间表。这些子表达式是分开的空白,代表:
- Seconds
- Minutes
- Hours
- Day-of-Month
- Month
- Day-of-Week
- 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