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
配置参数作为bean属性公开。很少需要使用Executor
,但是如果ConcurrentTaskExecutor
不够灵活地满足您的需要,ThreadPoolTaskExecutor
是另一种选择。ConcurrentTaskExecutor
-
这个实现实际上是Quartz的SimpleThreadPoolTaskExecutor
的子类,它监听Spring的生命周期回调。当您有一个线程池,可能需要由Quartz和non-Quartz组件共享时,通常会使用这种方法。SimpleThreadPool
-
这个实现是最常用的实现。它公开bean属性来配置ThreadPoolTaskExecutor
并将其包装在java.util.concurrent.ThreadPoolExecutor
中。如果您需要适应不同类型的TaskExecutor
,建议您使用java.util.concurrent.Executor
.ConcurrentTaskExecutor
-
该实现使用 CommonJWorkManagerTaskExecutor
作为其后备实现,并且是在Spring上下文中设置CommonJWorkManager
引用的中心便利类。与WorkManager
类似,该类实现SimpleThreadPoolTaskExecutor
接口,因此也可以直接作为WorkManager
使用。WorkManager
34.2.2 使用 TaskExecutor
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 = ; i < ; 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
@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
可以在一个方法上提供
@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
元素
scheduler
下面的元素将会创建一个
ThreadPoolTaskScheduler
的实例,并分配指定大小的线程池.
<task:scheduler id="scheduler" pool-size="10"/>
34.5.2 executor
元素
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
元素
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 Scheduler
Quartz使用
Trigger
,
Job
和
JobDetail
对象来实现各种作业的调度。对于Quartz背后的基本概念,请查看http://quartz-scheduler.org。出于方便的目的,Spring提供了几个类,可以简化基于Spring的应用程序中Quartz的使用。
34.6.1 使用 JobDetailFactoryBean
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
通常,您只需要在特定对象上调用一个方法。使用
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
triggers
SchedulerFactoryBean
我们已经创造了
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>