【IT168 技术】作业调度,在项目的开发中,是一个使用非常普遍的功能,用来执行一个周期性的任务。Quartz是一个开源的用于实现作业计划的框架。在和spring集成后,Quartz使用起来变得非常的简单,因此Quartz也被广泛的使用。一般的作业调度执行的周期都是事先定义好的,能满足大部分的需求。但是也有不能满足的场景,如:1、在系统刚刚上线时,时间周期是一个大概数值。更加符合真实情况的执行周期需要通过不断的调试才可以得出。但是每次调整执行周期,都要重新启动项目,这显然是不能接受的。2、当遇到某些特殊情形,需要在某个时期停止计划的执行。当遇到以上2种场景时,固定的执行周期是不能满足我们的需求。这就要求我们实现可以动态调整执行周期的作业调度。此篇文章就是自己在项目中实现动态的作业计划的一些技术分享。
一、背景介绍
1、Spring简介
Spring是一个开源框架,用来解决企业应用开发的复杂性。Spring使用基本的JavaBean来完成以前只可能由企业级JavaBean(EJB)完成的事情。对于开发者来说,简单性和程序的低耦合性是共同追求的开发方式。而spring使Java的开发变得简单,并且Spring的使用降低了程序间的耦合性,使程序更加容易维护。
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
1、轻量级。Spring的大小是轻量级的,整个spring的核心jar包加在一起也就1MB左右。Spring的性能开销也是轻量级的,Spring的对象模式默认使用的是单例模式,可以有效的减小系统的内存开销。
2、控制反转。Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建。当容器在对象初始化时容器会将其所依赖的其他对象主动的传递给所需的对象。这个方式也可以叫做依赖注入。调用对象本身不用去关心被调用对象的初始化,使开发变得相对简单。
3、面向切面。Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发。应用对象只实现它们所应该实现的业务逻辑。其他附件的功能,如日志的记录,可以在切面中进行实现。
Spring提供了使用xml配置文件和注解两种方式,对Spring容器内的类进行引用关系等的配置,在使用时可以根据各自的特点选择不同的方式。
2、Quartz简介
Quartz是一个实现作业调度的框架。Quartz的核心主要由Scheduler、Job、JobDetail和Trigger几部分组成。
1、Scheduler是一个接口,它提供了对作业计划的启动、停止、恢复、删除、和对作业计划的重新制定的方法。它通过JobDetail和trigger创建一个作业计划。
2、Job是一个接口,只有一个需要实现的方法void execute(JobExecutionContext context)。程序中需要被执行的作业就需要在exeute中实现。
3、JobDetail是一个类。通过JobDetail可以设置具体执行的Job,并且给执行的Job设置名称、组、描述等信息。该类包括一个JobDetail(java.lang.String name, java.lang.String group, java.lang.Class jobClass)构造器,它的参数分别是Job名称、组名和实现了Job接口的实现类。
4、Trigger是一个类。主要用于设置触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。SimpleTrigger擅长执行单次执行或者固定周期计划任务执行。而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案。可以说Quartz支持cron表达式,是Quartz被广泛使用的一个重要的原因。
3、Cron表达式简介
Cron表达式由6或7个由空格分隔的时间字段组成,如“0 0 4 * * ?”。具体的含义如下表:
Cron表达式还包括一些特殊字符。
1、“*”:表示匹配该域的任意值,如0 * * * * ?,表示每分钟执行。
2、“?” : 只能用在“天(月)” 和 “天(星期)” 两个域。它也匹配域的任意值。由于此两个域是由冲突的只能有一个生效,所以如果不使用哪个域的话,就可以在此域使用"?"。
3、“-”:表示范围,例如在“分钟”域使用5-20,表示从5分到20分钟每分钟触发一次。
4、“/”:表示起始时间开始触发,然后每隔固定时间触发一次,例如在“分钟”域使用0/20,则意味着从整分开始,每隔20分钟执行一次。
5、“,”:表示列出枚举值值。例如:在“分钟”域使用5,20,则意味着在5和20分每分钟触发一次。
6、“L”:表示最后,只能出现在“天(月)” 和 “天(星期)” 两个域,如果在“天(星期)”域使用5L,意味着在最后的一个星期四触发。
7、“W”: 表示有效工作日(周一到周五),只能出现在“天(月)”域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 “天(月)”使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日触发;如果5日在星期一到星期五 中的一天,则就在5日触发。
8、“#”:只能用于“天(星期)”。确定每个月第几个星期几。例如在4#2,表示某月的第二个星期三。
二、 项目中作业调度的使用
1、为什么使用Quartz
在Java中有多种作业调度的框架,如java.util.TimerTask。但是在项目中大多不采用TimerTask,而是使用quartz。为什么不使用java.util.Timer结合java.util.TimerTask?主要有以下几个原因:
1、主要的原因是使用不方便,特别是制定具体的年月日时分的时间,而quartz使用cron表达式时,很方便的配置每隔时间执行触发。
2、其次性能的原因,使用jdk自带的Timer不具备多线程,而quartz采用线程池,性能上比timer高出很多。
3、quartz具有TimerTask的所有的功能,而Timer则不是。
2、标注方式的Quartz使用
我们的项目采用的quartz版本为1.8.4,可以和 spring3.x版本进行很好的融合。在开发中,使用了简单直观的标注方式实现周期性的作业调度。只需要通过以下几步就可以实现在spring中使用Quartz。
1、在spring的xml配置文件中加入< task:annotation-driven /> ,用来开启任务调度功能。
2、实现一个JAVA类,在类的名称前加入@Component,将类可以被spring容器创建。
3、在类中添加一个方法,并且在方法名前加入类似于@Scheduled(cron = "0 0 4 * * ?")的标注。此处使用的是cron表达式的方式执行作业调度。
从上面的步骤中可以看到,在spring中使用quartz是多么的简单。虽然使用在spring中使用quartz来进行作业调度是那么的简单,但是在对于需要实现动态的作业调度,还需要自己通过编码实现。
三、 作业调度实现
1、实现思路及流程
在项目中使用注解的方式,可以快速的实现一个作业调度程序。但是它也有其缺点,就是执行的周期都是在代码中固定的,如果要修改执行周期,那么就需要修改Java源代码,并且重新编译和部署。对于一个上线的系统需要尽量的减少项目的重新部署。
既然采用注解的方式实现动态的任务调度,那么就只有使用非注解的方式。如果要实现动态的作业调度的话,很容易想到,我们可以将要动态改变的内容作为参数来传入到作业调度方法中。实现时可以有以下几个步骤:
1、简单的数据库设计。有了数据库表结构后,当手工修改表中的关于执行周期的字段后,程序就可以读取到最新的执行周期,用于重新加载。
表可以有如下信息:ID、jobName、clazz、scheduleType、startDate、intervalInMillis、cront。
2、实现Quartz框架中的Job接口。在Quartz中被调度的类,都是要实现org.quartz.Job接口的。
在实现程序后我们在调度方法中,我们在其中加入打印Job实现类的名称,作为测试使用。
3、实现一个支持了增加调度任务和修改调度任务的方法。此处采用的是CronTrigger,支持cron表达式。
此处新增和更新采用了一个方法。当任务没有被启动时会创建任务,当任务已经存在时会重新制定计划。
4、编写定时任务,读取数据库。
当程序都准备好后,那么就必须要及时的知道数据库中的记录是否发生变化。我们可以使用现有的标注方式,实现一个定期读取数据库记录的计划调度。如果发现记录中执行周期有变化,那么通过调用addOrUpdateCrontTask方法重新进行计划调度,使新的计划调度生效。
2、测试结果及问题
在完成了以上4个步骤后,需要对实现的程序进行测试。
测试结果程序运行正常。当手工修改数据库表的周期信息后,程序会自动发现数据库的记录中的信息发生了改变,从而调用addOrUpdateCrontTask,使计划周期进行了更改。这个测试结果说明作业调度可以在修改执行周期后,可以被重新的制定。
不过以上测试,是最简单的使用场景。我们知道在真实地使用中,计划调度的方法中肯定会调用现有的已经被实现的Service类的。因此我们对程序进行一下改造。在JobClass中注入一个JobService,然后调用JobService中的方法,看是否成功。
1、首先创建JobService类,并添加标注,以便可以被其他类进行依赖注入。并且在方法中添加sayHelloWorld方法。
2、修改JobClass类,添加JobService类,并且使用@Autowired标签根据类型注入JobService。并且在执行任务时调用JobService的sayHelloWorld方法。
程序准备好后,再次运行程序,程序发生了错误,空指针异常,“hello world”并没有被打印出来。定位后,这个问题出现在jobService上,jobService为NULL。这说明jobService没有Spring容器自动的传递进来(也就是依赖注入没有生效)。这样就比较麻烦了,虽然可以在excute方法中不使用注入的方式同样可以实现业务逻辑,但是这样就造成之前实现的Service方法不能够被复用,降低了工作效率。所以将类注入到Job的实现类中是非常必要的。
那么该如何解决呢?
四、问题的解决
现在的问题是,想办法将不能被注入的类,注入到Job的接口实现类中。查找资料后,了解到要想使不受Spring管理的类,进行依赖注入,可以通过AutowireCapableBeanFactory类实现。AutowireCapableBeanFactory提供了一个autowireBean(Object existBean)方法,可以给传入的参数existBean对象进行依赖注入。另一方面Spring是通过SpringBeanJobFactory类的createJobInstance方法创建Job实例。那么很容易想到我们可以实现一个SpringBeanJobFactory类的子类,并且重写createJobInstance方法,给创建的Job实例手工进行依赖注入即可解决无法进行依赖注入的问题。下面按照以上构想再对程序做出修改。
1、首先自己定义一个类JobSpringFactory继承SpringBeanJobFactory。
2、将AutowireCapableBeanFactory类注入到SpringBeanJobFactory中的beanFactory对象中,以方便使用其注入功能。
3、重写createJobInstance方法。使用AutowireCapableBeanFactory类的实例beanFactory对创建的实例jobInstance进行手工注入。
4、事情还没有完,要想让程序启用,还必须要告诉spring,使用JobSpringFactory来创建Job的实例。所以我们需要在spring的配置文件中加入如下配置。
通过此配置将自己实现的jobSpringFactory类注入到了SchedulerFactoryBean中,承担起了创建job的工作。
5、最后修改一下之前实现的程序。
在之前的程序中scheduler是通过new关键字产生的工厂的方法产生。现在则需要使用spring中已经配置的SchedulerFactoryBean类,并将其实例注入到scheduler上。
到此程序修改完毕,我们需要再尝试一下。
在测试后,我们发现在JobService中的方法sayHelloworld已经被执行,helloworld已经可以成功的打印出来。这就说明JobService已经被成功的注入到了,Job接口实现类JobClass中。这样就成功的实现了在与spring结合时使用quartz实现动态的作业调度。避免了不能复用spring已经实现过的程序的问题。
五、总结
通过此篇文章,我们简单了解Spring和quartz的背景。在此基础通过一个简单的例子,实现了使用Quartz实现动态的计划任务调度。但是在spring环境下,发现Job接口的实现类中,本来应该通过spring的依赖注入的对象,没有被注入。这个造成已经实现的类和方法不能够被复用,因此必须想办法通过手工的方式对类进行注入。最终我们通过复写了spring的相关类完成了对象的注入。以上就是关于spring下的quartz使用的总结,可以为以后在项目开发过程中遇到类似问题,提供参考。
作者简介:
杨鑫:主要从事JAVA方面的研发工作。