Spring框架分别通过TaskExecutor和TaskScheduler接口为任务的异步执行和调度提供了抽象。Spring还提供了支持应用程序服务器环境中的线程池或CommonJ委托的那些接口的实现。最终,在公共接口后面使用这些实现,消除了JavaSE5、JavaSE6和JakartaEE环境之间的差异。
Spring还具有集成类,以支持Timer(自1.3以来JDK的一部分)和Quartz Scheduler的调度。您可以分别使用FactoryBean和可选的Timer或Trigger实例引用来设置这两个调度器。此外,Quartz Scheduler和Timer都有一个方便类,它允许您调用现有目标对象的方法(类似于普通的MethodInvokingFactoryBean操作)。
执行器是线程池概念的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提供的变体如下:
SyncTaskExecutor:此实现不会异步运行调用。相反,每次调用都发生在调用线程中。它主要用于不需要多线程的情况,例如在简单的测试用例中。
SimpleAsyncTaskExecutor:此实现不重用任何线程。相反,它为每个调用启动一个新线程。然而,它确实支持一个并发限制,即在释放槽之前阻止任何超过该限制的调用。如果您正在寻找真正的池,请参阅此列表后面的ThreadPoolTaskExecutor。
ConcurrentSkExecutor:此实现是java.util.concurrent.Executor实例的适配器。还有一种替代方法(ThreadPoolTaskExecutor)将Executtor配置参数公开为bean财产。很少需要直接使用ConcurrentTaskExecutor。但是,如果ThreadPoolTaskExecutor不够灵活,无法满足您的需要,则ConcurrentTaskExecutor是另一种选择。
ThreadPoolTaskExecutor:此实现最常用。它公开用于配置java.util.concurrent.ThreadPoolExecutor的bean财产,并将其包装在TaskExecuttor中。如果您需要适应不同类型的java.util.concurrent.Executor,我们建议您改用ConcurrentSkExecutor。
DefaultManagedTaskExecutor:此实现在JSR-236兼容的运行时环境(如Jakarta EE应用程序服务器)中使用JNDI获得的ManagedExecutorService,以取代CommonJ WorkManager。
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财产:
除了TaskExecutor抽象之外,Spring还有一个TaskScheduler SPI,它具有多种方法来调度将来某个时刻运行的任务。以下列表显示了TaskScheduler接口定义:
public interface TaskScheduler {Clock getClock();ScheduledFuture schedule(Runnable task, Trigger trigger);ScheduledFuture schedule(Runnable task, Instant startTime);ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
最简单的方法是一个名为schedule的方法,它只需要一个Runnable和一个Instant。这会导致任务在指定时间后运行一次。所有其他方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但接受触发器的方法要灵活得多。
Trigger接口本质上受到JSR-236的启发。触发器的基本思想是,可以根据过去的执行结果甚至任意条件来确定执行时间。如果这些确定考虑了先前执行的结果,则该信息在TriggerContext中可用。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。它支持基于cron表达式的任务调度。例如,以下任务计划在每小时15分钟后运行,但仅在工作日的朝九晚五“工作时间”内运行:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一个实现是PeriodicTrigger,它接受一个固定的周期、一个可选的初始延迟值和一个布尔值,以指示该周期应该被解释为固定速率还是固定延迟。由于TaskScheduler接口已经定义了以固定速率或固定延迟调度任务的方法,因此应尽可能直接使用这些方法。PeriodicTrigger实现的价值在于,您可以在依赖Trigger抽象的组件中使用它。例如,允许交替使用周期性触发器、基于cron的触发器,甚至自定义触发器实现可能很方便。这样的组件可以利用依赖注入,这样您就可以在外部配置这样的触发器,从而轻松地修改或扩展它们。
与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 为任务调度和异步方法提供了注释支持 执行。
要启用对@Scheduled和@Async注释的支持,可以将@EnableScheduling和@EnableAsync添加到@Configuration类之一,如下例所示:
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
您可以选择应用程序的相关注释。例如,如果只需要对@Scheduled的支持,则可以省略@EnableAsync。对于更细粒度的控制,可以另外实现SchedulingConfigurer接口、AsyncConfigurer接口或两者。有关详细信息,请参阅SchedulingConfigurer和AsyncConfigurer javadoc。
如果您喜欢XML配置,可以使用<task:annotation-driven>元素,如下例所示:
注意,对于前面的XML,提供了一个executor引用来处理与带有@Async注释的方法相对应的任务,而提供了调度器引用来管理带有@Scheduled注释的方法。
您可以将@Scheduled注释与触发器元数据一起添加到方法中。例如,以下方法每五秒(5000毫秒)调用一次,具有固定的延迟,这意味着该时间段是从每次前一次调用的完成时间开始计算的。
@Scheduled(fixedDelay = 5000)
public void doSomething() {// something that should run periodically
}
默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的时间单位。如果您想使用不同的时间单位,例如秒或分钟,可以通过@Scheduled中的timeUnit属性进行配置。
例如,前面的示例也可以编写如下。
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {// something that should run periodically
}
如果需要固定速率执行,可以在注释中使用fixedRate属性。以下方法每五秒调用一次(在每次调用的连续开始时间之间测量)。
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {// something that should run periodically
}
对于固定延迟和固定速率的任务,可以通过指示在第一次执行方法之前等待的时间量来指定初始延迟,如下面的fixedRate示例所示。
@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注释并在容器中注册为常规Springbean的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 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();}}
默认情况下,在方法上指定@Async时,所使用的执行器是在启用异步支持时配置的执行器,即,如果使用XML或AsyncConfigurer实现(如果有),则为“注释驱动”元素。但是,当需要指示在执行给定方法时应使用默认值以外的执行器时,可以使用@Async注释的value属性。以下示例显示了如何执行此操作:
@Async("otherExecutor")
void doSomething(String s) {// this will be run asynchronously by "otherExecutor"
}
在这种情况下,“otherExecutor”可以是Spring容器中任何Executor bean的名称,也可以是与任何Executoor关联的限定符的名称(例如,使用
当@Async方法具有Future类型的返回值时,很容易管理在方法执行期间引发的异常,因为在Future结果上调用get时会引发此异常。然而,对于void返回类型,异常是未捕获的,无法传输。您可以提供AsyncUnaughtExceptionHandler来处理此类异常。以下示例显示了如何执行此操作:
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {@Overridepublic void handleUncaughtException(Throwable ex, Method method, Object... params) {// handle exception}
}
代码实例:
org.springframework spring-core 5.0.2.RELEASE
org.springframework spring-context 5.0.2.RELEASE
org.springframework spring-beans 5.0.2.RELEASE
/*** 在spring boot的启动类上面添加 @EnableScheduling 注解*/
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {public static void main(String[] args) {SpringApplication.run(ScheduleApplication.class,args);}
}
@Scheduled注解的另外两个重要属性:fixedRate和fixedDelay
fixedDelay:上一个任务结束后多久执行下一个任务
fixedRate:上一个任务的开始到下一个任务开始时间的间隔
@Component
public class ScheduleDoker {
/*** 测试fixedRate,每2s执行一次* @throws Exception*/
@Scheduled(fixedRate = 2000)
public void fixedRate() throws Exception {System.out.println("fixedRate开始执行时间:" + new Date(System.currentTimeMillis()));//休眠1秒Thread.sleep(1000);System.out.println("fixedRate执行结束时间:" + new Date(System.currentTimeMillis()));
}fixedRate开始执行时间:Sun Feb 12 19:59:05 CST 2022
fixedRate执行结束时间:Sun Feb 12 19:59:06 CST 2022
fixedRate开始执行时间:Sun Feb 12 19:59:07 CST 2022
fixedRate执行结束时间:Sun Feb 12 19:59:08 CST 2022
fixedRate开始执行时间:Sun Feb 12 19:59:09 CST 2022
fixedRate执行结束时间:Sun Feb 12 19:59:10 CST 2022/*** 等上一次执行完等待1s执行* @throws Exception*/
@Scheduled(fixedDelay = 1000)
public void fixedDelay() throws Exception {System.out.println("fixedDelay开始执行时间:" + new Date(System.currentTimeMillis()));//休眠两秒Thread.sleep(1000 * 2);System.out.println("fixedDelay执行结束时间:" + new Date(System.currentTimeMillis()));
}fixedDelay执行结束时间:Sun Feb 12 13:07:23 CST 2022
fixedDelay开始执行时间:Sun Feb 12 13:07:24 CST 2022
fixedDelay执行结束时间:Sun Feb 12 13:07:26 CST 2022
fixedDelay开始执行时间:Sun Feb 12 13:07:27 CST 2022
fixedDelay执行结束时间:Sun Feb 12 13:07:29 CST 2022
}
从版本3.0开始,Spring包含一个用于配置TaskExecutor和TaskScheduler实例的XML命名空间。它还提供了一种方便的方式来配置要使用触发器调度的任务
所有 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)。
以下是一些示例:
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 * * * *) |
上一篇:Scala编程(第四版)