Chapter 23. Spring中的定时调度(Scheduling)和线程池(Thread Pooling)

23.1. 简介

Spring包含了对定时调度服务的内置支持类。当前,Spring支持从JDK1.3开始内置的Timer类和Quartz Scheduler(http://www.opensymphony.com/quartz/)。二者都可以通过FactoryBean,分别指向TimerTrigger实例的引用进行配置。更进一步,有个对Quartz Scheduler和Timer都有效的工具类可以让你调用某个目标对象的方法(类似通常的MethodInvokingFactoryBean操作)。Spring 还包含有用于线程池调度的类,它针对Java 1.3,1.4,5和JEE环境的差异都进行了抽象。

23.2. 使用OpenSymphony Quartz 调度器

Quartz使用Trigger, Job以及JobDetail等对象来进行各种类型的任务调度。关于Quartz的基本概念,请参阅http://www.opensymphony.com/quartz。为了让基于Spring的应用程序方便使用,Spring提供了一些类来简化uartz的用法。

23.2.1. 使用JobDetailBean

JobDetail 对象保存运行一个任务所需的全部信息。Spring提供一个叫作JobDetailBean的类让JobDetail能对一些有意义的初始值进行初始化。让我们来看个例子:

<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailBean">
  <property name="jobClass" value="example.ExampleJob" />
  <property name="jobDataAsMap">
    <map>
      <entry key="timeout" value="5" />
    </map>
  </property>
</bean>

Job detail bean拥有所有运行job(ExampleJob)的必要信息。通过job的data map来制定timeout。Job的data map可以通过JobExecutionContext(在运行时刻传递给你)来得到,但是JobDetailBean同时把从job的data map中得到的属性映射到实际job中的属性中去。 所以,如果ExampleJob中包含一个名为timeout的属性,JobDetailBean将自动为它赋值:

package example;

public class ExampleJob extends QuartzJobBean {

  private int timeout;
  
  /**
   * Setter called after the ExampleJob is instantiated
   * with the value from the JobDetailBean (5)
   */ 
  public void setTimeout(int timeout) {
    this.timeout = timeout;
  }
  
  protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
      // do the actual work
  }
}

当然,你同样可以对Job detail bean中所有其他的额外配置进行设置。

注意:使用namegroup属性,你可以分别修改job在哪一个组下运行和使用什么名称。 默认情况下,job的名称等于job detail bean的名称(在上面的例子中为exampleJob)。

23.2.2. 使用 MethodInvokingJobDetailFactoryBean

通常情况下,你只需要调用特定对象上的一个方法即可实现任务调度。你可以使用MethodInvokingJobDetailFactoryBean准确的做到这一点:

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  <property name="targetObject" ref="exampleBusinessObject" />
  <property name="targetMethod" value="doIt" />
</bean>

上面例子将调用exampleBusinessObject中的doIt方法(如下):

public class ExampleBusinessObject {
  
  // properties and collaborators
  
