34. 任务执行和调度(Task Execution and Scheduling)
34.1 简介
Spring框架为 asynchronous execution 和 scheduling of tasks 提供了两个接口,分别是 TaskExecutor
和 TaskScheduler
. Spring也提供了一些接口来支持 Timer
和 Quartz Scheduler
.
34.2 TaskExecutor 接口
Spring提供的 TaskExecutor
接口等同于 java.util.concurrent.Executor
接口. 这个接口只有一个唯一的方法 execute(Runnable task)
, 这个方法接受一个任务去执行.
34.2.1 TaskExecutor 类型
Spring中包含了很多 TaskExecutor
的具体实现类,很多情况下不需要自己去实现新的类.
SimpleAsyncTaskExecutor
这个实现不会重用任何线程,而是为每次调用启动一个新线程。但是,它确实支持一个并发量的限制,超过限制时它将阻塞任何调用,直到一个槽被释放。如果您正在寻找真正的池,请参阅下面的SimpleThreadPoolTaskExecutor
和ThreadPoolTaskExecutor
的讨论。SyncTaskExecutor
这个实现不会异步地执行调用。相反,每次调用都发生在调用线程中。它主要用于在不需要多线程的情况下,比如简单的测试用例。ConcurrentTaskExecutor
该实现是java.util.concurrent.Executor
的适配器。还有另一种方法ThreadPoolTaskExecutor
,它将Executor
配置参数作为bean属性公开。很少需要使用ConcurrentTaskExecutor
,但是如果ThreadPoolTaskExecutor
不够灵活地满足您的需要,ConcurrentTaskExecutor
是另一种选择。SimpleThreadPoolTaskExecutor
这个实现实际上是Quartz的SimpleThreadPool
的子类,它监听Spring的生命周期回调。当您有一个线程池,可能需要由Quartz和non-Quartz组件共享时,通常会使用这种方法。ThreadPoolTaskExecutor
这个实现是最常用的实现。它公开bean属性来配置java.util.concurrent.ThreadPoolExecutor
并将其包装在TaskExecutor
中。如果您需要适应不同类型的java.util.concurrent.Executor
,建议您使用ConcurrentTaskExecutor
.WorkManagerTaskExecutor
该实现使用 CommonJWorkManager
作为其后备实现,并且是在Spring上下文中设置CommonJWorkManager
引用的中心便利类。与SimpleThreadPoolTaskExecutor
类似,该类实现WorkManager
接口,因此也可以直接作为WorkManager
使用。
34.2.2 使用 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属性已经被公开。
<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>
34.3 TaskScheduler 接口
从Spring 3.0 开始 引入了 TaskScheduler
接口,提供了很多方法,用来在将来某个时刻执行设定的任务.
public interface TaskScheduler {
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Date startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}
其中,只有 ScheduledFuture schedule(Runnable task, Date startTime)
方法只会在指定的时间之后运行一次.其他的方法都会周期的重复执行设定的任务.
34.3.1 Trigger 接口
Trigger
的含义是指执行时间可以根据过去的执行结果,甚至是任意的条件来确定。如果这些决定确实考虑了前一个执行的结果,那么这些信息就可以在 TriggerContext
中使用. Trigger
接口非常简单:
public interface Trigger {
Date nextExecutionTime(TriggerContext triggerContext);
}
public interface TriggerContext {
Date lastScheduledExecutionTime();
Date lastActualExecutionTime();
Date lastCompletionTime();
}
34.3.2 Trigger 接口的实现
Spring提供了 Trigger
接口的两个实现。最有趣的一个是 CronTrigger
。它支持基于cron表达式的任务调度。例如,下面的任务被安排在每小时15分钟的时间内运行,但只在工作日的9点到5点的“营业时间”。
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一个开箱即用的实现是一个 PeriodicTrigger
,它接受一个固定的周期、一个可选的初始延迟值和一个布尔值,以指示该周期是否应该被解释为 fixed-rate
或者 fixed-delay
.
34.4 基于注解的 Scheduling and Asynchronous Execution
Spring为任务调度和异步方法执行提供了注释支持。
34.4.1 开启 scheduling 相关注解
为了支持 @Scheduled
和 @Async
注释,可以将 @EnableScheduling
和 @EnableAsync
注解添加到你的被 @Configuration
注解的类中:
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
您可以自由地选择并为您的应用程序选择相关的注释。例如,如果您只需要 @Scheduled
的支持,那么就简单地省略 @EnableAsync
. 对于更细粒度的控制,您还可以实现 SchedulingConfigurer
和/或 AsyncConfigurer
接口。请参阅javadocs以获得详细信息。
如果您喜欢XML配置,则可以使用 <task:annotation-driven>
元素.
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
请注意,上面的XML中提供了 executor 引用 来处理被 @Async
注解的方法,并且提供了 scheduler 引用 来管理用 @Scheduled
注解的方法.
34.4.2 @Scheduled
注解
@Scheduled
注释可以连同 trigger metadata
一起添加到一个方法中。例如,每5秒就会调用下列方法,并使用 fixed delay
,这意味着周期将从每次调用的完成时间来测量。
@Scheduled(fixedDelay=5000)
public void doSomething() {
// 周期性执行的任务
}
如果需要一个 fixed rate
执行,只需改变注释中指定的属性名。在每次调用的连续开始时间之间,每5秒执行以下操作。
@Scheduled(fixedRate=5000)
public void doSomething() {
// 周期性执行的任务
}
对于 fixed-delay
和 fixed-rate
任务,可以指定一个初始的延迟(initial delay
),指示在方法第一次执行之前等待的毫秒数.
@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
// 周期性执行的任务
}
如果简单的周期调度没有足够的表达能力,那么就可以提供一个cron表达式。例如,下面的内容只在工作日执行.
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// 只在工作日执行的任务
}
你还可以使用 zone
属性来指定cron表达式将被解析的时区。
请注意,要调度的方法必须返回void,并且不能有任何参数.
如果该方法需要与来自应用程序上下文的其他对象进行交互,那么通常是通过依赖注入提供的.
34.4.3 @Async
注解
可以在一个方法上提供 @Async
注释,以便该方法的调用是异步发生的。换句话说,调用者在调用时立即返回,并且该方法的实际执行将发生在已提交给Spring的 TaskExecutor
的任务中。在最简单的情况下,注释可能被应用到一个返回void的方法中.
@Async
void doSomething() {
// 这将异步执行
}
与用 @Scheduled
注释标注的方法不同,这些方法可以有参数,因为它们将在运行时由调用者以“正常”的方式调用,而不是由容器管理的预定任务调用。
例如,下面是使用 @Async
注释的合法应用程序:
@Async
void doSomething(String s) {
// 这将异步执行
}
即使方法有返回值,它也可以被异步调用。然而,这样的方法需要有一个 Future
类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用 get()
之前执行其他任务.
@Async
Future<String> returnSomething(int i) {
// 这将异步执行
}
@Async
不能与生命周期回调一起使用,比如 @PostConstruct
。为了异步地初始化Spring beans,您现在必须使用一个单独的初始化Spring bean,它在目标上调用 @Async
注释的方法.
public class SampleBeanImpl implements SampleBean {
@Async
void doSomething() {
// ...
}
}
public class SampleBeanInitializer {
private final SampleBean bean;
public SampleBeanInitializer(SampleBean bean) {
this.bean = bean;
}
@PostConstruct
public void initialize() {
bean.doSomething();
}
}
34.4.4 Executor qualification with @Async
默认情况下,当在一个方法上指定 @Async
时,将使用annotation-driven
元素中指定的那个 executor, 如上所述. 然而,当需要指出在执行给定的方法时,指定使用不同于默认值的特定executor时,可以使用 @Async
注释的 value
属性来指定.
@Async("otherExecutor")
void doSomething(String s) {
// this will be executed asynchronously by "otherExecutor"
}
在这种情况下,”otherExecutor” 可能是Spring容器中的任何 Executor
bean的名称,也可能是与任何 Executor
相关联的限定符的名称,例如,由 <qualifier>
元素或Spring的 @Qualifier
注解指定的.
34.4.5 Exception management with @Async
当一个 @Async
方法有一个 Future
类型的返回值时,这很容易管理在方法执行期间抛出的异常,因为在调用 Future
的结果上的 get
方法时,这个异常会被抛出。然而,返回值是void类型时,异常是未捕获的,不能传输。对于这种情况,可以提供一个 AsyncUncaughtExceptionHandler
处理程序来处理此类异常.
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
默认情况下,一场是被记入日志的. 一个自定义的 AsyncUncaughtExceptionHandler
可以通过 AsyncConfigurer
或者 task:annotation-driven
元素来定义.
34.5 task 的 namespace
从spring3.0开始,有一个用于配置 TaskExecutor
和 TaskScheduler
实例的XML名称空间。它还提供了一种方便的方式来配置与触发器一起调度的任务.
34.5.1 scheduler
元素
下面的元素将会创建一个 ThreadPoolTaskScheduler
的实例,并分配指定大小的线程池.
<task:scheduler id="scheduler" pool-size="10"/>
34.5.2 executor
元素
下面的元素将会创建一个 ThreadPoolTaskExecutor
的实例.
<task:executor id="executor" pool-size="10"/>
同时,pool-size
属性可以接受一个范围值 min-max
, 指定线程池的最小线程数和最大线程数.
<task:executor
id="executorWithCallerRunsPolicy"
pool-size="5-25"
queue-capacity="100"
rejection-policy="CALLER_RUNS"/>
也可以设置 queue-capacity
和 rejection-policy
属性等等.
34.5.3 scheduled-tasks
元素
Spring的 task namespace
的最强大功能是支持在 Spring Application Context
中配置任务。这遵循了一种类似于Spring中的其他 “method-invokers” 的方法,比如JMS名称空间提供的用于配置消息驱动pojo的方法。基本上,一个 “ref” 属性可以指向任何spring管理的对象,而 “method” 属性提供了在该对象上调用的方法的名称。这里有一个简单的例子:
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
正如您所看到的,scheduler
由外部元素引用,并且每个单独的任务包括其触发器元数据的配置。在前面的例子中,元数据定义了一个带有 fixed-delay
的周期性触发器,指示每个任务执行完成后等待的毫秒数。另一种选择是 fixed-rate
,即不管之前的执行多长时间,该方法应该执行多长时间。此外,对于 fixed-delay
和 fixed-rate
的任务,可以指定 initial-delay
参数,指示初始等待的毫秒数.
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
<task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
<task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
34.6 使用 Quartz Scheduler
Quartz使用 Trigger
, Job
和 JobDetail
对象来实现各种作业的调度。对于Quartz背后的基本概念,请查看http://quartz-scheduler.org。出于方便的目的,Spring提供了几个类,可以简化基于Spring的应用程序中Quartz的使用。
34.6.1 使用 JobDetailFactoryBean
Quartz 中的 JobDetail
对象包含运行作业所需的所有信息。Spring提供了一个 JobDetailFactoryBean
,它为XML配置提供了bean风格的属性。让我们看一个例子:
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="example.ExampleJob"/>
<property name="jobDataAsMap">
<map>
<entry key="timeout" value="5"/>
</map>
</property>
</bean>
job detail 配置拥有运行作业(ExampleJob
)所需的所有信息。timeout 是在作业数据映射中指定的。作业数据映射可以通过 JobExecutionContext
(在执行时传递给你)来获得,但是JobDetail
也从映射到作业实例属性的作业数据中获得它的属性。在这种情况下,如果 ExampleJob
包含一个名为 timeout
的bean属性,那么 JobDetail
将自动应用它:
package example;
public class ExampleJob extends QuartzJobBean {
private int timeout;
/**
* Setter called after the ExampleJob is instantiated
* with the value from the JobDetailFactoryBean (5)
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
// do the actual work
}
}
从作业数据映射中获得的所有附加属性当然也可以使用。
34.6.2 使用 MethodInvokingJobDetailFactoryBean
通常,您只需要在特定对象上调用一个方法。使用 MethodInvokingJobDetailFactoryBean
,你可以这样做:
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
</bean>
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
上面的例子将会调用 exampleBusinessObject
类中的 doIt
方法(见下文):
public class ExampleBusinessObject {
// properties and collaborators
public void doIt() {
// do the actual work
}
}
默认情况下,Quartz作业是无状态的,这会导致作业相互干扰。
如果您为相同的 JobDetail
指定两个触发器,那么在第一项工作完成之前,可能会启动第二个触发器。如果 JobDetail
类实现了 Stateful
接口,则不会发生这种情况。第二份工作在第一个工作完成之前不会开始。为了使 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>
默认情况下,作业将以并发的方式运行。
34.6.3 通过使用 triggers
和 SchedulerFactoryBean
来连接 jobs
我们已经创造了 job details
和 jobs
。
Quartz和Spring提供了几个触发器,它们提供了两个 FactoryBean
的实现,并且提供了方便的默认值: CronTriggerFactoryBean
和 SimpleTriggerFactoryBean
.
触发器需要被调度。
Spring提供了一个 SchedulerFactoryBean
,它将触发器暴露为属性。 SchedulerFactoryBean
用这些触发器来调度实际的作业。
下面是例子:
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<!-- 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.CronTriggerFactoryBean">
<property name="jobDetail" ref="exampleJob"/>
<!-- run every morning at 6 AM -->
<property name="cronExpression" value="0 0 6 * * ?"/>
</bean>
现在我们已经设置了两个触发器,一个每隔50秒周期运行,开始延迟10秒,另一个触发器每天早上6点开始。为了完成所有的工作,我们需要设置 SchedulerFactoryBean
:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
<ref bean="simpleTrigger"/>
</list>
</property>
</bean>