Spring回顾之七 —— 和Quartz一起进行定时任务

    Quartz是一个由java编写的任务调度框架,是Spring默认的调度框架,很容易与 Spring 集成使用。作为一个优秀的开源框架,Quartz拥有强大的调度功能,支持丰富多样的调度方法,可以满足各种常规及特殊需求;也支持任务和调度的多种组合方式,支持调度数据的多种存储方式,使用过程中可以根据需求灵活应用;同时也支持分布式和集群能力,可以适应各种使用场景。

第一步:Quartz的基本原理介绍
    Quartz任务调度的核心是scheduler、trigger、job和misfire,顾名思义,scheduler是实际执行调度的控制器角色,trigger、job和misfire是任务调度相关的具体数据元素,其中misfire是本来应该被执行但实际没有被执行的任务调度。
    在Quartz中,scheduler是由DirectSchedulerFactory或者StdSchedulerFactory创建,一般StdSchedulerFactory使用较为普遍,Scheduler主要有三种:RemoteMBeanScheduler、RemoteScheduler和StdScheduler。trigger是用于定义调度时间的元素,即用来定义执行任务的时间规则。Quartz提供了四种类型的trigger:SimpleTrigger、CronTirgger、DateIntervalTrigger,和NthIncludedDayTrigger。job用于定义被调度的具体任务。Quartz主要有两种类型的job:无状态的(stateless)和有状态的(stateful)job。Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。
    关于调度元素的数据存储,Quartz支持两种存储方式:RAMJobStore和JobStoreSupport,从字面意思上可以看出来,RAMJobStore是将trigger和job存储在内存中,这个存取速度非常快;而JobStoreSupport则将trigger和job存储到数据库中,为了避免系统被停止后数据丢失,通常应用中可以选择JobStoreSupport;平时简单的使用是直接即在程序或配置文件中做相关的数据定义,在服务加载时直接使用。

第二步,Quartz的引入与简单使用
    打开pom文件,加入以下代码,我们将通过maven引入Quartz所需的依赖包
	    <dependency><!-- 即原spring-support.jar,包含支持UI模版,邮件服务,脚本服务(JRuby),缓存Cache(EHCache),任务计划Scheduling(Quartz)等方面的类  -->
	        <groupId>org.springframework</groupId>  
	        <artifactId>spring-context-support</artifactId>  
	        <version>${spring.version}</version>  
	    </dependency>
	    <dependency><!-- 为Spring 提供事务管理等  -->
	        <groupId>org.springframework</groupId>  
	        <artifactId>spring-tx</artifactId>  
	        <version>${spring.version}</version>  
	    </dependency> 
		<!-- ============== quartz begin ============== -->		
		<dependency>
		    <groupId>org.quartz-scheduler</groupId>
		    <artifactId>quartz</artifactId>
		    <version>2.2.3</version>
		</dependency>
		<dependency>  
		  <groupId>org.quartz-scheduler</groupId>  
		  <artifactId>quartz-jobs</artifactId>  
		  <version>2.2.3</version>  
		</dependency>
		<!-- ============== quartz end ============== -->

    这里Quartz方面提供的是quartz和quartz-jobs包,spring-context-support包是原来的spring-support包,包含支持UI模版,邮件服务,脚本服务(JRuby),缓存Cache(EHCache),任务计划Scheduling(Quartz)等方面的功能类,spring-tx是Spring提供做事物处理的,使用中都有所依赖。
    Quartz的简单使用有多种配置方式,我们就最基本的来依次试一下,先定义一个最基本的简单任务类,代码如下
package test.demo.job;

import org.apache.log4j.Logger;