  public void doIt() {
    // do the actual work
  }
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

使用MethodInvokingJobDetailFactoryBean你不需要创建只有一行代码且只调用一个方法的job, 你只需要创建真实的业务对象来包装具体的细节的对象。

默认情况下,Quartz Jobs是无状态的,可能导致jobs之间互相的影响。如果你为相同的JobDetail指定两个Trigger, 很可能当第一个job完成之前,第二个job就开始了。如果JobDetail对象实现了Stateful接口,就不会发生这样的事情。 第二个job将不会在第一个job完成之前开始。为了使得jobs不并发运行,设置MethodInvokingJobDetailFactoryBean中的concurrent标记为false

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  <property name="targetObject" ref="exampleBusinessObject" />
  <property name="targetMethod" value="doIt" />
  <property name="concurrent" value="false" />
</bean>
[Note]Note

注意:默认情况下,jobs在并行的方式下运行。

23.2.3. 使用triggers和SchedulerFactoryBean来包装任务

我们已经创建了job details,jobs。我们同时回顾了允许你调用特定对象上某一个方法的便捷的bean。 当然我们仍需要调度这些jobs。这需要使用triggers和SchedulerFactoryBean来完成。 Quartz自带一些可供使用的triggers。Spring提供两个子类triggers,分别为CronTriggerBeanSimpleTriggerBean

Triggers也需要被调度。Spring提供SchedulerFactoryBean来暴露一些属性来设置triggers。SchedulerFactoryBean负责调度那些实际的triggers。

几个例子:

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="jobDetail" />
    <!-- 10 seconds -->
    <property name="startDelay" value="10000" />
    <!-- repeat every 50 seconds -->
    <property name="repeatInterval" value="50000" />
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="exampleJob" />
    <!-- run every morning at 6 AM -->
    <property name="cronExpression" value="0 0 6 * * ?" />
</bean>

现在我们创建了两个triggers,其中一个开始延迟10秒以后每50秒运行一次,另一个每天早上6点钟运行。 我们需要创建一个SchedulerFactoryBean来最终实现上述的一切:

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="cronTrigger" />
            <ref bean="simpleTrigger" />
        </list>
    </property>
</bean>

更多的属性你可以通过SchedulerFactoryBean来设置,例如job details使用的Calendars, 用来订制Quartz的一些属性以及其它相关信息。 你可以查阅相应的JavaDOC(http://www.springframework.org/docs/api/org/springframework/scheduling/quartz/SchedulerFactoryBean.html)来了解进一步的信息。

23.3. 使用JDK Timer支持类

另外一个调度任务的途径是使用JDK Timer对象。更多的关于Timer的信息可以在这里http://java.sun.com/docs/books/tutorial/essential/threads/timer.html找到。 上面讨论的概念仍可以应用于Timer的支持。你可以创建定制的timer或者调用某些方法的timer。包装timers的工作由TimerFactoryBean完成。

23.3.1. 创建定制的timers

你可以使用TimerTask创建定制的timer tasks,类似于Quartz中的jobs:

public class CheckEmailAddresses extends TimerTask {

  private List emailAddresses;
  
  public void setEmailAddresses(List emailAddresses) {
    this.emailAddresses = emailAddresses;
  }
  
  public void run() {
    // iterate over all email addresses and archive them
  }
}

包装它很简单:

<bean id="checkEmail" class="examples.CheckEmailAddress">
    <property name="emailAddresses">
        <list>
            <value>test@springframework.org</value>
            <value>foo@bar.com</value>
            <value>john@doe.net</value>
        </list>
    </property>
</bean>

<bean id="scheduledTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
    <!-- wait 10 seconds before starting repeated execution -->
    <property name="delay" value="10000" />
    <!-- run every 50 seconds -->
    <property name="period" value="50000" />
    <property name="timerTask" ref="checkEmail" />
</bean>

注意若要让任务只运行一次,你可以把period属性设置为0(或者负值)。

23.3.2. 使用 MethodInvokingTimerTaskFactoryBean

和对Quartz的支持类似,对Timer的支持也包含一个组件,可以让你周期性的调用某个方法:

<bean id="doIt" class="org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean">
    <property name="targetObject" ref="exampleBusinessObject" />
    <property name="targetMethod" value="doIt" />
</bean>

以上的例子会调用exampleBusinessObject对象的doIt方法。(见下):

public class BusinessObject {
  
  // properties and collaborators
  
  public void doIt() {
    // do the actual work
  }
}

将上例中ScheduledTimerTasktimerTask引用修改为doIt,bean将会用一个固定的周期来调用doIt方法。

23.3.3. 打包:使用TimerFactoryBean来设置任务

TimerFactoryBean类和Quartz的SchedulerFactoryBean类有些类似,它们是为同样的目的而设计的:设置确切的任务计划。TimerFactoryBean对一个Timer进行配置,设置其引用的任务的周期。你可以指定是否使用背景线程。

<bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean">
    <property name="scheduledTimerTasks">
        <list>
            <!-- see the example above -->
            <ref bean="scheduledTask" />
        </list>
    </property>
</bean>

23.4. SpringTaskExecutor抽象

Spring 2.0 为执行器(Executor)处理引入了一个新的抽象层。Executor是Java 5的名词,用来表示线程池的概念。之所以用这个奇怪的名词,是因为实际上不能保证底层实现的确是一个池。实际上,很多情况下,executor只是单线程。Spring的抽象层帮助你把线程池引入到Java 1.3和1.4环境中,同时隐藏了 1.3, 1.4, 5, 和 Java EE环境中线程池实现的差异。

23.4.1. TaskExecutor接口

Spring的TaskExecutor接口等同于java.util.concurrent.Executor接口。实际上,它存在主要原因是为了在使用线程池的时候,将对Java 5的依赖抽象掉。这个接口只有一个方法execute(Runnable task),它根据线程池的语义和配置,来接受一个执行任务。

23.4.2. 何时使用TaskExecutor接口

TaskExecutor接口开始的时候,是为了其他Spring组件使用线程池的抽象需要创建的。例如ApplicationEventMulticaster组件、JMS的 AbstractMessageListenerContainer和对Quartz的整合都使用了TaskExecutor接口来抽象线程池。当然,如果你的bean需要线程池行为,你也可以使用这个抽象层。

23.4.3. TaskExecutor类型

在Spring发行包中预定义了一些TaskExecutor实现。有了它们,你甚至不需要再自行实现了。

  • SimpleAsyncTaskExecutor

    这个实现不重用任何线程,或者说它每次调用都启动一个新线程。但是,它还是支持对并发总数设限,当超过线程并发总数限制时,阻塞新的调用,直到有位置被释放。如果你需要真正的池,请继续往下看。

  • SyncTaskExecutor

    这个实现不会异步执行。相反,每次调用都在发起调用的线程中执行。它的主要用处是在不需要多线程的时候,比如简单的test case。

  • ConcurrentTaskExecutor

    这个实现是对Java 5 java.util.concurrent.Executor类的包装。有另一个备选, ThreadPoolTaskExecutor类,它暴露了Executor的配置参数作为bean属性。很少需要使用ConcurrentTaskExecutor, 但是如果ThreadPoolTaskExecutor不敷所需,ConcurrentTaskExecutor是另外一个备选。

  • SimpleThreadPoolTaskExecutor

    这个实现实际上是Quartz的SimpleThreadPool类的子类,它会监听Spring的生命周期回调。当你有线程池,需要在Quartz和非Quartz组件中共用时,这是它的典型用处。

  • ThreadPoolTaskExecutor

    这个实现只能在Java 5环境中使用,但是却是这个环境中最常用的。它暴露的bean properties可以用来配置一个java.util.concurrent.ThreadPoolExecutor,把它包装到一个TaskExecutor中。如果你需要更加先进的类,比如ScheduledThreadPoolExecutor,我们建议你使用ConcurrentTaskExecutor来替代。

  • TimerTaskExecutor

    这个实现使用一个TimerTask作为其背后的实现。它和SyncTaskExecutor的不同在于,方法调用是在一个独立的线程中进行的,虽然在那个线程中是同步的。

  • WorkManagerTaskExecutor

    这个实现使用了CommonJ WorkManager作为其底层实现,是在Spring context中配置CommonJ WorkManager应用的最重要的类。和SimpleThreadPoolTaskExecutor类似,这个类实现了WorkManager接口,因此可以直接作为WorkManager使用。

23.4.4. 使用TaskExecutor接口

Spring的TaskExecutor实现作为一个简单的JavaBeans使用。在下面的示例中,我们定义一个bean,使用 ThreadPoolTaskExecutor来异步打印出一系列字符串。

import org.springframework.core.task.TaskExecutor;

public class TaskExecutorExample {

  private class MessagePrinterTask implements Runnable {

    private String message;

    public MessagePrinterTask(String message) {
      this.message = message;
    }

    public void run() {
      System.out.println(message);
    }

  }

  private TaskExecutor taskExecutor;

  public TaskExecutorExample(TaskExecutor taskExecutor) {
    this.taskExecutor = taskExecutor;
  }

  public void printMessages() {
    for(int i = 0; i < 25; i++) {
      taskExecutor.execute(new MessagePrinterTask("Message" + i));
    }
  }

}

可以看到,无需你自己从池中获取一个线程来执行,你把自己的Runnable类加入到队列中去,TaskExecutor使用它自己的内置规则来决定何时应该执行任务。

为了配置TaskExecutor使用的规则,暴露了简单的bean properties。

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  <property name="corePoolSize" value="5" />
  <property name="maxPoolSize" value="10" />
  <property name="queueCapacity" value="25" />
</bean>

<bean id="taskExecutorExample" class="TaskExecutorExample">
  <constructor-arg ref="taskExecutor" />
</bean>