Quartz中提供了两种触发器,分别是CronTrigger和SimpleTrigger。
每 隔若干毫秒来触发纳入进度的任务。因此,对于夏令时来说,根本不需要做任何特殊的处理来“保持进度”。它只是简单地保持每隔若干毫秒来触发一次,无论你的 SimpleTrigger每隔10秒触发一次还是每隔15分钟触发一次,还是每隔24小时触发一次。
在特定“格林日历”时刻触发纳入进程的任务,因此,如果创建一个在每天上午10点触发的触发器,那么,在夏令时执行之前,系统将继续如此运作。但是,取决于是 春季还是秋季夏令时,因为对于特定的星期日,从星期六上午10点到星期日上午10点之间的时间间隔将不是24小时,而可能是23或者25个小时。
总之,如果你记住下面的两条规则,则会感觉良好并且很容易记忆:
• SimpleTrigger 总是每隔若干秒触发,而同夏令时没有关系。
• CronTrigger 总是在给定的时间出发然后计算它下次触发的时间。如果在给定的日期内没有该时间,则触发器将会被忽略,如果在给定的日期内该时间发生了两次,它只触发一次。因为是在第一次触发发生后计算当天下次触发的时间。
可以在http://sourceforge.net/projects/quartz/链接中下载Quartz包,也可以通过Maven来进行构建,如果读者觉得麻烦,可以采用下载jar包的方法,因为Quartz依赖于其它包。我这里通过Maven来进行构建:
首先构建公共依赖项,用于公共模块使用:
<properties> <!--Spring版本号--> <spring.version>3.2.4.RELEASE</spring.version> <!--log4j日志文件管理包版本--> <slf4j.version>1.6.6</slf4j.version> <log4j.version>1.2.12</log4j.version> </properties> <dependencies> <!--Junit测试包--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.2</version> </dependency> <!--日志文件管理包--> <!--log start--> <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> <!--log end--> <!--Apache 组件--> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>
下面添加Quartz依赖项:
<dependencies> <!--添加Quartz框架--> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.1.1</version> </dependency> <!--组件可选项--> <!--start--> <dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>commons-digester</groupId> <artifactId>commons-digester</artifactId> <version>1.8</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency> <!--end--> </dependencies>
在公共模块记得要将模块打包方式设置为pom。
我这里的Quartz为较新的2.1.x版本,因为在与Spring整合时,Spring3.1.0之前的版本只支持Quartz1.6以往的版本。
至于log4j如何配置,这里就不在赘言了,读者可以参考网上自行搜索。
每一个 Quartz Job 必须有一个实现了org.quartz.Job接口的具体类。这个接口仅有一个要你在 Job 中实现的方法,execute(),方法execute()的原型如下:
public void execute(JobExecutionContext context) throws JobExecutionException;
当 Quartz 调度器确定到时间要激发一个 Job 的时候,它就会生成一个 Job 实例,并调用这个实例的execute()方法。调度器只管调用execute()方法,而不关心执行的结果,除了在作业执行中出问题抛出的 org.quartz.JobExecutionException异常。
下面是我们的第一个 Quartz job,它被设计来扫描一个目录中的文并显示文件的详细信息。
import ***; /** * 定义一个Job扫描一个目录中的文件,并显示文件的详细信息 * * @author Barudisshu */ public class ScanDirectoryJob implements Job { private static Logger logger = Logger.getLogger(ScanDirectoryJob.class); /** * Job接口中的execute方法体内为一个逻辑事务,所有工作事务在这里完成 * * @param context 执行上下文环境, * 包含jobDetail、trigger、jobDataMap、calendar等一系列组合子项 * @throws JobExecutionException */ @Override public void execute(JobExecutionContext context) throws JobExecutionException { // Every job has its own job detail JobDetail jobDetail = context.getJobDetail(); // The name is defined in the job definition String jobName = ((JobDetailImpl)jobDetail).getName(); //任务名称 // Log the time the job started logger.info(jobName + " fired at " + new Date()); //记录任务开始的时间 // The directory to scan is stored in the job map JobDataMap dataMap = jobDetail.getJobDataMap(); //任务所配置的数据映射表 String dirName = dataMap.getString("SCAN_DIR"); //获取要扫描的目录 // Validate the required input if (dirName == null) {//所需要的扫描目录没有提供 throw new JobExecutionException("Directory not configured"); } // Make sure the directory exists File dir = new File(dirName); if (!dir.exists()) {//提供的是错误目录 throw new JobExecutionException("Invalid Dir " + dirName); } // Use FileFilter to get only XML files FileFilter filter = new FileExtensionFileFilter(".xml"); // 只统计XML文件 File[] files = dir.listFiles(filter); if (files == null || files.length <= 0) {//目录下没有XML文件 logger.info("No XML files found in " + dir); // Return since there were no files return; } // The number of XML files int size = files.length; logger.info("The number of XML files: " + size); // Iterate through the files found for (File file : files) { // Log something interesting about each file File aFile = file.getAbsoluteFile(); long fileSize = file.length(); String msg = aFile + " - Size: " + fileSize; logger.info(msg); //记录下文件的路径和大小 } } }
当 Quartz 调用 execute() 方法,会传递一个 org.quartz.JobExecutionContext 上下文变量,里面封装有 Quartz 的运行时环境和当前正执行的 Job。通过 JobexecutionContext,你可以访问到调度器的信息,作业和作业上的触发器的信息,还有更多更多的信息。在代码 中,JobExecutionContext 被用来访问 org.quartz.JobDetail 类,JobDetail 类持有 Job 的详细信息,包括为 Job 实例指定的名称,Job 所属组,Job 是否被持久化(易失性),和许多其他感兴趣的属性。
JobDetail 又持有一个指向 org.quartz.JobDataMap 的引用。JobDataMap 中有为指定 Job 配置的自定义属性。例如,在代码中我们从 JobDataMap 中获得欲扫描的目录名,我们可以在 ScanDirectoryJob 中硬编码这个目录名,但是这样的话我们难以重用这个 Job 来扫描别的目录了。在后面你将会看到目录是如何配置到 JobDataMap 的。
execute() 方法中剩下的就是标准 Java 代码了:获得目录名并创建一个 java.io.File 对象。它还对目录名作为简单的校验,确保是一个有效且存在的目录。接着调用 File 对象的 listFiles() 方法得到目录下的文件。还创建了一个 java.io.FileFilter 对象作为参数传递给 listFiles() 方法。org.quartzbook.cavaness.FileExtensionFileFilter 实现了 java.io.FileFilter 接口,它的作用是过滤掉目录仅返回 XML 文件。默认情况下,listFiles() 方法是返回目录中所有内容,不管是文件还是子目录,所以我们必须过滤一下,因为我们只对 XML 文件感兴趣。
FileExtensionFileFilter 被用来屏蔽名称中不含字符串 “.xml” 的文件。它还屏蔽了子目录--这些子目录原本会让 listFiles() 方法正常返回。过滤器提供了一种很便利的方式选择性的向你的 Quartz 作业提供它能接受的作为输入的文件。
import ***; /** * @author Barudisshu */ public class FileExtensionFileFilter implements FileFilter { private String extension; //文件后缀 public FileExtensionFileFilter(String extension) { this.extension = extension; } @Override public boolean accept(File pathname) {//只接受指定后缀的文件 //如果file是个目录 if(pathname.isDirectory()) return false; // Lowercase the filename for easier comparison String LCaseFilename = pathname.getName().toLowerCase(); //文件名转换为小写 return (pathname.isFile() && (LCaseFilename.indexOf(extension) > 0)); } }
到 目前为止,我们已经创建了一个 Quartz job,但还没有决定怎么处置它--明显地,我们需以某种方式为这个 Job 设置一个运行时间表。时间表可以是一次性的事件,或者我们可能会安装它在除周日之外的每个午夜执行。你即刻将会看到,Quartz Schduler 是框架的心脏与灵魂。所有的 Job 都通过 Schduler 注册;必要时,Scheduler 也会创建 Job 类的实例,并执行实例的 execute() 方法。
Quartz2.x以上版本和以往版本有许多的不同,主要是JobDetail和Trigger不在通过实例构建,而是由构造器进行构建,通过减少实例的构建来优化内存处理,这样对效率有很大的提高。
Quartz 提供了四种类型的 Trigger,但其中两种是最为常用的,它们就是上面提到的 SimpleTrigger 和 CronTrigger。CronTrigger用法比SimpleTrigger稍微复杂一点,但是用法都一样,因此这里不做介绍,可以参考 CronTrigger表达式相关内容。
import ***; import static org.quartz.JobBuilder.newJob; import static org.quartz.SimpleScheduleBuilder.simpleSchedule; import static org.quartz.TriggerBuilder.newTrigger; /** * @author Barudisshu */ public class SimpleSchedulerTests { private static Logger logger = Logger.getLogger(SimpleSchedulerTests.class); private Scheduler createScheduler() throws SchedulerException {//创建调度器 SchedulerFactory schedulerFactory = new StdSchedulerFactory(); return schedulerFactory.getScheduler(); } private void scheduleJob(Scheduler scheduler) throws SchedulerException { // Create a job detail for the job JobDetail jobDetail = newJob(ScanDirectoryJob.class) .withIdentity("ScanDirectory", "jobDetail-group") .withDescription("ScanDirectory from tomcat conf") .build(); // Configure the directory to scan jobDetail.getJobDataMap() .put("SCAN_DIR", "\apache-tomcat-7.0.39\conf"); // Create a trigger that fires every 10 seconds,forever Trigger trigger = newTrigger() .withIdentity("scanTrigger", "trigger-group") .startNow() .withSchedule(simpleSchedule() .withIntervalInSeconds(10)) // 每十秒触发一次 .build(); //Associate the trigger with the job in the schedule scheduler.scheduleJob(jobDetail, trigger); } @Test public void ScanDirectoryTests() { SimpleSchedulerTests simpleSchedulerTests = new SimpleSchedulerTests(); try { Scheduler scheduler = simpleSchedulerTests.createScheduler(); // Start the scheduler running scheduler.start(); logger.info("Scheduler started at " + new Date()); simpleSchedulerTests.scheduleJob(scheduler); // Stop the scheduler after 10 second Thread.sleep(10000); scheduler.shutdown(); } catch (SchedulerException e) { logger.error(e); } catch (InterruptedException e) { e.printStackTrace(); } } }
多个Job调度
Quartz允许多个Job调度,Job作业是建立在Schedule上的,这和Quartz1.6.x之前的版本的概念稍微有点不同,不过不用在意这些概念上细节。下面通过单元测试来说明如何进行多个Job调度。
import ***; import static org.quartz.JobBuilder.newJob; import static org.quartz.SimpleScheduleBuilder.simpleSchedule; import static org.quartz.TriggerBuilder.newTrigger; /** * @author Barudisshu */ public class MultiJobSchedulerTests { private static Logger logger = Logger.getLogger(MultiJobSchedulerTests.class); private Scheduler createScheduler() throws SchedulerException {//创建调度器 return new StdSchedulerFactory().getScheduler(); } private void scheduleJob(Scheduler scheduler, String jobName, Class<? extends Job> jobClass, String scanDir, String triggerName, int scanInterval) throws SchedulerException { // Create a job detail for the job JobDetail jobDetail = newJob(jobClass) .withIdentity(jobName, "jobDetail-group") .build(); // Configure the directory to scan jobDetail.getJobDataMap() .put("SCAN_DIR", scanDir); // Create a trigger that fires every 10 seconds,forever Trigger trigger = newTrigger() .withIdentity(triggerName, "trigger-group") .startNow() .withSchedule(simpleSchedule() .withIntervalInSeconds(scanInterval)) // 每十秒触发一次 .build(); //Associate the trigger with the job in the schedule scheduler.scheduleJob(jobDetail, trigger); } @Test public void multiScanTests() { MultiJobSchedulerTests multiJobSchedulerTests = new MultiJobSchedulerTests(); try { Scheduler scheduler = multiJobSchedulerTests.createScheduler(); // Scheduler the first job multiJobSchedulerTests.scheduleJob(scheduler, "ScanTomcat", ScanDirectoryJob.class, "\apache-tomcat-7.0.39\conf", "tomcatTrigger", 10); // Scheduler the second job multiJobSchedulerTests.scheduleJob(scheduler, "ScanGlassfish", ScanDirectoryJob.class, "\glassfish4\glassfish\domains\domain1\config", "glassfishTrigger", 15); // Start the scheduler running scheduler.start(); logger.info("Scheduler started at " + new Date()); // Stop the scheduler after 10 second Thread.sleep(10000); scheduler.shutdown(); } catch (SchedulerException e) { logger.error(e); } catch (InterruptedException e) { e.printStackTrace(); } } }
加载XML文件实现Job调度
文 件 quartz.properties 定义了 Quartz 应用运行时行为,还包含了许多能控制 Quartz 运转的属性。这个文件应该放在classpath所指的路径下,比如我们这个java工程,就将它和下面将介绍的jobs.xml一起放在项目根目录下就 是。如果不清楚就查看.classpath文件,它里面就配置了你的项目的classpath。
可 以通过自定quartz.properties来进行Job调度,需要注意的是,在实际项目中不要以quartz.properties同名进行命名,因 为StdSchedulerFactory默认加载quartz.properties文件,因此我这里命名为scan- quartz.properties,配置如下:
# 固定前缀org.quartz # 主要分为scheduler、threadPool、jobStore、plugin等部分 # # org.quartz.scheduler.instanceName = DefaultQuartzScheduler org.quartz.scheduler.rmi.export = false org.quartz.scheduler.rmi.proxy = false org.quartz.scheduler.wrapJobExecutionInUserTransaction = false # 实例化ThreadPool时,使用的线程类为SimpleThreadPool org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool # threadCount和threadPriority将以setter的形式注入ThreadPool实例 # 并发个数 org.quartz.threadPool.threadCount = 5 # 优先级 org.quartz.threadPool.threadPriority = 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true org.quartz.jobStore.misfireThreshold = 60000 # 默认存储在内存中 org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore # 通过插件获取声明Job org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin org.quartz.plugin.jobInitializer.fileNames = jobs.xml org.quartz.plugin.jobInitializer.failOnFileNotFound = true org.quartz.plugin.jobInitializer.scanInterval = 10 org.quartz.plugin.jobInitializer.wrapInUserTransaction = false
插件配置
在这个简单的 quartz.properties 文件中最后一部分是你要用到的 Quart 插件的配置。插件常常在别的开源框架上使用到,比如 Apache 的 Struts 框架(见 http://struts.apache.org/)。
一个声明式扩框架的方法就是通过新加实现了 org.quartz.spi.SchedulerPlugin 接口的类。SchedulerPlugin 接口中有给调度器调用的三个方法。
要在我们的例子中声明式配置调度器信息,我们会用到一个 Quartz 自带的叫做 org.quartz.plugins.xml.JobInitializationPlugin 的插件。
默认时,这个插件会在 classpath 中搜索名为 quartz_jobs.xml 的文件并从中加载 Job 和 Trigger 信息。
下面就是目录扫描例子的 Job 定义的 XML 文件。正如上一篇所示例子那样,这里我们用的是声明式途径来配置 Job 和 Trigger 信息的:
<?xml version='1.0' encoding='utf-8'?> <job-scheduling-data xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.quartz-scheduler.org/xml/JobSchedulingData http://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd" version="1.8"> <schedule> <job> <name>ScanDirectory</name> <group>jobDetail-group</group> <description>ScanDirectory from tomcat conf</description> <job-class>net.individuals.quartz.ScanDirectoryJob</job-class> <job-data-map> <entry> <key>SCAN_DIR</key> <value>\apache-tomcat-7.0.39\conf</value> </entry> </job-data-map> </job> <trigger> <simple> <name>scanTrigger</name> <group>trigger-group</group> <!--jobName和jobGroup必须与对应的Job匹配--> <job-name>ScanDirectory</job-name> <job-group>jobDetail-group</job-group> <!--注意日期和时间--> <start-time>2008-09-03T14:43:00</start-time> <!-- repeat indefinitely every 10 seconds --> <repeat-count>-1</repeat-count> <repeat-interval>10000</repeat-interval> </simple> </trigger> </schedule> </job-scheduling-data>
在jobs.xml 中 的格式是:
<start-time>2008-09-03T14:43:00</start-time>
其中T隔开日期和时间,默认时区
或者:
<start-time>2008-09-03T14:43:00+08:00</start-time>
其中+08:00 表示东八区
元素描述了一个要注册到调度器上的 Job,相当于我们在前面章节中使用 scheduleJob() 方法那样。你所看到的 和 这两个元素就是我们在代码中以编程式传递给方法 schedulerJob() 的参数。
这里的XML配置和Quartz1.6.x也有许多不同,但实质都是一样,不用在意这些细节。
下面进行上述描述文件的加载和测试:
import ***; /** * @author Barudisshu */ public class LoadXmlTests { private static Logger logger = Logger.getLogger(LoadXmlTests.class); private Scheduler createScheduler() throws SchedulerException {//创建调度器 return new StdSchedulerFactory("scan-quartz.properties").getScheduler(); } @Test public void ScanDirectoryTests() { LoadXmlTests loadXmlTests = new LoadXmlTests(); try { Scheduler scheduler = loadXmlTests.createScheduler(); // Start the scheduler running scheduler.start(); logger.info("Scheduler started at " + new Date()); // Stop the scheduler after 10 second Thread.sleep(20000); scheduler.shutdown(); } catch (SchedulerException e) { logger.error(e); } catch (InterruptedException e) { e.printStackTrace(); } } }
怎样?是不是觉得简单了很多(我就不明白为什么有些人反对XML配置,XML配置不是挺好的吗?一目了然)
Quartz的Web构建依赖项需要将打包方式改为war即可,下面以一个简单的HelloWorld为例进行说明:
import ***; /** * @author Barudisshu */ public class HelloWorld implements Job { private static Logger logger = Logger.getLogger(HelloWorld.class); @Override public void execute(JobExecutionContext context) throws JobExecutionException { logger.info("\n---------------------------------" + "\n\nHello World! \n\n" + "---------------------------------\n" + new Date()); } }
WEB构建基本上依赖配置文件,我们只需要的web工程的web.xml中配置相应项即可:
<!--quartz配置1:--> <context-param> <param-name>quartz:config-file</param-name> <param-value>web-quartz.properties</param-value> </context-param> <context-param> <param-name>quartz:shutdown-on-unload</param-name> <param-value>true</param-value> </context-param> <context-param> <param-name>quartz:wait-on-shutdown</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>quartz:start-scheduler-on-load</param-name> <param-value>true</param-value> </context-param> <listener> <listener-class> org.quartz.ee.servlet.QuartzInitializerListener </listener-class> </listener> <!--quartz配置2:--> <servlet> <servlet-name>QuartzInitializer</servlet-name> <servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class> <init-param> <param-name>shutdown-on-unload</param-name> <param-value>true</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>QuartzInitializer</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
这样,在启动Tomcat的时候,QuartzInitializerServlet这个Servlet就会自动读取quartz.properties这个配置文件,并初始化调度信息,启动Scheduler。
Spring 的scheduling.quartz包中对Quartz框架进行了封装,使得开发时不用写任何QuartSpring的代码就可以实现定时任务。 Spring通过JobDetailBean,MethodInvokingJobDetailFactoryBean实现Job的定义。后者更加实用, 只需指定要运行的类,和该类中要运行的方法即可,Spring将自动生成符合Quartz要求的JobDetail。
<!--添加Spring框架--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</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-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-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.1.1</version> </dependency>
这里的任务类不需要实现Job接口,并且注意任务的方法不能带参数,否则Spring会报错找不到该方法:
import ***; /** * @author Barudisshu */ public class HelloWorld{ private static Logger logger = Logger.getLogger(HelloWorld.class); public HelloWorld() { } /** * spring 检测要求不带参数 */ public void execute() { logger.info("-----------------------------------------" + "\n\nKick your ass and fuck your mother! \n\n" + "-----------------------------------------" + new Date()); } }
Spring的applicationContext.xml配置文件修改如下:
<!--要调用的工作类--> <bean id="quartzJob" class="net.individuals.quartz.HelloWorld"/> <!--定义调用对象和调用对象的方法--> <bean id="jobtask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <!--调用类--> <property name="targetObject" ref="quartzJob"/> <!--调用方法--> <property name="targetMethod" value="execute"/> </bean> <!--定义触发时间--> <bean id="doTime" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="jobDetail" ref="jobtask"/> <!--cron表达式--> <property name="cronExpression" value="0/5 * * * * ?"/> </bean> <!--总管理类--> <bean id="startQuartz" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <!--任务列表--> <list> <ref bean="doTime"/> </list> </property> </bean>
这里Bean的注入方式因个人喜好,也可以自动注入,另外web.xml的配置这里也不在赘言,请自行Spring文档。
注意,如果运行期间出错,可能是Spring的版本或者某些包冲突,我这里的Spring版本为3.2.4、Quartz为2.1.1 。