凌河锦州网站建设,专业建设情况,湖南网站建设磐石网络最好,企业备案网站可以做论坛吗【SpringBoot】 定时任务之任务执行和调度及使用指南 Spring框架分别通过TaskExecutor和TaskScheduler接口为任务的异步执行和调度提供了抽象。Spring还提供了支持应用程序服务器环境中的线程池或CommonJ委托的那些接口的实现。最终#xff0c;在公共接口后面使用这些实现在公共接口后面使用这些实现消除了JavaSE5、JavaSE6和JakartaEE环境之间的差异。 Spring还具有集成类以支持Timer自1.3以来JDK的一部分和Quartz Scheduler的调度。您可以分别使用FactoryBean和可选的Timer或Trigger实例引用来设置这两个调度器。此外Quartz Scheduler和Timer都有一个方便类它允许您调用现有目标对象的方法类似于普通的MethodInvokingFactoryBean操作。 本文将着重介绍TaskScheduler接口、TaskExecutor接口以及Spring中定时任务的正确使用。 一、ThreadPoolExecutor和ThreadPoolTaskExecutor
ThreadPoolExecutor和ThreadPoolTaskExecutor很多人容易把这两个搞混。
我们实际开发中更多的是使用SpringBoot来开发Spring默认自带一个线程池方便我们开发它就是ThreadPoolTaskExecutorThreadPoolTaskExecutor是对ThreadPoolExecutor进行了封装处理。
1.1 ThreadPoolExecutor
ThreadPoolExecutor这个类是JDK中的线程池类继承自Executor。 Executor 顾名思义是专门用来处理多线程相关的一个接口所有线程相关的类都实现了这个接口里面有一个execute()方法用来执行线程。ExecutorService为线程池接口提供了线程池生命周期方法继承自Executor接口ThreadPoolExecutor为线程池实现类提供了线程池的维护操作等相关方法继承自AbstractExecutorServiceAbstractExecutorService实现了ExecutorService接口。
线程池主要提供一个线程队列队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销提高了响应的速度。
ThreadPoolExecutor
1.2 ThreadPoolTaskExecutor
ThreadPoolTaskExecutor则是spring包下的是sring为我们提供的线程池类对ThreadPoolExecutor进行封装消除了JavaSE5、JavaSE6和JakartaEE环境之间的差异。
二、 Spring TaskExecutor
执行器是线程池概念的JDK名称。executor命名是因为无法保证底层实现实际上是一个池。执行器可以是单线程的甚至可以是同步的。Spring的抽象隐藏了JavaSE和JakartaEE环境之间的实现细节。
**Spring的TaskExecutor接口与java.util.concurrent.Executor接口相同。事实上最初它存在的主要原因是在使用线程池时不需要Java5。**该接口有一个方法execute(Runnable task)该方法根据线程池的语义和配置接受要执行的任务。
创建TaskExecutor最初是为了在需要时为其他Spring组件提供线程池抽象。ApplicationEventMulticaster、JMS的AbstractMessageListenerContainer和Quartz集成等组件都使用TaskExecutor抽象来池线程。然而如果您的bean需要线程池行为您也可以根据自己的需要使用此抽象。
2.1 TaskExecutor 默认实现
Spring包括许多预先构建的TaskExecutor实现。很可能你永远不需要实现你自己的。Spring提供的变体如下
SyncTaskExecutor此实现不会异步运行调用。相反每次调用都发生在调用线程中。它主要用于不需要多线程的情况例如在简单的测试用例中。SimpleAsyncTaskExecutor此实现不重用任何线程。相反它为每个调用启动一个新线程。然而它确实支持一个并发限制即在释放槽之前阻止任何超过该限制的调用。如果您正在寻找真正的池请参阅此列表后面的ThreadPoolTaskExecutor。ConcurrentSkExecutor此实现是java.util.concurrent.Executor实例的适配器。还有一种替代方法ThreadPoolTaskExecutor将Executtor配置参数公开为bean财产。很少需要直接使用ConcurrentTaskExecutor。但是如果ThreadPoolTaskExecutor不够灵活无法满足您的需要则ConcurrentTaskExecutor是另一种选择。ThreadPoolTaskExecutor此实现最常用。它公开用于配置java.util.concurrent.ThreadPoolExecutor的bean生产并将其包装在TaskExecutor中。如果您需要适应不同类型的java.util.concurrent.Executor我们建议您改用ConcurrentSkExecutor。DefaultManagedTaskExecutor此实现在JSR-236兼容的运行时环境如Jakarta EE应用程序服务器中使用JNDI获得的ManagedExecutorService以取代CommonJ WorkManager。
2.2 TaskExecutor 使用
在下面的示例中我们定义了一个bean它使用ThreadPoolTaskExecutor异步打印一组消息。
首先在配置类中注入ThreadPoolTaskExecutor的bean实例。
Configuration
public class ThreadPoolConfig {Beanprivate ThreadPoolTaskExecutor execThreadPoolTaskExecutor() {ThreadPoolTaskExecutor pool new ThreadPoolTaskExecutor();pool.setCorePoolSize(10); // 核心线程数pool.setMaxPoolSize(50); // 最大线程数pool.setQueueCapacity(500); // 等待队列sizepool.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));}}
}2.3 Jboss EnhancedQueueExecutor
在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();
}三、Spring的TaskScheduler接口详解
除了TaskExecutor抽象之外Spring还有一个TaskScheduler API它具有多种方法来调度将来某个时刻运行的任务。
TaskScheduler接口定义
public interface TaskScheduler { /** * 提交任务调度请求 * param task 待执行任务 * param trigger 使用Trigger指定任务调度规则 * return*/NullableScheduledFuture? 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。这会导致任务在指定时间后运行一次。所有其他方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行但接受触发器的方法要灵活得多。
3.1 Trigger 接口
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();
}3.2 Trigger 接口实现
Spring提供了Trigger接口的两种实现CronTrigger和PeriodicTrigger。
3.2.1 CronTrigger
最有趣的是CronTrigger。它支持基于cron表达式的任务调度。通过Cron表达式来生成调度计划。
例如以下任务计划在每小时15分钟后运行但仅在工作日的朝九晚五“工作时间”内运行
scheduler.schedule(task, new CronTrigger(0 15 9-17 * * MON-FRI));3.2.2 PeriodicTrigger
用于定期执行的Trigger它有两种模式
fixedRate两次任务开始时间之间间隔指定时长fixedDelay: 上一次任务的结束时间与下一次任务开始时间间隔指定时长
可见这两种情况的区别就在于在决定下一次的执行计划时是否要考虑上次任务在什么时间执行完成。 默认情况下PeriodicTrigger使用了fixedDelay模式。
PeriodicTrigger提供以下参数来达成目的
period: Duration类型表示间隔时长注意在fixedRate与fixedDelay两种模式下的不同含义chronoUnit: ChronoUnit类型计时单元initialDelay: Duration类型表示启动任务后间隔多长时间开始执行第一次任务fixedRate: boolean类型表示是否是fixedRate为true时是fixedRate否则是fixedDelay默认为false
PeriodicTrigger接受一个固定的周期、一个可选的初始延迟值和一个布尔值以指示该周期应该被解释为固定速率还是固定延迟。由于TaskScheduler接口已经定义了以固定速率或固定延迟调度任务的方法因此应尽可能直接使用这些方法。PeriodicTrigger实现的价值在于您可以在依赖Trigger抽象的组件中使用它。例如允许交替使用周期性触发器、基于cron的触发器甚至自定义触发器实现可能很方便。这样的组件可以利用依赖注入这样您就可以在外部配置这样的触发器从而轻松地修改或扩展它们。
3.3 TaskScheduler 的实现类
3.3.1 TimerManagerTaskScheduler
用于包装CommonJ中的TimerManager接口。在使用CommonJ进行调度时使用。
3.3.2 ThreadPoolTaskScheduler
包装Java Concurrent中的ScheduledThreadPoolExecutor类大多数场景下都使用它来进行任务调度。 除实现了TaskScheduler接口中的方法外它还包含了一些对ScheduledThreadPoolExecutor进行操作的接口其常用方法如下
setPoolSize 设置线程池大小最小为1默认情况下也为1setErrorHandler 设置异常处理器。getScheduledThreadPoolExecutor 获取ScheduledExecutor默认是ScheduledThreadPoolExecutor类型。getActiveCount 获取当前活动的线程数execute 提交执行一次的任务submit\submitListenable 提交执行一次的任务并且返回一个Future对象供判断任务状态使用。
与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 为任务调度和异步方法提供了注释支持 执行。
4.1 启用调度注解
必须要使用EnableScheduling注解来启用对Scheduled注解的支持EnableScheduling必须使用在工程中某一个被Configuration注解的类上当然程序的主启启动类上也可以因为程序主启动类底层也是含有Configuration注解。
如下例所示
Configuration
EnableAsync
EnableScheduling
public class AppConfig {
}您可以选择应用程序的相关注释。例如如果只需要对Scheduled的支持则可以省略EnableAsync。对于更细粒度的控制可以另外实现SchedulingConfigurer接口、AsyncConfigurer接口或两者。有关详细信息请参阅SchedulingConfigurer和AsyncConfigurer javadoc。
4.2 Scheduled注解
Scheduled注解用在方法上用于表示这个方法将会被调度。不同于Async注解它所注解的方法返回类型最好是void类型的否则它的返回值将不会被TaskScheduler所使用。同时被它注解的方法不能有参数。如果要使用其它的对象的值需要通过依赖注入的方式引用。
它包含有以下属性
cron: 使用Cron语法来指定调度计划zone: 指定时区默认为本地时区fixedDelay: 指定fixedDelay的值它表示上一次任务执行完后多长时间启动下一次任务单位默认是毫秒fixedRate: 指定上一次任务开始时间到下一次任务开始时间的间隔时间单位默认是毫秒initialDelay: 指定提交调度任务后多长时间开始执行第一次任务timeUnit: 指定提交调度任务的时间单位默认是毫秒
例如以下方法每五秒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方法被调用两次。 4.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类型的返回值。这仍然提供了异步执行的好处因此调用者可以在调用Future上的get()之前执行其他任务。以下示例显示如何在返回值的方法上使用Async
Async
FutureString 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 {Asyncvoid doSomething() {// ...}}public class SampleBeanInitializer {private final SampleBean bean;public SampleBeanInitializer(SampleBean bean) {this.bean bean;}PostConstructpublic void initialize() {bean.doSomething();}}4.4 Executor Qualification with Async
默认情况下在方法上指定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注释指定。
4.5 Async异常管理
当Async方法具有Future类型的返回值时很容易管理在方法执行期间引发的异常因为在Future结果上调用get时会引发此异常。然而对于void返回类型异常是未捕获的无法传输。您可以提供AsyncUnaughtExceptionHandler来处理此类异常。以下示例显示了如何执行此操作
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {Overridepublic void handleUncaughtException(Throwable ex, Method method, Object... params) {// handle exception}
}五、cron表达式
5.1 cron表达式详解
所有 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 * * *每天上午 600 和晚上 7000 0/30 8-10 * * *每天 800、830、900、930、1000 和 10300 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每月第一个星期一午夜
5.2 宏
对于人类来说诸如0 0***之类的表达式很难解析因此在出现错误时很难修复。为了提高可读性Spring支持以下宏这些宏表示常用的序列。您可以使用这些宏而不是六位数的值例如Scheduledcron“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 * * * *)