在开发时我们会常常遇到定时任务可以由客户进行管理在什么时候去执行或者甚至不再执行该定时任务。而Spring中所提供的定时任务组件却只能够通过修改trigger的配置才能够控制定时的时间以及是否启用定时任务,为此我搜索了网上的一些解决方法,发现还是不能够很好的解决这个问题。所以干脆仔仔细细的研究了一把Quartz和Spring中相关的源码,我们发现当我们在Spring通过如下声明定时任务时:
<bean id="yourJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="yourJobBean"/>
<property name="targetMethod" value="yourJobMethod"/>
<property name="concurrent" value="false"/>
</bean>
<bean id="yourCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean" >
<property name="jobDetail" ref="yourobDetail"/>
<property name="cronExpression">
<value>0 0 2 * * ?</value>
</property>
</bean>
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref local="yourCronTrigger"/>
</list>
</property>
</bean>
所生成的Quartz的JobDetail并不是你定义的Bean,因为JobDetail并不能够直接存放一个实例,具体可以参考JobDetail的构造函数:
JobDetail(String name, String group, Class jobClass)
Create a JobDetail with the given name, group and class, and the default settings of all the other properties.
Spring将其转换为MethodInvokingJob或者StatefulMethodInvokingJob类型,其实这两个类都是从QuartzJobBean类继承而来,那么我们来看看QuartzJobBean类的代码:
public abstract class QuartzJobBean implements Job {
/**
* This implementation applies the passed-in job data map as bean property
* values, and delegates to <code>executeInternal</code> afterwards.
* @see #executeInternal
*/
public final void execute(JobExecutionContext context) throws JobExecutionException {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValues(context.getScheduler().getContext());
pvs.addPropertyValues(context.getMergedJobDataMap());
bw.setPropertyValues(pvs, true);
}
catch (SchedulerException ex) {
throw new JobExecutionException(ex);
}
executeInternal(context);
}
/**
* Execute the actual job. The job data map will already have been
* applied as bean property values by execute. The contract is
* exactly the same as for the standard Quartz execute method.
* @see #execute
*/
protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
}
看到这个你或许已经明白了Spring对Quartz的封装原理了。是的,Spring通过这种方式最后就可以在Job真正执行的时候可以反调用到我们所注入的类和方法,具体的代码在MethodInvokingJobDetailFactoryBean类中,如下:
public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException {
prepare();
// Use specific name if given, else fall back to bean name.
String name = (this.name != null ? this.name : this.beanName);
// Consider the concurrent flag to choose between stateful and stateless job.
Class jobClass = (this.concurrent ? (Class) MethodInvokingJob.class : StatefulMethodInvokingJob.class);
// Build JobDetail instance.
this.jobDetail = new JobDetail(name, this.group, jobClass);
this.jobDetail.getJobDataMap().put("methodInvoker", this);
this.jobDetail.setVolatility(true);
this.jobDetail.setDurability(true);
// Register job listener names.
if (this.jobListenerNames != null) {
for (int i = 0; i < this.jobListenerNames.length; i++) {
this.jobDetail.addJobListener(this.jobListenerNames[i]);
}
}
postProcessJobDetail(this.jobDetail);
}
其实主要是这句:
this.jobDetail.getJobDataMap().put("methodInvoker", this);
这样在调用的时候那就可以调用到我们所注入的类和方法了。
那好我们理解了Spring的原理后就可以动手修改成我们自己的了,所以在设计的时候我们考虑到以下两个方面:
- 精简Spring的配置,添加一个定时任务至少有10几行配置代码,太麻烦了,最好只声明一个Bean就可以了;
- 客户不但能够控制执行的时间,还可以能够启用/禁用某个定时任务。
在上面的思路下,我们决定保留Spring的这个配置:
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> </bean>
只不过不需要注入trigger这个属性了,我们仅仅利用Spring来帮我们构造一个Scheduler。
为了简化后续的处理,我们决定定义一个自己Job的接口,方便进行控制及精简代码。
public interface MyJob {
/**
* 执行具体的任务处理
* @throws JobException
*/
public void execute() throws JobException;
}
接下来我们还是需要一个类似QuartzJobBean类,因此参考Spring的更改如下:
public class QuartzJobBean implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
String targetBeanId = (String)context.getMergedJobDataMap().get("targetObjectId");
if(StringUtils.isNullString(targetBeanId))
return;
Object targetBean = SpringUtils.getBean(targetBeanId);
if(null == targetBean)
return;
// 判断是否是实现了MyJob接口
if(!(targetBean instanceof MyJob))
return;
// 执行相应的任务
((MyJob)targetBean).execute();
}
}
这个类处理逻辑就是通过获取我在创建JobDetail任务是设定的目标任务Bean的,即targetObjectId的值,然后通过SpringUtils获取该Bean,最后转换成MyJob接口执行。
如何创建JobDetail呢,因为我们前面已经说明不需要在Spring配置那么麻烦而且客户还可以进行配置,因此我们将任务的定时信息存放在数据库中,相应的Domain定义如下:
public class SchedulingJob extends Entry {
public static final int JS_ENABLED = 0; // 任务启用状态
public static final int JS_DISABLED = 1; // 任务禁用状态
public static final int JS_DELETE = 2; // 任务已删除状态
private String jobId; // 任务的Id,一般为所定义Bean的ID
private String jobName; // 任务的描述
private String jobGroup; // 任务所属组的名称
private int jobStatus; // 任务的状态,0:启用;1:禁用;2:已删除
private String cronExpression; // 定时任务运行时间表达式
private String memos; // 任务描述
// 省略getter和setter... ...
public String getTriggerName(){
return this.getJobId() + "Trigger";
}
}
这时候我们就可以对定时任务进行控制了,具体控制代码如下:
/**
* 启用指定的定时任务
* @param context
* @param schedulingJob
*/
protected void enabled(Context context, SchedulingJob schedulingJob) {
try {
CronTrigger trigger = (CronTrigger)this.scheduler.getTrigger(schedulingJob.getTriggerName(), schedulingJob.getJobGroup());
if (null == trigger) {
// Trigger不存在,那么创建一个
JobDetail jobDetail = new JobDetail(schedulingJob.getJobId(), schedulingJob.getJobGroup(), QuartzJobBean.class);
jobDetail.getJobDataMap().put("targetObjectId", schedulingJob.getJobId());
trigger = new CronTrigger(schedulingJob.getTriggerName(), schedulingJob.getJobGroup(), schedulingJob.getCronExpression());
this.scheduler.scheduleJob(jobDetail, trigger);
}else{
// Trigger已存在,那么更新相应的定时设置
trigger.setCronExpression(schedulingJob.getCronExpression());
this.scheduler.rescheduleJob(trigger.getName(), trigger.getGroup(), trigger);
}
} catch (SchedulerException e) {
e.printStackTrace();
// TODO
} catch (ParseException e) {
e.printStackTrace();
// TODO
}
}
/**
* 禁用指定的定时任务
* @param context
* @param schedulingJob
*/
protected void disabled(Context context, SchedulingJob schedulingJob) {
try {
Trigger trigger = this.scheduler.getTrigger(schedulingJob.getTriggerName(), schedulingJob.getJobGroup());
if (null != trigger) {
this.scheduler.deleteJob(schedulingJob.getJobId(), schedulingJob.getJobGroup());
}
} catch (SchedulerException e) {
e.printStackTrace();
// TODO
}
}
再加上前台处理页面(这个俺就不写了,估计大家都会) 这样我们就可以在控制定时任务的定时时间及启用和禁用了。
最后我们的配置代码如下:
<bean id="taskScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"/> <bean id="tempDirClearTask" class="com.my.module.attachment.util.TempDirClearTask"/>
P.S.
这个定时任务组件是有局限性的,比如所设置的SchedulingJob.jobID必须是和Spring中定时任务Bean的id一致,否则这个定时任务不会被运行。还有这个现在仅支持CronTrigger。而且也不支持分组,不过你可以通过扩展。
再一个要记得写一个加载类在系统初始化时从数据库的配置中加载所有启用的定时任务哦,不然所有定时任务都不能够运行