Spring5源码解析-Spring中的异步和计划任务
Java提供了许多创建线程池的方式,并得到一个Future实例来作为任务结果。对于Spring同样小菜一碟,通过其scheduling
包就可以做到将任务线程中后台执行。
在本文的第一部分中,我们将讨论下Spring中执行计划任务的一些基础知识。之后,我们将解释这些类是如何一起协作来启动并执行计划任务的。下一部分将介绍计划和异步任务的配置。最后,我们来写个Demo,看看如何通过单元测试来编排计划任务。
什么是Spring中的异步任务?
在我们正式的进入话题之前,对于Spring,我们需要理解下它实现的两个不同的概念:异步任务和调度任务。显然,两者有一个很大的共同点:都在后台工作。但是,它们之间存在了很大差异。调度任务与异步不同,其作用与Linux中的CRON job
完全相同(windows里面也有计划任务)。举个栗子,有一个任务必须每40分钟执行一次,那么,可以通过XML文件或者注解来进行此配置。简单的异步任务在后台执行就好,无需配置执行频率。
因为它们是两种不同的任务类型,它们两个的执行者自然也就不同。第一个看起来有点像Java的并发执行器(concurrency executor
),这里会有专门去写一篇关于Java中的执行器来具体了解。根据Spring文档TaskExecutor所述,它提供了基于Spring的抽象来处理线程池,这点,也可以通过其类的注释去了解。另一个抽象接口是TaskScheduler,它用于在将来给定的时间点来安排任务,并执行一次或定期执行。
在分析源码的过程中,发现另一个比较有趣的点是触发器。它存在两种类型:CronTrigger或PeriodTrigger。第一个模拟CRON任务的行为。所以我们可以在将来确切时间点提交一个任务的执行。另一个触发器可用于定期执行任务。
Spring的异步任务类
让我们从org.springframework.core.task.TaskExecutor类的分析开始。你会发现,其简单的不行,它是一个扩展Java的Executor接口的接口。它的唯一方法也就是是执行,在参数中使用Runnable类型的任务。
1 | package org.springframework.core.task; |
相对来说,org.springframework.scheduling.TaskScheduler接口就有点复杂了。它定义了一组以schedule开头的名称的方法允许我们定义将来要执行的任务。所有 schedule* 方法返回java.util.concurrent.ScheduledFuture实例。Spring5中对scheduleAtFixedRate
方法做了进一步的充实,其实最终调用的还是ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period);
1 | public interface TaskScheduler { |
之前提到两个触发器组件,都实现了org.springframework.scheduling.Trigger接口。这里,我们只需关注一个的方法nextExecutionTime ,其定义下一个触发任务的执行时间。它的两个实现,CronTrigger和PeriodicTrigger,由org.springframework.scheduling.TriggerContext来实现信息的存储,由此,我们可以很轻松获得一个任务的最后一个执行时间(lastScheduledExecutionTime),给定任务的最后完成时间(lastCompletionTime)或最后一个实际执行时间(lastActualExecutionTime)。接下来,我们通过阅读源代码来简单的了解下这些东西。org.springframework.scheduling.concurrent.ConcurrentTaskScheduler包含一个私有类EnterpriseConcurrentTriggerScheduler
。在这个class
里面,我们可以找到schedule方法:
1 | public ScheduledFuture<?> schedule(Runnable task, final Trigger trigger) { |
SimpleTriggerContext
从其名字就可以看到很多东西了,因为它实现了TriggerContext
接口。
1 | /** |
也正如你看到的,在构造函数中设置的时间值来自javax.enterprise.concurrent.LastExecution的实现,其中:
- getScheduledStart:返回上次开始执行任务的时间。它对应于TriggerContext的lastScheduledExecutionTime属性。
- getRunStart:返回给定任务开始运行的时间。在TriggerContext中,它对应于lastActualExecutionTime。
- getRunEnd:任务终止时返回。它用于在TriggerContext中设置lastCompletionTime。
Spring调度和异步执行中的另一个重要类是org.springframework.core.task.support.TaskExecutorAdapter。它是一个将java.util.concurrent.Executor作为Spring基本的执行器的适配器(描述的有点拗口,看下面代码就明了了),之前已经描述了TaskExecutor
。实际上,它引用了Java的ExecutorService,它也是继承了Executor
接口。此引用用于完成所有提交的任务。
1 | /** |
在Spring中配置异步和计划任务
下面我们通过代码的方式来实现异步任务。首先,我们需要通过注解来启用配置。它的XML配置如下:
1 | <task:scheduler id="taskScheduler"/> |
可以通过将@EnableScheduling
和@EnableAsync
注解添加到配置类(用@Configuration注解)来激活两者。完事,我们就可以开始着手实现调度和异步任务。为了实现调度任务,我们可以使用@Scheduled
注解。我们可以从org.springframework.scheduling.annotation包中找到这个注解。它包含了以下几个属性:
cron
:使用CRON
风格(Linux配置定时任务的风格)的配置来配置需要启动的带注解的任务。zone
:要解析CRON
表达式的时区。fixedDelay
或fixedDelayString
:在固定延迟时间后执行任务。即任务将在最后一次调用结束和下一次调用的开始之间的这个固定时间段后执行。fixedRate
或fixedRateString
:使用fixedRate
注解的方法的调用将以固定的时间段(例如:每10秒钟)进行,与执行生命周期(开始,结束)无关。initialDelay
或initialDelayString
:延迟首次执行调度方法的时间。请注意,所有值(fixedDelay ,fixedRate ,initialDelay )必须以毫秒表示。 需要特别记住的是 ,用@Scheduled注解的方法不能接受任何参数,并且不返回任何内容(void),如果有返回值,返回值也会被忽略掉的,没什么卵用。定时任务方法由容器管理,而不是由调用者在运行时调用。它们由 org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor来解析,其中包含以下方法来拒绝执行所有不正确定义的函数:1
2
3
4
5
6
7
8
9
10protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
Assert.isTrue(method.getParameterCount() == 0,
"Only no-arg methods may be annotated with @Scheduled");
/**
* 之前的版本中直接把返回值非空的给拒掉了,在Spring 4.3 Spring5 的版本中就没那么严格了
* Assert.isTrue(void.class.equals(method.getReturnType()),
* "Only void-returning methods may be annotated with @Scheduled");
**/
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33/**
* 注释很重要
* An annotation that marks a method to be scheduled. Exactly one of
* the {@link #cron()}, {@link #fixedDelay()}, or {@link #fixedRate()}
* attributes must be specified.
*
* <p>The annotated method must expect no arguments. It will typically have
* a {@code void} return type; if not, the returned value will be ignored
* when called through the scheduler.
*
* <p>Processing of {@code @Scheduled} annotations is performed by
* registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be
* done manually or, more conveniently, through the {@code <task:annotation-driven/>}
* element or @{@link EnableScheduling} annotation.
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em> with attribute overrides.
*
* @author Mark Fisher
* @author Dave Syer
* @author Chris Beams
* @since 3.0
* @see EnableScheduling
* @see ScheduledAnnotationBeanPostProcessor
* @see Schedules
*/
({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
(RetentionPolicy.RUNTIME)
(Schedules.class)
public Scheduled {
...
}
使用@Async
注解标记一个方法或一个类(通过标记一个类,我们自动将其所有方法标记为异步)。与@Scheduled
不同,异步任务可以接受参数,并可能返回某些东西。
写一个在Spring中执行异步任务的Demo
有了上面这些知识,我们可以来编写异步和计划任务。我们将通过测试用例来展示。我们从不同的任务执行器(task executors)的测试开始:
1 | (SpringJUnit4ClassRunner.class) |
在过去,我们可以有更多的执行器可以使用(SimpleThreadPoolTaskExecutor,TimerTaskExecutor 这些都2.x 3.x的老古董了)。但都被弃用并由本地Java的执行器取代成为Spring的首选。看看输出的结果:
1 | Running task 'SimpleAsyncTask-1' in Thread thread_name_prefix_____1 |
以此我们可以推断出,第一个测试为每个任务创建新的线程。通过使用不同的线程名称,我们可以看到相应区别。第二个,同步执行器,应该执行所调用线程中的任务。这里可以看到’main’是主线程的名称,它的主线程调用执行同步所有任务。最后一种例子涉及最大可创建3个线程的线程池。从结果可以看到,他们也确实只有3个创建线程。
现在,我们将编写一些单元测试来看看@Async和@Scheduled实现。
1 | (SpringJUnit4ClassRunner.class) |
另外,我们需要创建新的配置文件和一个包含定时任务方法的类:
1 | <!-- imported configuration file first --> |
1 | // scheduled methods after, all are executed every 6 seconds (scheduledAtFixedRate and scheduledAtFixedDelay start to execute at |
该测试的输出:
1 | <R> Increment at fixed rate |
本文向我们介绍了关于Spring框架另一个大家比较感兴趣的功能–定时任务。我们可以看到,与Linux CRON风格配置类似,这些任务同样可以按照固定的频率进行定时任务的设置。我们还通过例子证明了使用@Async注解的方法会在不同线程中执行。