之前已经把功能基本都实现了,这里我们再来优化一下代码。
我们发现,在创建、修改、和删除定时任务时,对于quartz的操作其实是可以封装成一个简单的工具辅助类的,如创建的代码可以抽取成:
/** * 创建定时任务 * * @param scheduler the scheduler * @param jobName the job name * @param jobGroup the job group * @param cronExpression the cron expression * @param isSync the is sync * @param param the param */ public static void createScheduleJob(Scheduler scheduler, String jobName, String jobGroup, String cronExpression, boolean isSync, Object param) { //同步或异步 Class<? extends Job> jobClass = isSync ? JobSyncFactory.class : JobFactory.class; //构建job信息 JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).build(); //放入参数,运行时的方法可以获取 jobDetail.getJobDataMap().put(ScheduleJobVo.JOB_PARAM_KEY, param); //表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); //按新的cronExpression表达式构建一个新的trigger CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup) .withSchedule(scheduleBuilder).build(); try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { LOG.error("创建定时任务失败", e); throw new ScheduleException("创建定时任务失败"); } }
把任务的具体信息包括Scheduler都使用参数方式传入。
看过前面文章的同学或许还记得,quartz在spring中需要声明的对象只剩下一行:
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" />
既然这个schedulerFactoryBean只是在spring中声明一下,并没有做特殊的操作,在辅助的工具类中直接使用单例模式创建一个不是更好,还能少传一个参数?
你想的没错,这种方式的确更好还能解耦,但是我们来看一下SchedulerFactoryBean类的代码:
public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBean<Scheduler>, BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle { //...... }
它实现了spring的FactoryBean接口,也就是说它在创建时并不是简单的new而已,还夹杂了一些其它的复杂行为,所以我们也没必要特地的去怎么怎么样,还是在spring中声明一下吧。
另外,我们看它的getObject()方法:
public Scheduler getObject() { return this.scheduler; }
发现它实际返回的已经是Scheduler对象,既然如此在我们类中就不必注入schedulerFactoryBean再调用getScheduler()这么麻烦了,可以直接声明Scheduler对象:
@Service public class ScheduleJobServiceImpl implements ScheduleJobService { /** 调度Bean */ @Autowired private Scheduler scheduler; //...... }
当然你注入schedulerFactoryBean也不会有错,看过spring源码的同学应该立马就能明白这是getBean("bean")和getBean("&bean")的区别了。
另外说说前面漏掉的两个地方。
一、更新任务
先前我们在更新任务时,虽然更新了定时任务的执行时间,但是并没有对参数进行更新,即使用context.getMergedJobDataMap().get(...)方法获取到的参数还是旧的。
假设我们更新了任务的时间表达式,任务已按新的时间表达式在执行,但在获取到参数后发现时间表达式还是原来的。
尝试对参数进行更新,使用如下代码:
JobDetail jobDetail = scheduler.getJobDetail(getJobKey(jobName, jobGroup)); //jobDetail = jobDetail.getJobBuilder().ofType(jobClass).build(); //更新参数 实际测试中发现无法更新 JobDataMap jobDataMap = jobDetail.getJobDataMap(); jobDataMap.put(ScheduleJobVo.JOB_PARAM_KEY, param); jobDetail.getJobBuilder().usingJobData(jobDataMap);
发现无法更新,试过其它几个api发现都不行,没有办法,最后采用了先删除任务再进行创建的方式来迂回实现参数的更新。demo中更新任务有直接修改方式和删除修改方式,区别就在这里。
二、任务的同步和异步
同步和异步在quartz 2.2的版本中对于使用者来说区别只在于是否在job类上添加了@DisallowConcurrentExecution注解。
按时这个特点我们建立两个job的实现工厂类,在其中一个类上添加注解@DisallowConcurrentExecution,然后可以根据添加任务时的参数来确定具体使用哪个:
//同步或异步 Class<? extends Job> jobClass = isSync ? JobSyncFactory.class : JobFactory.class;
需要注意在定时任务运行时更新是没有办法改变同步和异步的。
接下来说说我在整合使用时碰到的一些已知问题。
一、更新任务时参数问题。也就是前面说的无法更新任务中传入的参数。
二、同步或异步在定时任务运行时修改是不能改变的,这个在前面也提到了。
三、在定时任务运行时修改,可能会该让任务长时间处于线程阻塞状态,即BLOCKED状态,即使你的任务中只有简单的一行System.out输出。要使它恢复也很简单,删除重建即可。
四、定时任务运行两次的问题。这个也是网上传的最多的问题,这里来着重的说一下。
网上流传引起该问题的原因目前主要有两个说法:
1 spring配置文件加载了多次,导致quartz的bean被实例化多次而导致任务多次执行。
2 tomcat的webapps目录问题。tomcat运行时加载了两次配置文件导致任务多次执行。
这两个说法在我的demo中应该并不存在,但为了验证我也尝试了下。
不使用tomcat,在main方法中用编程的方式启动spring,甚至不使用spring,直接用quartz官方给出的代码:
try { // Grab the Scheduler instance from the Factory Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // and start it off scheduler.start(); scheduler.shutdown(); } catch (SchedulerException se) { se.printStackTrace(); }
问题还是存在,这就说明不是配置文件加载的问题了,这应该是quartz本身存在的一个bug,而且这个多次运行是很有规律的,基本按如下套路走:
- 定为5秒运行一次,一切正常,没有多次执行现象发生。
- 定为10秒运行一次,一切正常,没有多次执行现象发生。
- 定为29秒运行一次,运行时一次正常,一次不正常。
- 定为59秒运行一次,运行时一次正常,一次不正常。
以上是我实测得出的,再长时间就没测了,毕竟太耗时。在有运行两次的现象时都是间隔的,即一次正常一次不正常这种方式。
既然推断是quartz本身存在bug,那我们又要如何解决这个问题了?
其实在我个人看来,这个问题是无关紧要的,为什么说无关紧要呢?这就涉及到你项目业务设计的是否完善,代码是否健壮了。
一个设计良好的业务方法,特别是那些供外部调用的接口或方法,应该都支持幂等性,何为幂等性?即这个方法同样的参数至少在一个时间区间内,我调用1次和调用10次100次,结果都是一样的。
支持了幂等性,前面说的运行两次的情况是不是就无关紧要了?在有些定时任务为分布式设计的系统(后面会探讨)中,为了确保定时任务的执行甚至会故意人为的去调用两次。
当然支持幂等性最好是在进入方法时就判断,发现已经执行过时就立即返回而不是真的再去同样的结果再执行一遍,以节省资源。