Spring 为创建 Quartz 的 Scheduler、Trigger 和 JobDetail 提供了便利的 FactoryBean 类,以便能够在 Spring 容器中享受注入的好处。此外,Spring 还提供了一些便利工具类,用于直接将 Spring 中的 Bean 包装成合法的任务。Spring 进一步降低了使用 Quartz 的难度,能以更具风格的方式使用 Quartz。概括来说,Spring 提供了两方面的支持。
(1)为 Quartz 的重要组件类提供更具 Bean 风格的扩展类。
(2)提供创建 Scheduler 的 BeanFactory 类,方便在 Spring 环境下创建对应的组件对象,并结合 Spring 容器生命周期执行启动和停止的动作。
1.创建JobDetail
用户可以直接使用 Quartz 的 JobDetail 在 Spring 中配置一个 JobDetail Bean,但是 JobDetail 使用带参的构造函数,对于习惯通过属性配置的 Spring 用户来说存在使用上的不便。为此,Spring 通过扩展 JobDetail 提供了一个更具 Bean 风格的 JobDetailFactoryBean。
此外,Spring 还提供了一个 MethodInvokingJobDetailFactoryBean,通过这个 FactoryBean可以将 Spring 容器中 Bean 的方法包装成 Quartz 任务,这样开发者就不必为 Job 创建对应的类。
1)JobDetailFactoryBean
JobDetailFactoryBean 扩展于 Quartz 的 JobDetail。使用该 Bean 声明 JobDetail 时,Bean 的名字即任务的名字,如果没有指定所属组,就使用默认组。除了 JobDetail 中的属性外,还定义了以下属性。
(1)jobClass:类型为 Class,实现 Job 接口的任务类。
(2)beanName:默认为 Bean 的 id 名,通过该属性显式指定 Bean 名称,它对应任务的名称。
(3)jobDataAsMap:类型为 Map,为任务所对应的 JobDataMap 提供值。之所以需要提供这个属性,是因为用户无法在 Spring 配置文件中为 JobDataMap 类型的属性提供信息,所以 Spring 通过 jobDataAsMap 设置 JobDataMap 的值。
(4)applicationContextJobDataKey:用户可以将 SpringApplicationContext 的引用保存到 JobDataMap 中,以便在 Job 的代码中访问 ApplicationContext。为了达到这个目的,用户需要指定一个键,用于在 jobDataAsMap 中保存 ApplicationContext。如果不设置此键,JobDetailBean 就不会将 ApplicationContext 放入 JobDataMap 中。
(5)jobListenerNames:类型为 String[],指定注册在 Scheduler 中的 JobListeners 名称,以便让这些监听器对本任务的事件进行监听。
下面的配置片段使用 JobDetailBean 在 Spring 中配置一个 JobDetail。
<bean name="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean" p:jobClass="com.smart.quartz.MyJob" p:applicationContextJobDataKey="applicationContext"> <property name="jobDataAsMap"> <map> <entry key="size" value="10" /> </map> </property> </bean>
JobDetaiIFactoryBean 封装了 MyJob 任务类,并为 Job 对应的 JobDataMap 设置了一个键为 size 的数据。此外,通过指定 applicationContextJobDataKey,让 Job 的 JobDataMap 持有 SpringApplicationContext 的引用。
这样,MyJob 在运行时就可以通过 JobDataMap 访问到 size 和 ApplicationContext。来看一下 MyJob 的代码,如下面代码所示。
public class MyJob implements StatefulJob { public void execute(JobExecutionContext jctx) throws JobExecutionException { Map dataMap = jctx.getJobDetail().getJobDataMap();//①获取JobDetail关联的JobDataMap //Map dataMap = jctx.getTrigger().getJobDataMap(); String size =(String)dataMap.get("size");//② ApplicationContext ctx = (ApplicationContext)dataMap.get("applicationContext");//③ System.out.println("size:"+size); dataMap.put("size",size+"0");//④对JobDataMap所做的更改是否会被持久化取决于任务的类型 String count =(String)dataMap.get("count"); System.out.println("count:"+count); } }
在②处获取 size 值,在③处可以根据键 applicanonContext 获取 ApplicationContext,有了 ApplicationContext 的引用,Job 就可以毫无障碍地访问 Spring 容器中的任何 Bean。MyJob 可以在 execute() 方法中对 JobDataMap 进行更改,如④处所示。如果 MyJob 实现了 Job 接口,则这种更改对于下一次执行是不可见的;如果 MyJob 实现了 StatefulJob 接口,则这种更改对于下一次执行是可见的。
2)MethodInvokingJobDetailFactoryBean
通常情况下,任务都定义在一个业务类方法中。这时,为了满足 QuartzJob 接口的规定,还需要定义一个引用业务类方法的实现类。为了避免创建这个只包含一行调用代码的 Job 实现类,Spring 提供了 MethodInvokingJobDetailFactoryBean,借由该 FactoryBean,可以将一个 Bean 的某个方法封装成满足 Quartz 要求的 Job。来看一个具体的例子:
<bean id="jobDetail_1" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean" p:targetObject-ref="myService" //①引用一个Bean
p:targetMethod="doJob" //②指定目标Bean的方法
p:concurrent="false" /> //③指定最终封装的任务是否有状态 <bean id="myService" class="com.smart.service.MyService" />
jobDetail1_1 将 MyService#doJob() 封装成一个任务,同时通过 concurrent 属性指定任务的类型。默认情况下封装为无状态的任务。如果希望封装为有状态的任务,仅需将 concurrent 属性设置为 false 就可以了。Spring 通过名为 concurrent 的属性指定任务的类型,能够更直接地描述任务执行的方式(有状态的任务不能并发执行,无状态的任务可并发执行),对于不熟悉 Quartz 内部机制的用户来说,比起 stateful,concurrent 显然更简明达意些。
Myservice 服务类拥有一个 doJob() 方法,它的代码如下所示:
public class MyService { public void doJob(){//①被封装成任务的目标方法 System.out.println("in MyService.dojob()."); } }
doJob() 方法既可以是 static 的,也可以是非 static 的,但不能拥有方法入参。通过 MethodInvokingJobDetailFactoryBean 产生的 JobDetail 不能被序列化,所以不能被持久化到数据库中。如果希望使用持久化任务,则用户只能创建正规的 Quartz 的 Job 实现类。
2.创建Trigger
Quartz 中另一个重要的组件就是 Trigger,Spring 按照相似的思路分别为 SimpleTrigger 和 CronTrigger 提供了更具 Bean 风格的 SimpleTriggerFactoryBean 和 CronTriggerFactoryBean 扩展类,通过这两个扩展类可以更容易地在 Spring 中以 Bean 的方式配置 Trigger。
1)SimpleTriggerFactoryBean
在默认情况下,通过 SimpleTriggerFactoryBean 配置的 Trigger 名称即为 Bean 的名称,属于默认组。 SimpleTriggerFactoryBean 在 SimpleTrigger 的基础上新增了以下属性。
(1)jobDetail:对应的 JobDetail。
(2)beanName:默认为 Bean 的 id 名,通过该属性显式指定Bean名称,它对应 Trigger 的名称。
(3)jobDataAsMap:以 Map 类型为 Trigger 关联的 JobDataMap 提供值。
(4)startDelay:延迟多少时间开始触发,单位为毫秒,默认值为0。
(5)triggerListenerNames:类型为 String[],指定注册在 Scheduler 中的 TriggerListener 名称,以便让这些监听器对本触发器的事件进行监听。
下面的实例使用 SimpleTriggerFactoryBean 定义了一个 Trigger,该 Trigger 和 jobDetail 相关联,延迟1秒后启动,时间间隔为2秒,重复执行100次。此外,还为 Trigger 设置了 JobDataMap 数据。
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean" p:jobDetail-ref="jobDetail"
p:startDelay="1000"
p:repeatInterval="2000" p:repeatCount="100"> <property name="jobDataAsMap"> //① <map> <entry key="count" value="10" /> </map> </property> </bean>
需要特别注意的是,在①处配置的 Map 数据将填充到 Trigger 的 JobDataMap 中,执行任务时必须通过以下方式获取配置的值:
public class MyJob implements StatefulJob { public void execute(JobExecutionContext jctx) throws JobExecutionException { Map dataMap = jctx.getTrigger().getJobDataMap();//①获取Trigger的JobDataMap String count =(String)dataMap.get("count"); dataMap.put("count","30");//②对JobDataMap的更改不会被持久化,不影响下次的执行 } }
2)CronTriggerFactoryBean
CronTriggerFactoryBean 扩展于 CronTrigger,触发器的名称即为 Bean 的名称,保存在默认组中。在 CronTrigger 的基础上,新增的属性和 SimpleTriggerFactoryBean 大致相同,配置的方法也和 SimpleTriggerFactoryBean 相似。下面给出一个简单的例子:
<bean id="checkImagesTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean" p:jobDetail-ref="jobDetail" p:cronExpression="0/5 * * * * ?"/>
3.创建Scheduler
Quartz 的 SchedulerFactory 是标准的工厂类,不太适合在 Spring 环境下使用。此外,为了保证 Scheduler 能够感知 Spring 容器的生命周期,在 Spring 容器启动后,Scheduler 自动开始工作,而在 Spring 容器关闭前,自动关闭 Scheduler。为此,Spring 提供了 SchedulerFactoryBean,这个 FactoryBean 大致拥有以下功能。
(1)以更具 Bean 风格的方式为 Scheduler 提供配置信息。
(2)让 Scheduler 和 Spring 容器的生命周期建立关联,相生相息。
(3)通过属性配置的方式代替 Quartz 自身的配置文件。
来看一个 SchedulerFactoryBean 配置的例子,如下面代码所示。
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> //①注册多个Trigger <list> <ref bean="simpleTrigger" /> </list> </property> <property name="schedulerContextAsMap"> //②以Map类型设置SchedulerContext数据 <map> <entry key="timeout" value="30" /> </map> </property> <property name="configLocation" value="classpath:com/smart/quartz/quartz.properties" />//③显示指定Quartz的配置文件地址 </bean>
SchedulerFactoryBean 的 triggers 属性为 Trigger[] 类型,可以通过该属性注册多个 Trigger,在①处注册了一个 Trigger。Scheduler 拥有一个类似于 ServletContext 的 SchedulerContext。SchedulerFactoryBean 允许用户以 Map 的形式设置 SchedulerContext 的参数值,如②处所示。在默认情况下,Quartz 在类路径下查询 quartz.properties 配置文件,用户也可以通过 configLocation 属性显式指定配置文件的位置,如③处所示。
除了实例中所用的属性外,SchedulerFactoryBean 还拥有一些常见的属性。
(1)calendars:类型为 Map,通过该属性向 Scheduler 注册 Calendar。
(2)jobDetails:类型为 JobDetail[],通过该属性向 Scheduler 注册 JobDetail。
(3)autoStartup:SchedulerFactoryBean 在初始化后是否马上启动 Scheduler,默认为 true。如果设置为 false,则需要手工启动 Scheduler。
(4)startupDelay:在 SchedulerFactoryBean 初始化完成后,延迟多少秒启动 Scheduler,默认值为0,表示马上启动。除非拥有需要立即执行的任务,一般情况下,可以通过属性让 Scheduler 延迟一小段时间后启动,以便让 Spring 能够更快初始化容器中剩余的 Bean。
SchedulerFactoryBean 的一个重要功能是允许用户将 Quartz 配置文件中的信息转移到 Spring 配置文件中,由此带来的好处是配置信息的集中化管理,同时我们不必熟悉多种框架有差异的配置文件结构。回忆一个 Spring 集成的 Hibernate 框架,就知道这是 Spring 在集成第三方框架时采用的惯用招数。通过以下属性代替框架的自身配置文件。
(1)datasource:当需要使用数据库来持久化任务调度数据时,用户可以在 Quartz 中配置数据源,也可以直接在 Spring 中通过 datasource 指定一个 Spring 管理的数据源。如果指定了该属性,那么,即使在 quartz.properties 中已经定义了数据源,也会被 dataSource 覆盖。
(2)transactionManager:可以通过该属性设置一个 Spring 事务管理器。在设置 datasource 时,Spring 强烈推荐用户使用一个事务管理器,否则数据表锁定可能无法正常工作。
(3)nonTransactionalDataSource:在全局事务的情况下,如果用户不希望 Scheduler 执行的相关数据操作参与到全局事务中,则可以通过该属性指定数据源。在 Spring 本地事务的情况下,使用 datasource 属性就足够了。
(4)quartzProperties:类型为 Properties,允许用户在 Spring 中定义 Quartz 的属性,其值将覆盖 quartz.properties配置文件中的设置。这些属性必须是Quartz能够识别的合法属性,在配置时,需要查看 Quartz 的相关文档。
下面是一个配置 quartz.properties 属性的例子:
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> ... <property name="quartzProperties"> //①Quartz属性项1 <props> <prop key="org.quartz.threadPool.class"> org.quartz.simpl.SimpleThreadPool </prop> <prop key="org.quartz.threadPool.threadCount">10</prop>//②Quartz属性项2 </props> </property> </bean>
在实际应用中,我们并不总是在程序部署的时候就确定需要哪些任务,往往需要在运行期根据业务数据动态产生触发器和任务。用户完全可以在运行期通过代码调用 SchedulerFactoryBean 获取 Scheduler 实例,然后动态注册触发器和任务。