executor是线程池概念的JDK名称。之所以命名为“执行者”,是因为无法保证底层实现实际上是一个池。执行程序可以是单线程的,甚至可以是同步的。Spring的抽象隐藏了Java SE和Java EE环境之间的实现细节。
Spring的TaskExecutor接口与java.util.concurrent相同。Executor接口。实际上,最初,它存在的主要原因是在使用线程池时抽象出对Java 5的需求。该接口只有一个方法(execute(可运行任务)),该方法根据线程池的语义和配置接受任务执行。
最初创建TaskExecutor是为了给其他Spring组件在需要时提供线程池的抽象。像ApplicationEventMulticaster、JMS的AbstractMessageListenerContainer和Quartz integration这样的组件都使用TaskExecutor抽象来池线程。然而,如果您的bean需要线程池行为,您也可以使用这个抽象来满足您自己的需要。
7.1.1。TaskExecutor类型
Spring包括许多预构建的TaskExecutor实现。在所有的可能性中,您应该永远不需要实现您自己的。Spring提供的变体如下:
SyncTaskExecutor:此实现不会异步运行调用。相反,每个调用都发生在调用线程中。它主要用于不需要多线程的情况,例如在简单的测试用例中。
SimpleAsyncTaskExecutor:这个实现不重用任何线程。相反,它为每次调用都启动一个新线程。但是,它支持一个并发限制,该限制将阻塞任何超过该限制的调用,直到释放一个插槽。如果您正在寻找真正的池,请参阅本列表后面的ThreadPoolTaskExecutor。
ConcurrentTaskExecutor:这个实现是java.util.concurrent的适配器。Executor实例。有一种替代方法(ThreadPoolTaskExecutor)将Executor配置参数公开为bean属性。很少需要直接使用ConcurrentTaskExecutor。但是,如果ThreadPoolTaskExecutor不够灵活,不能满足您的需要,那么ConcurrentTaskExecutor是一个备选方案。
ThreadPoolTaskExecutor:这个实现最常用。它公开用于配置java.util.concurrent的bean属性。ThreadPoolExecutor,并将其包装在TaskExecutor中。如果您需要适应不同类型的java.util.concurrent。Executor,我们建议您使用ConcurrentTaskExecutor。
WorkManagerTaskExecutor:该实现使用CommonJ WorkManager作为其后台服务提供者,并且是用于在Spring应用程序上下文中在WebLogic或WebSphere上设置基于CommonJ的线程池集成的中心便利类。
DefaultManagedTaskExecutor:该实现在兼容JSR-236的运行时环境(例如Java EE 7+应用服务器)中使用jndi获得的ManagedExecutorService,取代CommonJ WorkManager。
7.1.2. Using a TaskExecutor
Spring的TaskExecutor实现被用作简单的javabean。在下面的例子中,我们定义了一个使用ThreadPoolTaskExecutor异步打印一组消息的bean:
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>
7.2。Spring TaskScheduler抽象
除了TaskExecutor抽象之外,Spring 3.0还引入了一个带有各种方法的任务调度程序,用于调度任务在未来的某个时间点运行。下面的清单显示了TaskScheduler接口的定义:
public interface TaskScheduler {
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Instant startTime);
ScheduledFuture schedule(Runnable task, Date startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}
最简单的方法是名为schedule的方法,它只需要一个可运行文件和一个日期。这会导致任务在指定的时间之后运行一次。所有其他方法都能够调度任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但是接受触发器的方法要灵活得多。
7.2.1. Trigger Interface
触发器接口基本上是受到JSR-236的启发,而JSR-236到Spring 3.0时还没有正式实现。触发器的基本思想是,执行时间可以根据过去的执行结果甚至任意条件确定。如果这些确定确实考虑了前面执行的结果,那么这些信息在TriggerContext中是可用的。触发器接口本身非常简单,如下面的清单所示:
public interface Trigger {
Date nextExecutionTime(TriggerContext triggerContext);
}
TriggerContext是最重要的部分。它封装了所有相关数据,并在必要时对未来的扩展开放。TriggerContext是一个接口(默认情况下使用SimpleTriggerContext实现)。下面的清单显示了触发器实现的可用方法。
public interface TriggerContext {
Date lastScheduledExecutionTime();
Date lastActualExecutionTime();
Date lastCompletionTime();
}
7.2.2. Trigger Implementations
Spring提供了触发器接口的两种实现。最有趣的是克朗触发器。它支持基于cron表达式调度任务。例如,以下任务计划在每小时过后15分钟运行,但只在工作日的9点至5点“工作时间”内运行:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一个实现是接受一个固定周期、一个可选的初始延迟值和一个布尔值的周期性触发器,该布尔值指示周期应该被解释为固定速率还是固定延迟。因为TaskScheduler接口已经定义了以固定速率或固定延迟调度任务的方法,所以应该在可能的情况下直接使用这些方法。PeriodicTrigger实现的价值在于,您可以在依赖触发器抽象的组件中使用它。例如,允许交替使用周期触发器、基于cron的触发器甚至自定义触发器实现可能很方便。这样的组件可以利用依赖注入,这样您就可以在外部配置此类触发器,从而轻松地修改或扩展它们。
7.2.3. TaskScheduler implementations
与Spring的TaskExecutor抽象一样,TaskScheduler安排的主要好处是应用程序的调度需求与部署环境解耦。当部署到应用程序服务器环境时,这个抽象级别特别相关,在这种环境中,应用程序本身不应该直接创建线程。对于这样的场景,Spring提供了一个TimerManagerTaskScheduler,它委托给WebLogic或WebSphere上的CommonJ TimerManager,以及一个更近期的DefaultManagedTaskScheduler,它委托给Java EE 7+环境中的JSR-236 ManagedScheduledExecutorService。两者通常都配置了JNDI查找。
只要不需要外部线程管理,就可以在应用程序中设置本地ScheduledExecutorService,这可以通过Spring的ConcurrentTaskScheduler进行调整。为了方便起见,Spring还提供了一个ThreadPoolTaskScheduler,它在内部委托ScheduledExecutorService,以沿着ThreadPoolTaskExecutor行提供公共bean样式的配置。在宽松的应用程序服务器环境中,这些变体对于本地嵌入式线程池设置也非常有效——特别是在Tomcat和Jetty上。
7.3. Annotation Support for Scheduling and Asynchronous Execution 对调度和异步执行的注释支持
Spring为任务调度和异步方法执行提供了注释支持。
7.3.1. Enable Scheduling Annotations
要启用对@Scheduled和@Async注解的支持,你可以添加@EnableScheduling和@EnableAsync到你的@Configuration类之一,如下面的示例所示:
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
您可以为应用程序选择相关的注解。例如,如果您只需要支持@Scheduled,您可以省略@EnableAsync。对于更细粒度的控制,您可以另外实现SchedulingConfigurer接口、AsyncConfigurer接口或同时实现这两个接口。详细信息请参阅SchedulingConfigurer和AsyncConfigurer javadoc。
如果您喜欢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注释的方法对应的任务,并提供了调度程序引用来管理那些带有@Scheduled注释的方法。
处理@Async注释的默认建议模式是代理模式,它允许仅通过代理拦截调用。同一个类中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到aspectj模式。
7.3.2. @Scheduled 注解
您可以向方法添加@Scheduled注释,以及触发器元数据。例如,下面的方法每5秒调用一次,有一个固定的延迟,这意味着周期是从每次调用的完成时间开始计算的:
@Scheduled(fixedDelay=5000)
public void doSomething() {
// something that should run periodically
}
如果需要固定速率的执行,可以更改注释中指定的属性名。下面的方法每5秒被调用一次(在每次调用的连续开始时间之间测量):
@Scheduled(fixedRate=5000)
public void doSomething() {
// something that should run periodically
}
对于固定延迟和固定速率的任务,你可以通过指定方法第一次执行前等待的毫秒数来指定初始延迟,如下面的fixerate示例所示:
@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
// something that should run periodically
}
如果简单的周期调度不够有表现力,您可以提供一个cron表达式。以下示例只在工作日运行:
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// something that should run on weekdays only
}
还可以使用zone属性指定解析cron表达式的时区。
请注意,要调度的方法必须有void返回,并且不能期望任何参数。如果该方法需要与应用程序上下文中的其他对象交互,那么这些对象通常可以通过依赖项注入提供。
从Spring Framework 4.3开始,@Scheduled方法在任何范围的bean上都受到支持。
确保没有在运行时初始化同一个@Scheduled注释类的多个实例,除非您确实希望调度对每个这样的实例的回调。与此相关的是,确保不要在用@Scheduled注释的bean类上使用@ configured,这些bean类作为常规的Spring bean注册到容器中。否则,您将得到双重初始化(一次通过容器,另一次通过@可配置方面),结果是每个@Scheduled方法被调用两次。
7.3.3. @Async 注解
您可以在方法上提供@Async注释,以便异步地调用该方法。换句话说,调用者在调用后立即返回,而方法的实际执行发生在提交给Spring TaskExecutor的任务中。在最简单的情况下,您可以将注释应用于返回void的方法,如下面的示例所示:
@Async
void doSomething() {
// this will be run asynchronously
}
与用@Scheduled注释注释的方法不同,这些方法可以期待参数,因为它们是由调用者在运行时以“正常”方式调用的,而不是从容器管理的计划任务中调用的。例如,下面的代码是@Async注释的合法应用:
@Async
void doSomething(String s) {
// this will be run asynchronously
}
甚至可以异步调用返回值的方法。但是,这些方法必须具有future类型的返回值。这仍然提供了异步执行的好处,因此调用者可以在将来调用get()之前执行其他任务。下面的例子展示了如何在返回值的方法上使用@Async:
@Async
Future<String> returnSomething(int i) {
// this will be run asynchronously
}
@Async方法不仅可以声明一个常规的java.util.concurrent。未来的返回类型以及Spring的org.springframework.util.concurrent。ListenableFuture或者,从Spring 4.2开始,JDK 8的java.util.concurrent。CompletableFuture,用于与异步任务的更丰富的交互,以及与进一步处理步骤的直接组合。
你不能把@Async和@PostConstruct这样的生命周期回调结合使用。要异步初始化Spring bean,您目前必须使用一个单独的初始化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();
}
}
@Async没有直接的XML对等物,因为这样的方法首先应该设计为异步执行,而不是从外部重新声明为异步。但是,您可以使用Spring AOP与自定义切入点结合,手动设置Spring的AsyncExecutionInterceptor
7.3.4. 使用@Async的Executor限定
默认情况下,当在方法上指定@Async时,所使用的执行器是在启用异步支持时配置的,例如,如果你使用XML或AsyncConfigurer实现,则使用“annotation-driven”元素。但是,当您需要指出在执行给定方法时应该使用默认执行器以外的执行器时,您可以使用@Async注释的value属性。下面的例子展示了如何做到这一点:
@Async("otherExecutor")
void doSomething(String s) {
// this will be run asynchronously by "otherExecutor"
}
在这种情况下,“otherExecutor”可以是Spring容器中任何Executor bean的名称,也可以是与任何Executor关联的限定符的名称(例如,通过<qualifier>元素或Spring的@Qualifier注释指定)。
7.3.5. 使用@Async进行异常管理
当@Async方法有一个Future类型的返回值时,很容易管理在方法执行期间抛出的异常,因为这个异常是在对Future结果调用get时抛出的。但是,对于void返回类型,异常是未捕获的,不能被传输。你可以提供一个AsyncUncaughtExceptionHandler来处理这样的异常。下面的例子展示了如何做到这一点:
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
缺省情况下,只记录异常情况。您可以通过使用AsyncConfigurer或<task:annotation-driven/> XML元素定义一个自定义的AsyncUncaughtExceptionHandler。
7.4.task名称空间
从3.0版开始,Spring包括了一个用于配置TaskExecutor和TaskScheduler实例的XML命名空间。它还提供了一种方便的方式来配置使用触发器调度的任务。
7.4.1。scheduler的元素
以下元素创建了一个具有指定线程池大小的ThreadPoolTaskScheduler实例:
<task:scheduler id="scheduler" pool-size="10"/>
为id属性提供的值用作池中线程名的前缀。scheduler元素相对简单。如果不提供池大小属性,则默认线程池只有一个线程。调度程序没有其他配置选项。
7.4.2 executor 的元素
下面创建一个ThreadPoolTaskExecutor实例:
<task:executor id="executor" pool-size="10"/>
与上一节所示的调度程序一样,为id属性提供的值被用作池中线程名的前缀。就池大小而言,executor元素比scheduler元素支持更多的配置选项。首先,ThreadPoolTaskExecutor的线程池本身更易于配置。执行程序的线程池可以有不同的核心值和最大大小,而不是只有一个大小。如果提供单个值,执行程序将有一个固定大小的线程池(核心和最大大小是相同的)。然而,executor元素的pool-size属性也接受min-max形式的范围。设置最小值为5,最大值为25。
<task:executor
id="executorWithPoolSizeRange"
pool-size="5-25"
queue-capacity="100"/>
在前面的配置中,还提供了一个队列容量值。还应该根据执行程序的队列容量来考虑线程池的配置。有关池大小和队列容量之间关系的完整描述,请参阅有关ThreadPoolExecutor的文档。其主要思想是,当提交一个任务时,执行程序首先尝试使用一个空闲线程,如果当前活动线程的数量小于核心大小。如果已经达到了核心大小,只要任务的容量还没有达到,就会将其添加到队列中。只有这样,当队列的容量已经达到时,执行程序才会创建超出核心大小的新线程。如果也达到了最大大小,则执行程序将拒绝该任务。
默认情况下,队列是不受限制的,但这很少是理想的配置,因为如果在所有池线程都很忙的情况下向队列添加足够的任务,可能会导致OutOfMemoryErrors。此外,如果队列是无限制的,则最大大小根本没有影响。由于执行程序总是在创建超出核心大小的新线程之前尝试队列,因此队列必须具有有限的容量,以便线程池超出核心大小(这就是为什么在使用无界队列时,固定大小的池是唯一合理的情况)。
如上所述,考虑任务被拒绝时的情况。默认情况下,当任务被拒绝时,线程池执行器会抛出一个TaskRejectedException。但是,拒绝策略实际上是可配置的。当使用默认的拒绝策略(即AbortPolicy实现)时,会抛出异常。对于在重载情况下可能跳过某些任务的应用程序,可以配置DiscardPolicy或DiscardOldestPolicy。对于需要在高负载下限制提交任务的应用程序,另一个工作良好的选项是callerrunsppolicy。该策略强制调用submit方法的线程运行任务本身,而不是抛出异常或丢弃任务。其想法是这样的调用者在运行该任务时处于忙碌状态,不能立即提交其他任务。因此,它提供了一种简单的方法来控制传入负载,同时保持线程池和队列的限制。通常,这允许执行程序“追赶”它正在处理的任务,从而释放队列、池或两者上的一些容量。您可以从executor元素上的reject -policy属性的枚举值中选择这些选项中的任何一个。
下面的例子展示了一个executor元素,它有许多属性来指定各种行为:
<task:executor
id="executorWithCallerRunsPolicy"
pool-size="5-25"
queue-capacity="100"
rejection-policy="CALLER_RUNS"/>
最后,keep-alive设置确定线程在停止之前可能保持空闲的时间限制(以秒为单位)。如果池中当前的线程数超过核心线程数,则在等待此时间后不处理任务,将停止多余的线程。时间值为0将导致在执行任务后立即停止多余的线程,而任务队列中没有剩余的后续工作。设置keep-alive值为2分钟的示例如下:
<task:executor
id="executorWithKeepAlive"
pool-size="5-25"
keep-alive="120"/>
7.4.3. The 'scheduled-tasks' Element
Spring的任务名称空间最强大的特性是支持在Spring应用程序上下文中配置要调度的任务。这采用了与Spring中的其他“方法调用程序”类似的方法,例如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"/>
调度程序由外部元素引用,每个单独的任务都包含其触发器元数据的配置。在前面的示例中,该元数据定义了一个周期性触发器,该触发器具有固定的延迟,该延迟指示每个任务执行完成后需要等待的毫秒数。另一个选项是固定速率,表示方法应该多长时间运行一次,而不管之前的执行需要多长时间。此外,对于固定延迟和固定速率的任务,您都可以指定一个“initial-delay”参数,指示在第一次执行方法之前需要等待的毫秒数。为了获得更多的控制,您可以提供一个cron属性来提供一个cron表达式。下面的例子展示了这些其他选项:
<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"/>
7.5.Cron表达式
所有Spring cron表达式都必须遵循相同的格式,无论您是在@Scheduled annotations、task:scheduled-tasks元素中还是在其他地方使用它们。格式良好的cron表达式,如* * * * * ,由六个以空格分隔的时间和日期字段组成,每个字段都有自己的有效值范围:
┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *
这里有一些适用的规则:
字段可以是星号(),它总是代表“first-last”。对于月日或周日字段,可以使用问号(?)来代替星号。
逗号(,)用于分隔列表中的项。
用连字符(-)分隔的两个数字表示一个数字范围。指定的范围包含在内。
在一个范围(或*)后面加上/指定数字的值在这个范围内的间隔。
英文名称还可以用于月日和周日字段。使用特定日期或月份的前三个字母(大小写无关紧要)。
day-of-month和day-of-week字段可以包含一个L字符,它具有不同的含义
在month -of-month字段中,L代表一个月的最后一天。如果后面跟着一个负偏移量(即L-n),则表示从一个月的第n天到最后一天。
在day-of-week字段中,L代表一周的最后一天。如果前缀为数字或3个字母的名字(dL或DDDL),表示一个月中的最后一天(d或DDD)。
month -of-month field可以是nW,这表示与月n日最近的工作日。如果n在周六,这将产生它之前的周五。如果n在周日下降,这将产生之后的周一,如果n是1并且在周六(即:1W代表一个月的第一个工作日)也会发生。
如果day-of-month字段为LW,则表示每月的最后一个工作日。
“day-of-week”字段可以设置为“d#n”(或“ddd# n”),表示一个月内第d周(或DDD)的第n天。
Cron Expression | Meaning |
---|---|
0 0 * * * * | top of every hour of every day |
*/10 * * * * * | every ten seconds |
0 0 8-10 * * * | 8, 9 and 10 o’clock of every day |
0 0 6,19 * * * | 6:00 AM and 7:00 PM every day |
0 0/30 8-10 * * * | 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day |
0 0 9-17 * * MON-FRI | on the hour nine-to-five weekdays |
0 0 0 25 DEC ? | every Christmas Day at midnight |
0 0 0 L * * | last day of the month at midnight |
0 0 0 L-3 * * | third-to-last day of the month at midnight |
0 0 0 * * 5L | last Friday of the month at midnight |
0 0 0 * * THUL | last Thursday of the month at midnight |
0 0 0 1W * * | first weekday of the month at midnight |
0 0 0 LW * * | last weekday of the month at midnight |
0 0 0 ? * 5#2 | the second Friday in the month at midnight |
0 0 0 ? * MON#1 | the first Monday in the month at midnight |