public class SimpleJob {
	private Logger logger = Logger.getLogger(SimpleJob.class);  
    public void runTask(){  
    	logger.info("SimpleJob===========runTask()");
    }
}

    然后在Spring配置文件applicationContext.xml中进行设置
    <!-- 基本的配置方式 -->
    <!-- 配置任务bean类 -->  
    <bean id="simpleJob" class="test.demo.job.SimpleJob"></bean>  
    <!-- 配置方法映射工厂类 -->  
	<bean id="simpleJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	    <property name="targetObject" ref="simpleJob"/>  
	    <property name="targetMethod" value="runTask"/>  
	    <property name="concurrent" value="false"/>  
    	<!-- concurrent : false表示等上一个任务执行完后再开启新的任务 -->  
	</bean>
	<!-- 配置任务调度的时间/周期、延时 -->  
	<bean id="simpleJobDetailTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">  
	    <property name="jobDetail" ref="simpleJobDetail"/>
	    <property name="cronExpression" value="*/10 * * * * ?"/>  
	    <property name="startDelay" value="3000"/>
	</bean>

    simpleJob是配置相应的任务bean,simpleJobDetail是配置任务具体执行方法映射的工厂类,simpleJobDetailTrigger是用来设置任务调度的具体时间、周期、延迟等参数,然后一切妥当,我们还要配置一下任务的调度中心,进行整体的加载,代码如下
	<!-- 配置任务调度中心 -->
	<bean id="schedulerFactoryBean"  class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
	    <property name="triggers">  
	        <list>  
	           <ref bean="simpleJobDetailTrigger"/>
	        </list>
	    </property>  
	</bean>

    好了,这些都完成的时候,我们打包、启动程序,可以看到输出栏可以打出日志,如下图
   

    可以看到,当程序正常启动的时候,这个定义好的任务就开始按照cronExpression的值定义的、每10秒一次的频率进行执行,具体的使用方法稍后再说。

    我们也可以使用注解的方式来省去任务bean的配置,只需要在定义任务累的时候使用@Component标签即可,代码如下
package test.demo.job;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;

@Component("simpleAnnotationJob")  
public class SimpleAnnotationJob {
	private Logger logger = Logger.getLogger(SimpleAnnotationJob.class);  
    public void runTask(){  
    	logger.info("SimpleAnnotationJob===========runTask()");
    }
}

    然后在applicationContext.xml中进行设置
	<!-- 任务bean使用注解的配置方式 -->
	<context:component-scan base-package="test.demo.job"/>
    <!-- 配置方法映射工厂类 -->  
	<bean id="simpleAnnotationJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	    <property name="targetObject" ref="simpleAnnotationJob"/>  
	    <property name="targetMethod" value="runTask"/>  
	    <property name="concurrent" value="false"/>  
    	<!-- concurrent : false表示等上一个任务执行完后再开启新的任务 -->  
	</bean>
	<!-- 配置任务调度的时间/周期、延时 -->  
	<bean id="simpleAnnotationJobDetailTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">  
	    <property name="jobDetail" ref="simpleAnnotationJobDetail"/>
	    <property name="cronExpression" value="*/10 * * * * ?"/>  
	    <property name="startDelay" value="3000"/>
	</bean>
	<!-- 配置任务调度中心 -->
	<bean id="schedulerFactoryBean"  class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
	    <property name="triggers">  
	        <list>  
	           <ref bean="simpleJobDetailTrigger"/>
	           <ref bean="simpleAnnotationJobDetailTrigger"/>
	        </list>
	    </property>  
	</bean>

    这里context:component-scan会在启动时扫描@Component标签的内容,然后拿来使用即可,其余的基本都一样,运行如下
   

    可以看到效果基本一样。

    还有一种配置方式,是在定义任务的时候直接继承QuartzJobBean的形式,代码如下
package test.demo.job;

import org.apache.log4j.Logger;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class SimpleExtendsJob extends QuartzJobBean{
	private Logger logger = Logger.getLogger(SimpleExtendsJob.class); 
	@Override
	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
		logger.info("SimpleExtendsJob===========runTask()");
	}
}

    然后在applicationContext.xml中如下设置
	<!-- 使用继承QuartzJobBean的配置方式 -->
	<!-- 配置方法映射工厂类 -->  
	<bean id="simpleExtendsJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">  
	    <property name="jobClass" value="test.demo.job.SimpleExtendsJob"/> 
	    <property name="durability" value="true"/>  
	</bean>
	<!-- 配置任务调度的时间/周期、延时 -->  
	<bean id="simpleExtendsJobDetailTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">  
	    <property name="jobDetail" ref="simpleExtendsJobDetail"/>
	    <property name="cronExpression" value="*/10 * * * * ?"/>  
	    <property name="startDelay" value="3000"/>
	</bean>

	<!-- 配置任务调度中心 -->
	<bean id="schedulerFactoryBean"  class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
	    <property name="triggers">  
	        <list>  
	           <ref bean="simpleJobDetailTrigger"/>
	           <ref bean="simpleAnnotationJobDetailTrigger"/>
	           <ref bean="simpleExtendsJobDetailTrigger"/>
	        </list>
	    </property>  
	</bean>

    这里需要给jobClass赋值为任务类的相对路径即可,然后运行效果如下
   

    可以看出,效果一样。
    好了,Quartz的简单使用到这里就基本讲完了,在一般的使用场景中,这样的配置可以满足基本的需求。

