赞
踩
Spring框架分别通过TaskExecutor和TaskScheduler接口为任务的异步执行和调度提供了抽象。Spring还提供了支持应用程序服务器环境中的线程池或CommonJ委托的那些接口的实现。最终,在公共接口后面使用这些实现,消除了JavaSE5、JavaSE6和JakartaEE环境之间的差异。
Spring还具有集成类,以支持Timer(自1.3以来JDK的一部分)和Quartz Scheduler的调度。您可以分别使用FactoryBean和可选的Timer或Trigger实例引用来设置这两个调度器。此外,Quartz Scheduler和Timer都有一个方便类,它允许您调用现有目标对象的方法(类似于普通的MethodInvokingFactoryBean操作)。
本文将着重介绍TaskScheduler接口、TaskExecutor接口以及Spring中定时任务的正确使用。
ThreadPoolExecutor
和ThreadPoolTaskExecutor
很多人容易把这两个搞混。
我们实际开发中更多的是使用SpringBoot来开发,Spring默认自带一个线程池方便我们开发,它就是ThreadPoolTaskExecutor,ThreadPoolTaskExecutor是对ThreadPoolExecutor进行了封装处理。
ThreadPoolExecutor
这个类是JDK中的线程池类,继承自Executor。 Executor 顾名思义是专门用来处理多线程相关的一个接口,所有线程相关的类都实现了这个接口,里面有一个execute()方法,用来执行线程。ExecutorService为线程池接口,提供了线程池生命周期方法,继承自Executor接口,ThreadPoolExecutor为线程池实现类,提供了线程池的维护操作等相关方法,继承自AbstractExecutorService,AbstractExecutorService实现了ExecutorService接口。
线程池主要提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销,提高了响应的速度。
ThreadPoolExecutor
ThreadPoolTaskExecutor
则是spring包下的,是sring为我们提供的线程池类,对ThreadPoolExecutor
进行封装,消除了JavaSE5、JavaSE6和JakartaEE环境之间的差异。
执行器是线程池概念的JDK名称。executor
命名是因为无法保证底层实现实际上是一个池。执行器可以是单线程的,甚至可以是同步的。Spring的抽象隐藏了JavaSE和JakartaEE环境之间的实现细节。
**Spring的TaskExecutor接口与java.util.concurrent.Executor接口相同。事实上,最初,它存在的主要原因是在使用线程池时不需要Java5。**该接口有一个方法execute(Runnable task)
,该方法根据线程池的语义和配置接受要执行的任务。
创建TaskExecutor最初是为了在需要时为其他Spring组件提供线程池抽象。ApplicationEventMulticaster、JMS的AbstractMessageListenerContainer和Quartz集成等组件都使用TaskExecutor抽象来池线程。然而,如果您的bean需要线程池行为,您也可以根据自己的需要使用此抽象。
Spring包括许多预先构建的TaskExecutor实现。很可能,你永远不需要实现你自己的。Spring提供的变体如下:
在下面的示例中,我们定义了一个bean,它使用ThreadPoolTaskExecutor异步打印一组消息。
首先,在配置类中注入ThreadPoolTaskExecutor的bean实例。
@Configuration public class ThreadPoolConfig { @Bean private ThreadPoolTaskExecutor execThreadPoolTaskExecutor() { ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor(); pool.setCorePoolSize(10); // 核心线程数 pool.setMaxPoolSize(50); // 最大线程数 pool.setQueueCapacity(500); // 等待队列size pool.setKeepAliveSeconds(60); // 线程最大空闲存活时间 pool.setWaitForTasksToCompleteOnShutdown(true); pool.setAwaitTerminationSeconds(60); // 程序shutdown时最多等60秒钟让现存任务结束 pool.setRejectedExecutionHandler(new CallerRunsPolicy()); // 拒绝策略 return pool; } }
然后,在业务逻辑类中引用ThreadPoolTaskExecutor的bean示例处理业务逻辑。
import org.springframework.core.task.TaskExecutor;
@Service
public class TaskExecutorService {
@Resource(name = "execThreadPoolTaskExecutor")
private ThreadPoolTaskExecutor execThreadPoolTaskExecutor;
public void execLogic() {
for(int i = 0; i < 25; i++) {
execThreadPoolTaskExecutor.execute(() -> System.ount.println("exec logic" + i));
}
}
}
在JDK线程池中自带的Executor遵循一种典型的生产者,消费者队列模型,即一个统一的阻塞队列,然后一个线程数组不停地消费其中的数据。其本身的处理逻辑为 coreSize->queueSize->maxSize 的增长方式,即先尝试增加 coreSize, 然后再不断地将任务放进队列中,如果队列满了,则再尝试增加 maxSize, 直至拒绝任务。
通过一些手法可以调整策略为 coreSize->maxSize->queueSize。
此次使用 jboss-threads 中的 EnhancedQueueExecutor,中文为增加型队列执行器。其除支持典型的executor模型外,也同样保留如 coreSize,maxSize, queueSize 这些模型。与jdk中实现相区别的是,其本身采用单个链表来完成任务的提交和线程的执行,同时采用额外的数据来存储计数类数据. 更重要的是,其默认线程策略即 coreSize->maxSize->queueSize, 同时可以根据参数调整此策略。
创建对象与ThreadPoolExecutor类似,指定相应的参数即可,如下所示:
@Configuration @Slf4j public class EnhancedQueueExecutorConfig { Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (t, e) -> log.error("任务失败: {}", e.getMessage(), e); var threadFactory = new ThreadFactoryBuilder().setNameFormat("myExecutor" + "-%d") .setUncaughtExceptionHandler(uncaughtExceptionHandler) .build(); EnhancedQueueExecutor executor = new EnhancedQueueExecutor.Builder() .setCorePoolSize(corePoolSize) .setMaximumPoolSize(maxPoolSize) .setKeepAliveTime(Duration.ofMinutes(5)) .setMaximumQueueSize(1024) .setThreadFactory(threadFactory) .setExceptionHandler(uncaughtExceptionHandler) .setRegisterMBean(false) .setGrowthResistance(growthResistance) //增长因子,控制新线程创建逻辑(if >= coreSize时) .build(); }
除了TaskExecutor抽象之外,Spring还有一个TaskScheduler API,它具有多种方法来调度将来某个时刻运行的任务。
TaskScheduler接口定义:
public interface TaskScheduler { /** * 提交任务调度请求 * @param task 待执行任务 * @param trigger 使用Trigger指定任务调度规则 * @return */ @Nullable ScheduledFuture<?> schedule(Runnable task, Trigger trigger); /** * 提交任务调度请求 * 注意任务只执行一次,使用startTime指定其启动时间 * @param task 待执行任务 * @param startTime 任务启动时间 * @return */ ScheduledFuture<?> schedule(Runnable task, Instant startTime); /** * 使用fixedRate的方式提交任务调度请求 * 任务首次启动时间由传入参数指定 * @param task 待执行的任务 * @param startTime 任务启动时间 * @param period 两次任务启动时间之间的间隔时间,默认单位是毫秒 * @return */ ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period); /** * 使用fixedRate的方式提交任务调度请求 * 任务首次启动时间未设置,任务池将会尽可能早的启动任务 * @param task 待执行任务 * @param period 两次任务启动时间之间的间隔时间,默认单位是毫秒 * @return */ ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period); /** * 使用fixedDelay的方式提交任务调度请求 * 任务首次启动时间由传入参数指定 * @param task 待执行任务 * @param startTime 任务启动时间 * @param delay 上一次任务结束时间与下一次任务开始时间的间隔时间,单位默认是毫秒 * @return */ ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay); /** * 使用fixedDelay的方式提交任务调度请求 * 任务首次启动时间未设置,任务池将会尽可能早的启动任务 * @param task 待执行任务 * @param delay 上一次任务结束时间与下一次任务开始时间的间隔时间,单位默认是毫秒 * @return */ ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay); }
最简单的方法是一个名为schedule的方法,它只需要一个Runnable和一个Instant。这会导致任务在指定时间后运行一次。所有其他方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但接受触发器的方法要灵活得多。
Trigger接口本质上受到JSR-236的启发。触发器的基本思想是,可以根据过去的执行结果甚至任意条件来确定执行时间。如果这些确定考虑了先前执行的结果,则该信息在TriggerContext中可用。Trigger接口用于计算任务的下次执行时间。
Trigger接口本身非常简单,如下表所示:
public interface Trigger {
Instant nextExecution(TriggerContext triggerContext);
}
TriggerContext是最重要的部分。它封装了所有相关数据,如果需要,将来可以进行扩展。TriggerContext是一个接口(默认使用SimpleTriggerContext实现)。下面的列表显示了Trigger实现的可用方法。
public interface TriggerContext {
Clock getClock();
Instant lastScheduledExecution();
Instant lastActualExecution();
Instant lastCompletion();
}
Spring提供了Trigger接口的两种实现CronTrigger
和PeriodicTrigger
。
最有趣的是CronTrigger。它支持基于cron表达式的任务调度。通过Cron表达式来生成调度计划。
例如,以下任务计划在每小时15分钟后运行,但仅在工作日的朝九晚五“工作时间”内运行:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
用于定期执行的Trigger;它有两种模式:
可见这两种情况的区别就在于,在决定下一次的执行计划时是否要考虑上次任务在什么时间执行完成。 默认情况下PeriodicTrigger使用了fixedDelay模式。
PeriodicTrigger提供以下参数来达成目的:
PeriodicTrigger
接受一个固定的周期、一个可选的初始延迟值和一个布尔值,以指示该周期应该被解释为固定速率还是固定延迟。由于TaskScheduler接口已经定义了以固定速率或固定延迟调度任务的方法,因此应尽可能直接使用这些方法。PeriodicTrigger实现的价值在于,您可以在依赖Trigger抽象的组件中使用它。例如,允许交替使用周期性触发器、基于cron的触发器,甚至自定义触发器实现可能很方便。这样的组件可以利用依赖注入,这样您就可以在外部配置这样的触发器,从而轻松地修改或扩展它们。
用于包装CommonJ中的TimerManager接口。在使用CommonJ进行调度时使用。
包装Java Concurrent中的ScheduledThreadPoolExecutor类,大多数场景下都使用它来进行任务调度。 除实现了TaskScheduler接口中的方法外,它还包含了一些对ScheduledThreadPoolExecutor进行操作的接口,其常用方法如下:
与Spring的TaskExecutor抽象一样,TaskScheduler抽象的主要好处是应用程序的调度需求与部署环境分离。当部署到应用程序服务器环境时,这个抽象级别尤其重要,因为应用程序本身不应该直接创建线程。对于这样的场景,Spring提供了一个TimerManagerTaskScheduler,它委托给WebLogic或WebSphere上的CommonJ TimerManager,以及一个更新的DefaultManagedTaskScheduler,在Jakarta EE环境中委托给JSR-236 ManagedScheduledExecutorService。两者通常都配置有JNDI查找。
每当不需要外部线程管理时,一个更简单的替代方案就是在应用程序中设置本地ScheduledExecutorService,它可以通过Spring的ConcurrentTaskScheduler进行调整。为了方便起见,Spring还提供了ThreadPoolTaskScheduler,它在内部委托给ScheduledExecutorService,以提供与ThreadPoolTaskExecutor类似的通用bean样式配置。这些变体对于宽松的应用程序服务器环境中的本地嵌入式线程池设置也非常适用 — 特别是在Tomcat和Jetty上。
Spring 为任务调度和异步方法提供了注释支持 执行。
必须要使用@EnableScheduling注解来启用对@Scheduled注解的支持,@EnableScheduling必须使用在工程中某一个被Configuration注解的类上,当然程序的主启启动类上也可以,因为程序主启动类底层也是含有Configuration注解。
如下例所示:
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
您可以选择应用程序的相关注释。例如,如果只需要对@Scheduled的支持,则可以省略@EnableAsync。对于更细粒度的控制,可以另外实现SchedulingConfigurer接口、AsyncConfigurer接口或两者。有关详细信息,请参阅SchedulingConfigurer和AsyncConfigurer javadoc。
Scheduled注解用在方法上,用于表示这个方法将会被调度。不同于Async注解,它所注解的方法返回类型最好是void类型的,否则它的返回值将不会被TaskScheduler所使用。同时,被它注解的方法不能有参数。如果要使用其它的对象的值,需要通过依赖注入的方式引用。
它包含有以下属性:
例如,以下方法每五秒(5000毫秒)调用一次,具有固定的延迟,这意味着该时间段是从每次前一次调用的完成时间开始计算的。
// 每5秒执行一次。时间段是从每次前一次调用的完成时间开始计算
@Scheduled(fixedDelay = 5000)
public void doSomething() {
// something that should run periodically
}
默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的时间单位。如果您想使用不同的时间单位,例如秒或分钟,可以通过@Scheduled中的timeUnit属性进行配置。
例如,前面的示例也可以编写如下。
// 每5秒执行一次。时间段是从每次前一次调用的完成时间开始计算
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// something that should run periodically
}
如果需要固定速率执行,可以在注释中使用fixedRate属性。以下方法每五秒调用一次(在每次调用的连续开始时间之间测量)。
// 固定速率每5秒执行一次。时间段是从每次前一次调用的开始时间开始计算,即以固定速率执行任务,不关注上次执行完成时间
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// something that should run periodically
}
对于固定延迟和固定速率的任务,可以通过指示在第一次执行方法之前等待的时间量来指定初始延迟,如下面的fixedRate示例所示。
// 第一次执行延时1秒,然后以固定速率每5秒执行一次
@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
}
从Spring Framework 4.3开始,任何范围的bean都支持@Scheduled方法。
确保您在运行时没有初始化同一@Scheduled注释类的多个实例,除非您确实希望调度对每个此类实例的回调。与此相关的是,请确保不要在用@Scheduled注释并在容器中注册为常规Spring Bean的类上使用@Configurationable。否则,您将获得两次初始化(一次通过容器,一次通过@Configurationable注解),结果是每个@Scheduled方法被调用两次。
您可以在方法上提供@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类型的返回值。这仍然提供了异步执行的好处,因此调用者可以在调用Future上的get()之前执行其他任务。以下示例显示如何在返回值的方法上使用@Async:
@Async
Future<String> returnSomething(int i) {
// this will be run asynchronously
}
@异步方法不仅可以声明常规java.util.concurrent.Future返回类型,还可以声明Spring的org.springframework.util.concurrent.ListenableFuture,或者从Spring 4.2开始,JDK 8的java.util.coccurrent.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或AsyncConfigurer实现(如果有),则为“注释驱动”元素。但是,当需要指示在执行给定方法时应使用默认值以外的执行器时,可以使用@Async注释的value属性。以下示例显示了如何执行此操作:
@Async("otherExecutor")
void doSomething(String s) {
// this will be run asynchronously by "otherExecutor"
}
在这种情况下,“otherExecutor”可以是Spring容器中任何Executor Bean的名称,也可以是与任何Executoor关联的限定符的名称(例如,使用元素或Spring的@Qualifier注释指定)。
当@Async方法具有Future类型的返回值时,很容易管理在方法执行期间引发的异常,因为在Future结果上调用get时会引发此异常。然而,对于void返回类型,异常是未捕获的,无法传输。您可以提供AsyncUnaughtExceptionHandler来处理此类异常。以下示例显示了如何执行此操作:
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
所有 Spring cron 表达式都必须符合相同的格式,无论您是在@Scheduled注释、任务、计划任务元素、 或其他地方。 格式正确的 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”。对于月日或星期日字段,可以使用问号(?)代替星号。
逗号(,)用于分隔列表中的项目。
用连字符(-)分隔的两个数字表示一系列数字。指定的范围包含在内。
在带/的范围(或*)之后指定数字值在该范围内的间隔。
英文名称也可以用于月份和星期几字段。使用特定日期或月份的前三个字母(大小写无关紧要)。
“月日”和“星期日”字段可以包含L字符,其含义不同。
在月日字段中,L代表该月的最后一天。如果后面跟着一个负偏移量(即L-n),则表示该月的第n天到最后一天。
在星期几字段中,L代表一周的最后一天。如果前缀为数字或三个字母的名称(dL或DDDL),则表示当月的最后一天(d或DDD)。
“月日”字段可以是nW,它代表一个月中最近的一个工作日。如果n落在星期六,这将产生前一个星期五。如果n在星期天,这将生成后一个星期一,如果n为1并且落在星期天(即:1W代表一个月中的第一个工作日),也会发生这种情况。
如果月日字段为LW,则表示该月的最后一个工作日。
星期几字段可以是d#n(或DDD#n),表示一个月中第n个星期d(或DDD)。
问号(?)只能用在DayofMonth和DayofWeek两个域,由于指定日期(DayofMonth)和指定星期(DayofWeek)存在冲突,所以当指定了日期(DayofMonth)后(包括每天*),星期(DayofWeek)必须使用问号(?),同理,指定星期(DayofWeek)后,日期(DayofMonth)必须使用问号(?)。
以下是一些示例:
Cron 表达式 | 意义 |
---|---|
0 0 * * * * | 每天每个小时之间 |
*/10 * * * * * | 每十秒 |
0 0 8-10 * * * | 每天8点、9点及10点 |
0 0 6,19 * * * | 每天上午 6:00 和晚上 7:00 |
0 0/30 8-10 * * * | 每天 8:00、8:30、9:00、9:30、10:00 和 10:30 |
0 0 9-17 * * MON-FRI | 工作日朝九晚五的整点 |
0 0 0 25 DEC ? | 每个圣诞节午夜 |
0 0 0 L * * | 每月最后一天午夜 |
0 0 0 L-3 * * | 每月倒数第三天的午夜 |
0 0 0 * * 5L | 每月最后一个星期五午夜 |
0 0 0 * * THUL | 每月最后一个星期四午夜 |
0 0 0 1W * * | 每月第一个工作日的午夜 |
0 0 0 LW * * | 每月最后一个工作日的午夜 |
0 0 0 ? * 5#2 | 每月第二个星期五午夜 |
0 0 0 ? * MON#1 | 每月第一个星期一午夜 |
对于人类来说,诸如0 0***之类的表达式很难解析,因此在出现错误时很难修复。为了提高可读性,Spring支持以下宏,这些宏表示常用的序列。您可以使用这些宏而不是六位数的值,例如:@Scheduled(cron=“@hourly”)。
宏 | 意义 |
---|---|
@yearly(或@annually) | 每年一次(0 0 0 1 1 *) |
@monthly | 每月一次(0 0 0 1 * *) |
@weekly | 每周一次(0 0 0 * * 0) |
@daily(或@midnight) | 每天一次 (),或0 0 0 * * * |
@hourly | 每小时一次,(0 0 * * * *) |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。