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

34. 任务执行和调度(Task Execution and Scheduling)

34.1 简介

Spring框架为 asynchronous execution 和 scheduling of tasks 提供了两个接口,分别是 TaskExecutorTaskScheduler. Spring也提供了一些接口来支持 TimerQuartz Scheduler.

34.2 TaskExecutor 接口

Spring提供的 TaskExecutor 接口等同于 java.util.concurrent.Executor 接口. 这个接口只有一个唯一的方法 execute(Runnable task), 这个方法接受一个任务去执行.

34.2.1 TaskExecutor 类型

Spring中包含了很多 TaskExecutor 的具体实现类,很多情况下不需要自己去实现新的类.

  • SimpleAsyncTaskExecutor 这个实现不会重用任何线程,而是为每次调用启动一个新线程。但是,它确实支持一个并发量的限制,超过限制时它将阻塞任何调用,直到一个槽被释放。如果您正在寻找真正的池,请参阅下面的 SimpleThreadPoolTaskExecutorThreadPoolTaskExecutor 的讨论。

  • 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 = 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-delayfixed-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开始,有一个用于配置 TaskExecutorTaskScheduler 实例的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-capacityrejection-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-delayfixed-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, JobJobDetail对象来实现各种作业的调度。对于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 通过使用 triggersSchedulerFactoryBean 来连接 jobs

我们已经创造了 job detailsjobs
Quartz和Spring提供了几个触发器,它们提供了两个 FactoryBean 的实现,并且提供了方便的默认值: CronTriggerFactoryBeanSimpleTriggerFactoryBean.

触发器需要被调度。
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>

猜你喜欢

转载自blog.csdn.net/hbtj_1216/article/details/80185812