第三步,Quartz任务调度的动态使用
    有时候有稍多的任务配置,将导致配置文件过于庞大繁冗,且修改起来必须重启才能生效,很不方便,可以在代码中考虑动态配置任务。
    先将原来applicationContext.xml中任务调度中心的triggers给注释掉,只留调度工厂本身,代码如下
	<!-- 配置任务调度中心 -->
	<bean id="schedulerFactoryBean"  class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
	<!-- 
	    <property name="triggers">  
	        <list>  
	           <ref bean="simpleJobDetailTrigger"/>
	           <ref bean="simpleAnnotationJobDetailTrigger"/>
	           <ref bean="simpleExtendsJobDetailTrigger"/>
	        </list>
	    </property>
	 -->  
	</bean>

    由于我们想要动态的控制任务数据,所以要创建一个用于存储任务数据的实体类
package test.demo.data;
/**
 * 任务实体类
 * @author Veiking
 */
public class ScheduleJob {
	private String jobId;// 任务id
	private String jobName;// 任务名称
	private String jobGroup;// 任务分组
	private String jobStatus;// 任务状态 0-禁用 ;1-启用; 2-删除
	private String cronExpression;// 任务运行时间表达式
	private String desc;// 任务描述

	public String getJobId() {
		return jobId;
	}

	public void setJobId(String jobId) {
		this.jobId = jobId;
	}

	public String getJobName() {
		return jobName;
	}

	public void setJobName(String jobName) {
		this.jobName = jobName;
	}

	public String getJobGroup() {
		return jobGroup;
	}

	public void setJobGroup(String jobGroup) {
		this.jobGroup = jobGroup;
	}

	public String getJobStatus() {
		return jobStatus;
	}

	public void setJobStatus(String jobStatus) {
		this.jobStatus = jobStatus;
	}

	public String getCronExpression() {
		return cronExpression;
	}

	public void setCronExpression(String cronExpression) {
		this.cronExpression = cronExpression;
	}

	public String getDesc() {
		return desc;
	}

	public void setDesc(String desc) {
		this.desc = desc;
	}

}

    为了体现对任务调度使用,我们需要重新定义一下任务工厂,实现Quartz提供的Job接口
package test.demo.job;

import org.apache.log4j.Logger;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import test.demo.data.ScheduleJob;

@DisallowConcurrentExecution //不允许并发执行
public class QuartzDynamicJobFactory implements Job{
	private Logger logger = Logger.getLogger(QuartzDynamicJobFactory.class);  
	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
        ScheduleJob scheduleJob = (ScheduleJob)context.getMergedJobDataMap().get("scheduleJob");
        logger.info("任务 : [" + scheduleJob.getJobName() + "] 成功运行!");
	}
}

    接下来要进行测试使用,我们定义一个专门的TestQuartzController,整体代码如下
package test.demo.controller;

import java.util.ArrayList;
import java.util.List;

import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import test.demo.data.ScheduleJob;
import test.demo.job.QuartzDynamicJobFactory;

/**
 * 用于Quartz测试
 * @author Veiking
 */
@Controller
public class TestQuartzController {
	private static final Logger logger = LoggerFactory.getLogger(TestQuartzController.class);
	private ApplicationContext ctx;

	@RequestMapping(value = "/quartz", method = RequestMethod.GET)
	public void quartz() throws SchedulerException {
		ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
		logger.info("ApplicationContext: " + ctx.toString());
		Scheduler scheduler = (Scheduler) ctx.getBean("schedulerFactoryBean");
		logger.info("Scheduler: " + scheduler);
		//初始化任务相关信息数据
		List<ScheduleJob> jobList = new ArrayList<ScheduleJob>();
		for (int i = 0; i < 5; i++) {
			ScheduleJob job = new ScheduleJob();
			job.setJobId("test" + i);
			job.setJobName("JobName_" + i);
			job.setJobGroup("test");
			job.setJobStatus("1");
			job.setCronExpression("0/5 * * * * ?");
			job.setDesc("test任务");
			jobList.add(job);
		}

		//初始执行任务
		for (ScheduleJob job : jobList) {
			TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
			//获取trigger,即在spring配置文件中定义的 bean id="myTrigger"
			CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
			//表达式调度构建器
			CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
			if (null == trigger) {
				//trigger不存在,创建一个
				JobDetail jobDetail = JobBuilder.newJob(QuartzDynamicJobFactory.class).withIdentity(job.getJobName(), job.getJobGroup()).build();
				jobDetail.getJobDataMap().put("scheduleJob", job);
				//按新的cronExpression表达式构建一个新的trigger
				trigger = TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup()).withSchedule(scheduleBuilder).build();
				scheduler.scheduleJob(jobDetail, trigger);
			} else {
				//Trigger已存在,那么更新相应的定时设置
				//按新的cronExpression表达式重新构建trigger
				trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
				//按新的trigger重新设置job执行
				scheduler.rescheduleJob(triggerKey, trigger);
			}
		}
	}
}


    注意我们这里是通过一个jobList进行任务相关信息数据的初始化,实际使用的时候这个数据可以从包括数据库在内的任何地方动态获得,可以随时编辑修改等
    然后当一切都安排妥当,打包运行,在浏览器请求 http://localhost:8080/demo/quartz ,输出栏会打出如下日志


    OK,一切运行良好,简单的动态实现任务调度已经OK。

