天天看点

【Spring4.3.16】任务执行和调度(Task Execution and Scheduling)34. 任务执行和调度(Task Execution and Scheduling)

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

    该实现使用 CommonJ

    WorkManager

    作为其后备实现,并且是在Spring上下文中设置CommonJ

    WorkManager

    引用的中心便利类。与

    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 = ; 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

注释可以连同

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>
           

继续阅读