第31章 Spring中的任务调度和线程池支持(一)

第31章 Spring中的任务调度和线程池支持

本章内容

  • Spring与Quartz

  • Spring对JDKTimer的集成

  • Executor的孪生兄弟TaskExecutor

在我们眼中,或许批处理(Batch Processing)是那么的不引人注目,但不可否认的一点是,几乎所有的企业级应用程序都会或多或少地依赖于相关的批处理程序。可以说,批处理在企业级应用程序中占据重要地位,这或许也正是Spring框架的娘家Interface21(现在叫SpringSource)要特地发起SpringBatch(http://static.springframework.org/spring-batch/)子项目的原因吧!

为了能够有效地管理批处理程序中的各种批处理任务(Batch Job),我们通常会根据当前场景需求,选用某种合适的任务调度程序(Job Scheduler)。至于可供我们选择的调度程序,则可能随需求的不同而变化,从操作系统自身提供的简单任务调度功能支持(比如,Unix/Linux的cron,或者Windows的“计划任务”,到开源的调度程序实现(比如OpenSymphony的Quartz(http://www.penswcphony.omquartz/),一直到功能更为强劲的商业产品(比如BMC的CONTROL-M或者CA的AutoSys),可以说是不一而足。虽然商业产品可以提供更多更强劲的功能支持,比如灾难恢复、运行期间任务状态监控等,但在需求不是很复杂的情况下,使用商业产品显得有些“大材小用”的感觉。对于一般的任务调度需求,普通的任务调度程序就能满足要求,比如OpenSymphony的Quartz。我们不打算就批处理的历史,甚至所有的任务调度产品的秉性做什么长篇大论,JDK1.3之后引入的Timer和OpenSymphone的Quartz是我们即将开幕的这场戏的主角,尤其是在有Spring做它们后盾的情况下。

31.1 Spring与Quartz

31.1.1 初识Quartz

Quartz是OpenSymphony(http://www.opensymphony.com/)开发的一款开源的任务调度框架(或者说任务调度引擎)。相对于JDK的Timer之类的简单任务调度程序来说,Quartz拥有更为全面的功能支持:

  • 允许批处理任务状态的持久化,并且提供不同的持久化策略支持;

  • 批处理任务的远程调度;

  • 提供基于Web的监控接口;

  • 集群支持;

  • 插件式的可扩展性。

要想完全理解和使用Quartz所有的功能特性,或许需要些许时日。不过,如果想快速上手使用Quartz进行任务调度的话,仅理解Quartz相关的几个基本概念,或许真的使得这一想法变得可行。Quartz拥有明确的角色划分,分别对应处理任务调度过程中的各种相关关注点,如下所述。

  • Job。代表将被调度的任务,一个任务要想让Quartz能够调度它,Job的帽子是必须戴上的。

  • JobDetail。JobDetail的主要职能是提供Job执行的上下文信息,Job所依赖的某些配置信息,可以通过JobDetail提供,二者通过JobDataMap进行数据交互。

  • Trigger。Trigger用于触发被调度任务的执行,可以根据不同的触发条件,提供不同的Trigger实现。

  • Scheduler。Quartz的核心调度程序,负责管理所有相关的Trigger和JobDetail,是最终掌管调度任务生杀大权的“人”。

图31-1或许可以更为形象地描述Quartz各种角色之间的关系。

image-20220721110322304

通常情况下,一个Trigger只能用于一个Job,但多个Trigger可以用于同一Job。这一特性可以帮助我们实现更加复杂的调度需求,稍后我们将就这一点做更多阐述。

要使用Quartz进行任务调度,首先我们必须有能够被调度的任务实现,给将被调度的对象冠以Job或者StatefulJob的“头衔”,可以让我们得到一个可被Quartz调度的任务类。还记得我们最初的FXNewProvider吗?现在该是让它真正上场的时间了!现在,我们构建FXNewsJob,以便Quartz可以定时调度它去抓取相应的外汇新闻信息,具体实现如下方代码清单所示。

public class FXNewsJob implements Job {
    
    
	private FXNewsProvider fxNewsProvider;
  
	public void execute(JobExecutionContext jobExecCtx) throws JobExecutionException {
    
    
		getFxNewsProvider().getAndPersistNews();
  	}
  
	public FXNewsProvider getFxNewsProvider() {
    
    
		return fxNewsProvider;
  	}
  
	public void setFxNewsProvider(FXNewsProvider fxNewsProvider) {
    
    
		this.fxNewsProvider = fxNewsProvider;
  	}
}

有了要调度的任务,接着需要考虑的问题就是,应该在什么样的时机下调用它,而这也正是Trigger存在的意义。Quartz中最经常使用的且是主要的两种Trigger实现类,一个是SimpleTrigger,可以指定简单的基于时间间隔的调度规则;另一个就是CronTrigger,可以类似于Unix/Linux操作系统中Cron程序所使用的表达式来指定调度规则。通常情况下,仅使用CronTrigger即可满足所有的调度需求。在Quartz中,我们可以直接实例化相应的Trigger实现类并使用,例如:

Trigger simpleTrigger = new SimpleTrigger("triggerName", Scheduler.DEFAULT_GROUP, new Date(), nul1, SimpleTrigger.REPEAT_INDEFINITELY, 60000);

Trigger cronTrigger = new CronTrigger("cronTriggerName", Scheduler.DEFAULT_GROUP, "0 0/1 * * * ?");

以上两个Trigger定义完成同样的功能,将使得对应的调度任务每隔一分钟被调度一次。Quartz中的每一Trigger都归属于相应的组,在它们各自的组中需要有唯一的名称。在我们的定义中,我们让SimpleTrigger和CronTrigger同样归属于Scheduler的默认组。有关两种Trigger的更多信息,请参照对应的Javadoc文档,我们就不再做更多解释了,尤其是CronTrigger,要发挥它的最大能量,足够熟悉cron表达式是必须的。

在Job和Trigger都具备之后,我们就该通过Scheduler将它们关联起来并进行最终的任务调度,如下所示:

Scheduler scheduler = new StaSchedulerFactory().getScheduler();
scheduler.start();

JobDetail jobDetail = new JobDetail("jobName", Scheduler.DEFAULT_GROUP, FXNewsJob.class);
Trigger cronTrigger = new CronTrigger("cronTriggerName", Scheduler.DEFAULT_GROUP, "0 0/1 * * * ?");

scheduler.scheduleJob(jobDetail, cronTrigger);

Scheduler是从相应的Factory中获取的。在使用获取到的Scheduler进行任务调度之前,需要start()该scheduler。我们可以发现,相应的Job不是由我们直接实例化,而是通过JobDetail以Class类型提供的,也就是说,Scheduler将通过反射来实例化并执行相应的调度任务。在start()之后再调用Scheduler的scheduleJob(..)方法,我们的FXNewsJob就算被提交执行了。整个过程看起来好像并不像想象的那么难,不是吗?

注意:有关Quartz框架的更多信息可以参考Open Symphony官方网站提供的详细文档。另外,Quartz Job Scheduling Framework一书对Quartz框架给予了全面的介绍,阅读此书是了解Quartz的最佳途径。

原理上来说,使用Quartz进行任务调度的基本过程就是这个样子。不过,你觉得我们的FXNewsJob可以被正确地调度吗?实际上,我根本就没有去运行这段调度程序,因为我知道它注定执行不了,它所依赖的所有相关对象都没有实例化,它又怎么会成功运行呢?为了让被调度的任务能够享受依赖注入等一系列服务,也为了让Quartz的使用更加便捷和可配置,Spring对Quartz的集成需求自然也就该浮出水面了。

31.1.2 融入Spring大家庭的Quartz

Spring对Quartz的集成主要体现在使用的便利性上,比如,为各种Quartz相关的概念实体(Job、JobDetail等)提供合理的默认值,或者提供更加接近普通bean风格的配置形式等。当然,最主要的,纳入Spring框架管理的Quartz,将很自然地获得依赖注入和AOP等相关服务。

在对Quartz(甚至Timer)的集成过程中,Spring基本上是清一色地采用了FactoryBean这一“制式装备”。只要抓住这一特点,整个Spring对Quartz的集成就变得容易理解多了。

1. Job的实现策略

在Quartz中,每一Job所需要的执行上下文(Execution Context)信息是由其对应的JobDetail提供的,二者通过JobDataMap进行数据通信。如果我们通过JobDetail的JobDataMap设置Job执行的上下文信息,如下所示:

JobDetail jobDetail = new JobDetail("jobName", Scheduler.DEFAULT_GROUP, HelloWorldJob.class);
jobDetail.getJobDataMap().put("message", "helloworld");
jobDetail.getJobDataMap().put("counter", 10);

那么,在对应的HelloWorldJob中,我们就可以通过如下方式获得这些上下文信息,并使用它们:

public class HelloworldJob implements Job {
    
    
	public void execute(JobExecutionContext ctx) throws JobExecutionException {
    
    
		JobDataMap jobDataMap = ctx.getJobDetail().getJobDataMap();
		String messsage = jobDataMap.getString("message");
		int counter = jobDataMap.getInt("counter");
		// ...
  	}
}

为了改善具体Job实现类获取上下文信息的“生态环境”,Spring提供了org.springframework.schedul.ing.quartz.QuartzJobBean。在实现Job的时候,通过继承QuartzJobBean,而不是直接实现Job接口,可以让我们在Job实现类中,直接以bean属性的形式访问当前Job执行的上下文信息。下方代码清单给出了一个QuartzJobBean实现实例。

public class HelloWorldJobExtendingQuartzJobBean extends QuartzJobBean {
    
    
  private String message;
	private int counter;

  @Override
	protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException {
    
    
		// getMessage()并使用
		// getCounter()并使用
  	}
	public String getMessage() {
    
    
		return message;
  	}
	public  void setMessage(String message) {
    
    
    this.message = message;
  	}
	public int getCounter() {
    
    
		return counter;
  	}
	public void setCounter(int counter) {
    
    
		this.counter = counter;
  	}
}

QuartzJobBean所做的工作,将保证上下文信息在executeInternal(JobExecutionContext)方法执行之前被逐一注入到相应的bean属性中。如果只从Job执行上下文信息的获取这一点来看,QuartzJobBean算是一种进步,但实际上,QuartzJobBean本身的极具侵入性,却使得它基本丧失存在的价值。如果某一Job实现确实需要在执行之前做一些后处理的话,比如刚才的获取执行上下文信息,那么完全可以提供相应的后处理机制来实现,而没有必要非得强制人家去继承某一父类,不是吗?不过,这或许是Quartz历史遗留问题导致的,在1.5或者更高版本之后,Quartz提供了一种JobFactory机制,允许对要执行的Job进行定制。这样,相应的Job实现就不需要受到任何父类继承的限制了。Spring提供的org.springframework.scheduling.quartz.SpringBeanJobFactory就是一种能够完成QuartzJobBean同样使命的JobFactory实现。所以,如果你能够使用Quartz1.5或者更高版本,那么把QuartzJobBean完全放到脑后吧!

2. JobDetail的更多选择

按照Quartz的惯例,一个典型的Job实现类,最终需要一个对应的JobDetail来帮助提供执行的上下文信息。现在,我们可以通过org.springframework.scheduling.quartz.JobDetailBean来创建和配置相应Job所对应的JobDetail实例,如下方代码清单所示。

<bean id="fxNewsJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
	<property name="jobC1ass" value="HelloworldJobIHelloworldJobExtendingQuartzJobBean"/>
	<property name="jobDataAsMap">
		<map>
			<entry key="message">
				<value>He1loWorld</value>
			</entry>
			<entry key="">
				<value>10</value>
			</entry>
		</map>
	</property>
</bean>

可以看到,原先通过编码指定的Job类型和添加到JobDataMap的执行上下文信息,现在全部可以通过配置的方式完成设置。而且,原先编码实例化JobDetail的时候,必须指定JobDetail所在的组和组内的唯一标志名称,现在即使我们不明确指定,JobDetailBean也会提供合理的默认值(以bean定义的名称作为JobDetail的名称,使用DEFAULT组作为jobDetail的组)。

JobDetailBean支持Spring为JobDetail提供的最基础的集成设施。不过,MethodInvokingJobDetailFactoryBean或许更加讨人喜爱一些。对于将被调度的业务逻辑来说,合理情况下,应该以独立的形式而存在,不应该因为某些业务逻辑可能会被调度执行,就将它们直接编码到Job实现类中(或者Timer的TimerTask中)。这样,当需要对它们进行调度的时候,只需要根据当前情况提供相应的Job实现类,让Job实现类在调度方法中调用相应的业务对象即可,而我们的FXNewsProvider和FXNewsJob之间的关系,实际上就是如此。不过,要是调度程序执行的业务逻辑涉及多个独立的业务对象,我们可能就得为每个业务对象都提供一个薄薄的Job实现类了,而实际上,这些Job实现类中可能也只是调用业务对象方法的一行代码而已,MethodInvokingJobDetailFactoryBean的出现可以让我们避免这种尴尬。

实际上,对于FXNewsProvider的执行调度来说,我们也确实没有提供单独的FXNewsJob实现,而是直接让MethodInvokingJobDetailFactoryBean帮我们在执行的时候直接调用FXNewsProvider的相应方法,如下配置代码所示:

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	<property name="targetObject" ref="fxNewsProvider"/>
	<property name="targetMethoa" value="getAndPersistNews"/>
</bean>

使用MethodInvokingJobDetailFactoryBean,我们只需要指定调度执行时候应该调用哪个对象实例的哪一方法就可以了,MethodInvokingJobDetailFactoryBean将根据这些信息,通过反射完成指定业务方法的调用。至于MethodInvokingJobDetailFactoryBean作为JobDetail为什么能够接受非Job类型的对象作为Job(FXNewsProvider之类的对象并不需要实现Job之类的接口),原理上实际也很简单,MethodInvokingJobDetailFactoryBean会在内部构建相应的Job实现类(MethodInvokingJobDetailFactoryBean.MethodInvokingJob和MethodInvokingJobDetailFactoryBean.StatefulMethodInvokingJob),这些Job实现类会在合适的时机调用指定给MethodInvokingJobDetailFactoryBean的业务对象上的业务方法。所以,你看,MethodInvokingJobDetailFacto-ryBean实际上并没有“推卸”它作为JobDetail必须管理Job类型调度任务的责任。

虽然使用MethodInvokingJobDetailFactoryBean可以带给我们很大的便利,但是,通过它所返回的JobDetail信息是不可序列化的,从而也就无法保存或者说持久化。所以,对于状态紧要的调度任务来说,还是应该给出具体的Job实现类,并结合JobDetailBean进行管理。

注意:有关JobDetailBean和MethodInvokingJobDetailFactoryBean的更多配置项,在实际开发过程中,请直接参考相应类的Javadoc文档。

3. Trigger的可配置化

Quartz的两种Trigger实现SimpleTrigger和CronTrigger,实际上已经可以作为普通的JavaBean添加到Spring的IoC容器进行管理,但是,在实例化的时候,它们都需要指定相应的管理组和组内唯一标志名称。考虑到这一点,Spring分别为它们提供了对应的SimpleTriggerBean和CronTriggerBean封装类。SimpleTriggerBean和CronTriggerBean可以提供合理的默认值,比如以bean定义名称直接作为Trigger名称,以DEFAULT组作为trigger默认的组等,从而免去每次都要指定所在组和组内标志名称的琐事。当然,SimpleTriggerBean和CronTriggerBean也同样是采用FactoryBean机制实现的。

基本上,原先SimpleTrigger和CronTrigger所拥有的属性,在配置SimpleTriggerBean和CronTriggerBean的时候都可以使用,下方代码清单给出了一段二者的配置代码示例。

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
	<property name="jobDetail" ref="jobDetail"/>
	<property name="repeatInterval" value="3000"/>
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
	<property name="jobDetail" ref="jobDetail"/>
	<property name="cronExpression" value="0 0/1 * * * ?"/>
</bean>

对于二者来说,共同的配置项就是jobDetail。在最初介绍Quartz的时候我们提过,每个Trigger都只对应一个Job(或者说JobDetail,因为Job和JobDetail通常是一对一的),所以,为了让Quartz的Scheduler在调度的时候,可以直接以scheduler.scheduleJob(trigger);的形式,而不是最初的scheduler.scheduleJob(jobDetail, cronTrigger);形式进行调度,我们通常需要在配置Trigger的时候将与之关联的JobDetail指定给它。

在实际开发过程中,通常会遇到比较棘手的调度需求。对于这种情况,使用单一的Trigger往往达不到效果,即使是采用表达能力较强的CronTrigger也是如此,这时,我们不妨从以下两种方向进行考虑。

  • 虽然一个Trigger同时只能对应一个Job,但是多个Trigger所对应的Job却可以是同一个,所以,我们可以通过组合多个Trigger的形式来指定同一Job的调度规则,一个Trigger完成总调度规则的一个规则子集,最终所有Trigger的规则自己取并集就是最终我们所需要的调度规则,如图31-2所示。

image-20220721180347968

  • 多个Trigger规则的附加或许更多的是为了补足单一Trigger调度规则覆盖范围不够的问题。如果单一Trigger的规则覆盖范围足够大,只是覆盖范围内有小部分调度规则需要剔除,我们可以结合Trigger与Quartz提供的Calender来达成最终所需要的调度规则。Trigger负责提供覆盖范围足够广的调度规则,而Calender负责排除这一规则范围内不需要的部分,如图31-3所示。

image-20220721180434187

有关Calender和HolidayCalender的信息,可以参考Quartz相关文档。

当然,解决问题的方式肯定不止一种两种,如果你可以想出更好的解决方式,一定得告诉我。

4. Scheduler的新家

Scheduler本来就是从相应的Factory中来的(Scheduler scheduler = new stdSchedulerFactory().getscheduler()),所以,使用FactoryBean对其进行封装看起来也是很自然的事情。Spring提供了org.springframework.scheduling.quartz.SchedulerFactoryBean对Quartz的Scheduler进行管理,通过它,我们可以为所管理的Scheduler实例注册相应的Trigger、Calender等系列“装备”。当然,大部分时候,或许只是注册必要的Trigger而已,如下所示:

<bean id="scheduler" class="org.springFramework.scheduling.quartz.SchedulerFactoryBean">
	<property name="triggers">
		<list>
			<ref bean="newsTrigger"/>
			<ref bean="simpleTrigger"/>
		</list>
	</property>
</bean>

SchedulerFactoryBean所管理的Scheduler将随ApplicationContext的实例化自动启动,随ApplicationContext的关闭自动关闭,所以,在将调度过程中所需要的所有对象实例添加到IoC容器之后,只要实例化SchedulerFactoryBean所在的ApplicationContext,Scheduler即开始执行正式的任务调度,如下代码所示:

public static void main(String[] args) {
    
    
	ApplicationContext ctx = new ClassPathXmlApplicationContext(".../container-configlocation.xml");
	((AbstractApplicationContext)ctx).registerShutdownHook();
}

大部分情况下,我们都会在IoC容器启动之前将所有任务调度相关的对象配备完毕,所以,容器启动之后,并不需要获取SchedulerFactoryBean所管理的Scheduler实例。不过,如果在运行期间依然需要动态注册某些调度任务的话,可以从ApplicationContext中获取SchedulerFactoryBean对应的对象实例,并强制转型为Scheduler类型,如下所示:

ApplicationContext ctx = new ClassPathXmlApplicationContext(".../container-config-location.xml");
((AbstractApplicationContext)ctx).registerShutdownHook();

Scheduler scheduler = (Scheduler)ctx.getBean("scheduler");
scheduler.addlJob(newJobDetail, true);
// 如果必要,使用scheduler做进一步操作.....

至于为什么将SchedulerFactoryBean强制转型为Scheduler,我想就不必解释了。如果你不清楚,那么回头看一下FactoryBean的特性。最后,我们给出的是FXNewsProvider对应的任务调度相关配置片段,如下方代码清单所示。

<!--//其他可能依赖的bean定义-->
<bean id="fxNewsProvider" class="org.darrenstudio.books.unveilspring.news.FXNewsProvider" p:newsListener-ref="newsListener" p:newPersistener-ref="newsPersister"/>

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	<property name="targetObject" ref="fxNewsProvider"/>
	<property name="targetMethod" values"getAndPersistNews"/>
</bean>

<bean id="newsTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
	<property name="jobDetail" ref="jobDetai1"/>
	<property name="cronExpression" value="0 0/1 * * * ?"/>
</bean>

<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
	<property name="triggers">
		<list>
			<ref bean="newsTrigger"/>
		</list>
	</property>
</bean>

这或许可以帮助你回顾一下从“原生态的Quartz"走到“Spring中的Quartz”这整个旅程。实际上,如果要添加新的调度任务的话,基本上也就是在重复以上这一配置过程,只不过每一调度任务特定的细节还是需要我们时刻关注的。

猜你喜欢

转载自blog.csdn.net/qq_34626094/article/details/125956835