第四步,Cron表达式详解
    在上边定义任务调度时间的时候,属性cronExpression值是一串表达式,这个表达式是用来设任务定执行的时间、间隔、频率等,所以CronTrigger的使用核心是Cron表达式的使用。
    Cron表达式的使用格式是:秒 分 时 日 月 周 年(可选),每个占位的使用规则如下:
位置		含义		允许的特殊字符
1		秒(0~59)		, -  *  /
2		分(0~59)		, -  *  /
3		小时(0~24)		, -  *  /
4		日期(1~31)		, -  *  /  ?  L  W  C
5		月(JAN~DEC或1~12)		, -  *  /
6		星期(SUN~SAT或1~7)		, -  *  /  ?  L  C  #
7		年(可选,1970~2099),若为空,表示全部时间范围		, -  *  /

    特殊字符的含义,见下表。
特殊字符	说明
*	通配符,任意值
?	无特定值。通常和其他指定的值一起使用,表示必须显示该值但不能检查
-	范围。e.g.小时部分10-12表示10:00,11:00, 12:00
,	列分隔符。可以让你指定一系列的值。e.g.在星期域中指定MON、TUE和WED
/	增量。表示一个值的增量,e.g.分钟域中0/1表示从0开始,每次增加1min
L	表示Last。它在日期和星期域中表示有所不同。在日期域中,表示这个月的最后一天,而在星期域中,它永远是7(星期六)。当你希望使用星期中某一天时,L字符非常有用。e.g.星期域中6L表示每一个月的最后一个星期五
W	在本月内离当天最近的工作日触发,所谓的最近工作日,即当天到工作日的前后最短距离,如果当天即为工作日,则距离是0;所谓本月内指的是不能跨月取到最近工作日,即使前/后月份的最后一天/第一天确实满足最近工作日。e.g. LW表示本月的最后一个工作日触发,W强烈依赖月份。
#	表示该月的第几个星期,e.g. 1#2表示每一个月的第一个星期一
C	日历值。日期值是根据一个给定的日历计算出来的。在日期域中给定一个20C将在20日(日历包括20日)或20日后日历中包含的第一天(不包括20日)激活触发器。例如在一个星期域中使用6C表示日历中星期五(日历包括星期五)或者第一天(日历不包括星期五)


    常见的Cron表达式示例:
"30 * * * * ?"	每半分钟触发任务
"30 10 * * * ?"	每小时的10分30秒触发任务
"30 10 1 * * ?"	每天1点10分30秒触发任务
"30 10 1 20 * ?"	每月20号1点10分30秒触发任务
"30 10 1 20 10 ? *"	每年10月20号1点10分30秒触发任务
"30 10 1 20 10 ? 2011"	2011年10月20号1点10分30秒触发任务
"30 10 1 ? 10 * 2011"	2011年10月每天1点10分30秒触发任务
"30 10 1 ? 10 SUN 2011"	2011年10月每周日1点10分30秒触发任务
"15,30,45 * * * * ?"	每15秒,30秒,45秒时触发任务
"15-45 * * * * ?"	15到45秒内,每秒都触发任务
"15/5 * * * * ?"	每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?"	每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?"	每小时的第0分0秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI"	星期一到星期五的10点15分0秒触发任务
"0 15 10 L * ?"	每个月最后一天的10点15分0秒触发任务
"0 15 10 LW * ?"	每个月最后一个工作日的10点15分0秒触发任务
"0 15 10 ? * 5L"	每个月最后一个星期四的10点15分0秒触发任务
"0 15 10 ? * 5#3"	每个月第三周的星期四的10点15分0秒触发任务






猜你喜欢

转载自veiking.iteye.com/blog/2371511
今日推荐