当前位置: 首页 > news >正文

定时/延时任务-万字解析Spring定时任务原理

文章目录

  • 1. 概要
  • 2. @EnableScheduling 注解
  • 3. @Scheduled 注解
  • 4. postProcessAfterInitialization 解析
    • 4.1 createRunnable
  • 5. 任务 Task 和子类
  • 6. ScheduledTaskRegistrar
    • 6.1 添加任务的逻辑
    • 6.2 调度器初始化
    • 6.3 调用时机
  • 7. taskScheduler 类型
    • 7.1 ConcurrentTaskScheduler
    • 7.2 ThreadPoolTaskScheduler
  • 8. 小结

1. 概要

上一篇文章:定时/延时任务-Spring定时任务的两种实现方式。这篇文章就来看下 Spring 中 @Scheduled 和接口方式的定时任务是如何实现的。

2. @EnableScheduling 注解

Spring 中如果需要使用定时任务,就需要引入 @EnableScheduling,我们看下这个注解是怎么定义的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {}

在这个注解中关键就是:@Import(SchedulingConfiguration.class),这里面通过 Import 引入了 SchedulingConfiguration 配置类。Import 是 Spring 中定义的一种引入配置类的方式,通过 Import 注解可以把对应的类交给 Spring 管理,达到动态开关配置的目的。然后我们再来看下引入的这个注解配置类。

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {return new ScheduledAnnotationBeanPostProcessor();}}

可以看到这个配置类代码比较简单,就是注册了一个 ScheduledAnnotationBeanPostProcessor 后置处理器。后置处理器是 Spring 中用于在 bean 初始化之后调用来处理 bean 的方法。对于 @Scheduled 和 @Schedules 注解解析的核心逻辑就在 postProcessAfterInitialization 中。但是在这之前,我们看下 @Scheduled 注解的属性。

3. @Scheduled 注解

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {/*** 是否禁用 cron 表达式,如果设置成 '-' 就表示不使用 cron 表达式*/String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;/*** cron 表达式* @return*/String cron() default "";/*** 时区* @return*/String zone() default "";/*** 固定延时任务的延时时间* @return*/long fixedDelay() default -1;/*** 固定延时任务的延时时间字符串,可动态配置* @return*/String fixedDelayString() default "";/*** 固定速率任务的延时时间* @return*/long fixedRate() default -1;/*** 固定速率任务的延时时间字符串,可动态配置* @return*/String fixedRateString() default "";/*** 第一次执行任务之前延迟多少秒* @return*/long initialDelay() default -1;/*** 第一次执行任务之前延迟多少秒,字符串,可动态配置* @return*/String initialDelayString() default "";/*** 时间单位,默认是毫秒* @return*/TimeUnit timeUnit() default TimeUnit.MILLISECONDS;}

@Scheduled 定义了几种定时任务的实现方式

  1. cron 表达式任务
  2. 固定延时任务 fixedDelay
  3. 固定速率任务 fixedRate

然后我们再看下 postProcessAfterInitialization 是如何处理上面这几种方法的。


4. postProcessAfterInitialization 解析

/*** 初始化之后回回调后置处理器处理定时任务* @param bean the new bean instance* @param beanName the name of the bean* @return*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||bean instanceof ScheduledExecutorService) {// 注册的 bean 是上面类型的就不处理// Ignore AOP infrastructure such as scoped proxies.return bean;}// 获取实际要处理的 bean 的类型Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);// 1.如果这个目标类还没有被处理过// 2.这个类能不能被 Scheduled 或者 Schedules 注解处理(如果这个类是以 java. 开头或者是 Ordered 类,就不可以)if (!this.nonAnnotatedClasses.contains(targetClass) &&AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {// 从目标类中获取有 Scheduled 或者 Schedules 注解的方法Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class);return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);});// 如果找不到方法if (annotatedMethods.isEmpty()) {// 加入 nonAnnotatedClasses 集合中this.nonAnnotatedClasses.add(targetClass);if (logger.isTraceEnabled()) {logger.trace("No @Scheduled annotations found on bean class: " + targetClass);}}else {// 找到了包含特定注解的方法annotatedMethods.forEach((method, scheduledAnnotations) ->// 遍历来处理所有的方法scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));if (logger.isTraceEnabled()) {logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +"': " + annotatedMethods);}}}return bean;
}

当 ScheduledAnnotationBeanPostProcessor 初始化完成之后,调用 postProcessAfterInitialization 来处理相关的注解,下面来看下具体逻辑。

if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||bean instanceof ScheduledExecutorService) {// 注册的 bean 是上面类型的就不处理// Ignore AOP infrastructure such as scoped proxies.return bean;
}

如果 bean 的类型是 上面三个类型的,就不处理。

// 获取实际要处理的 bean 的类型
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
// 1.如果这个目标类还没有被处理过
// 2.这个类能不能被 Scheduled 或者 Schedules 注解处理(如果这个类是以 java. 开头或者是 Ordered 类,就不可以)
if (!this.nonAnnotatedClasses.contains(targetClass) &&AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {// 从目标类中获取有 Scheduled 或者 Schedules 注解的方法Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class);return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);});// 如果找不到方法if (annotatedMethods.isEmpty()) {// 加入 nonAnnotatedClasses 集合中this.nonAnnotatedClasses.add(targetClass);if (logger.isTraceEnabled()) {logger.trace("No @Scheduled annotations found on bean class: " + targetClass);}}else {// 找到了包含特定注解的方法annotatedMethods.forEach((method, scheduledAnnotations) ->// 遍历来处理所有的方法scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));if (logger.isTraceEnabled()) {logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +"': " + annotatedMethods);}}
}
return bean;

然后获取下 bean 的类型,并且判断下这个类型能不能被 Scheduled 或者 Schedules 注解处理,如果这个类是以 java. 开头或者是 Ordered 类,就不可以。最后遍历这些标记了上面两个注解的方法,一个一个处理,处理的逻辑是 processScheduled,看下 processScheduled 的逻辑

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {// 根据调用的对象和调用的方法创建一个任务Runnable runnable = createRunnable(bean, method);boolean processedSchedule = false;String errorMessage ="Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";Set<ScheduledTask> tasks = new LinkedHashSet<>(4);...
}

这个方法中首先会创建一个任务,然后设置几个属性。

// 把 @Scheduled 注解的 initialDelay 属性转化成毫秒,initialDelay 是指延时多少秒进行第一次执行
long initialDelay = convertToMillis(scheduled.initialDelay(), scheduled.timeUnit());
// @Scheduled 注解的 initialDelayString 属性,作用和上面的 initialDelay 作用一样
String initialDelayString = scheduled.initialDelayString();
if (StringUtils.hasText(initialDelayString)) {// 如果指定了 initialDelayString,那么就不能指定 initialDelay 了// 同时指定 initialDelay 会报错Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");if (this.embeddedValueResolver != null) {// 因为 @Scheduled 注解的几个 String 类型的值都可以通过配置文件引入,也就是 ${} 的方式// 这个方法就是去解析动态配置值的initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);}// 如果配置了if (StringUtils.hasLength(initialDelayString)) {try {// 转换成毫秒单位initialDelay = convertToMillis(initialDelayString, scheduled.timeUnit());}catch (RuntimeException ex) {throw new IllegalArgumentException("Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");}}
}

上面就是解析 @Scheduled 中 initialDelayinitialDelayString 的逻辑。

  1. 首先把 initialDelay 属性转化成毫秒
  2. 然后再解析 initialDelayString,需要注意的是 initialDelay initialDelayString 只能选一个,如果两个都填就会报错
  3. this.embeddedValueResolver.resolveStringValue 是解析动态配置的逻辑,因为 initialDelayString 可以使用 ${} 动态配置
  4. 最后都转化成 initialDelay 毫秒

上面解析的 initialDelayinitialDelayString 表示延时多少 ms 再执行第一次任务。下面再来看下 cron 表达式的解析,这是第一种定时任务的配置方式。

// 获取 cron 表达式
String cron = scheduled.cron();
// 如果设置了 cron 表达式
if (StringUtils.hasText(cron)) {// 获取指定的时区String zone = scheduled.zone();// 同时也是去解析 cron 表达式和时区的动态值if (this.embeddedValueResolver != null) {cron = this.embeddedValueResolver.resolveStringValue(cron);zone = this.embeddedValueResolver.resolveStringValue(zone);}// 如果设置了 cron 表达式if (StringUtils.hasLength(cron)) {// 如果设置了 cron 表达式,那么就不能设置 initialDelay 了Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");// 设置下标记,表示已经设置 cron 表达式了processedSchedule = true;// 如果 cron 表达式没有被设置成 '-' 了,就代表用 cron 去触发if (!Scheduled.CRON_DISABLED.equals(cron)) {// 创建 cron 触发器CronTrigger trigger;if (StringUtils.hasText(zone)) {// 设置时区和 cron 表达式trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone));}else {// 不需要设置时区,设置 cron 表达式trigger = new CronTrigger(cron);}// 将创建的 ScheduledTask 加入任务集合中// 使用 ScheduledTaskRegistrar 创建一个 ScheduledTask,同时需要传入触发器,这个触发器里面需要传入 cron 表达式和时区tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger)));}}
}
  1. 解析 cron 表达式,先获取下时区
  2. 然后解析 cron 表达式和时区的动态值
  3. 如果设置了 cron 表达式,那么就不能设置 initialDelay 了,需要依靠 cron 表达式来执行定时任务
  4. processedSchedule 标记位是代表有没有设置了定时任务的调度方式,这里如果使用 cron 表达式来调度任务,processedSchedule 就会设置为 true
  5. 检查下 cron 有没有被设置成 -,这个标记代表禁用 cron 表达式,如果没有设置为 -,就通过 cron 和时区创建一个 CronTrigger,这是 cron 触发器,在这个触发器里面可以获取表达式和时区等信息,同时也可以获取 cron 下一次执行的时间
  6. 新建一个 ScheduledTask,把这个任务添加到任务集合中,这个是任务的统一包装,里面可以对 CronTaskFixedDelayTaskFixedRateTask 进行包装

上面就是解析 cron 表达式的逻辑,下面继续看解析@Scheduled 注解的 fixedDelay 字段逻辑,这个字段就是固定延时任务。

// 下面检查 @Scheduled 注解的 fixedDelay 字段,这个字段就是固定延时任务
long fixedDelay = convertToMillis(scheduled.fixedDelay(), scheduled.timeUnit());
if (fixedDelay >= 0) {// 如果前面 cron 已经设置了,那么这里就不能设置 fixedDelay 了Assert.isTrue(!processedSchedule, errorMessage);// 设置标记processedSchedule = true;// 同样添加任务,不过这里是创建一个 FixedDelayTask,表示固定延时的任务tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
// 看下有没有设置 fixedDelayString
String fixedDelayString = scheduled.fixedDelayString();
// 如果设置 fixedDelayString 了
if (StringUtils.hasText(fixedDelayString)) {if (this.embeddedValueResolver != null) {// 解析动态字符串fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);}if (StringUtils.hasLength(fixedDelayString)) {// 如果前面没有设置 cron 和 fixDelayAssert.isTrue(!processedSchedule, errorMessage);// 设置标记,表示已解析processedSchedule = true;try {// 转化成毫秒fixedDelay = convertToMillis(fixedDelayString, scheduled.timeUnit());}catch (RuntimeException ex) {throw new IllegalArgumentException("Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");}// 同样添加任务,不过这里是创建一个 FixedDelayTask,表示固定延时的任务tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));}
}
  1. 首先检测 @Scheduled 注解的 fixedDelay,转化成毫秒
    • 判断下 processedSchedule 字段,如果前面已经设置 cron了,那么这里就会抛出异常,也就是说如果设置了 cron 表达式的调度方式,这里就不能设置 fixedDelay 了
    • 创建一个 ScheduledTask 添加到集合里面,这个任务包装类包装的是 FixedDelayTask,里面设置了初始延时时间和固定速率
  2. 然后继续检测 @Scheduled 注解的 fixedDelayString
    • 解析动态字符串
    • 判断下 processedSchedule 字段,如果前面已经设置 cron 或者设置了 fixedDelay 了,那么这里就会抛出异常,也就是说如果设置了 cron 表达式的调度方式或者设置了 fixedDelay 字段,这里就不能设置 fixedDelayString 了
    • 创建一个 ScheduledTask 添加到集合里面,这个任务包装类包装的是 FixedDelayTask,里面设置了初始延时时间和固定速率

上面就是解析固定速率的逻辑,下面继续看解析@Scheduled 注解的 fixedRate 字段逻辑,这个字段就是固定速率任务。

// 下面检查 @Scheduled 注解的 fixedRate 字段,这个字段就是固定速率任务
long fixedRate = convertToMillis(scheduled.fixedRate(), scheduled.timeUnit());
if (fixedRate >= 0) {// 如果设置了 fixedRate,同时前面几种方式都没有设置Assert.isTrue(!processedSchedule, errorMessage);// 设置标记processedSchedule = true;// 同样添加任务,不过这里是创建一个 FixedRateTask,表示固定延时的任务tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
}
// 继续检测 fixedRateString
String fixedRateString = scheduled.fixedRateString();
if (StringUtils.hasText(fixedRateString)) {if (this.embeddedValueResolver != null) {// 解析动态表达式fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);}if (StringUtils.hasLength(fixedRateString)) {// 如果前面几种方式已经设置过了,这里就抛出异常Assert.isTrue(!processedSchedule, errorMessage);// 设置标记位processedSchedule = true;try {// 把 fixedRateString 转化成毫秒fixedRate = convertToMillis(fixedRateString, scheduled.timeUnit());}catch (RuntimeException ex) {throw new IllegalArgumentException("Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");}// 同样添加任务,不过这里是创建一个 FixedRateTask,表示固定延时的任务tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));}
}
  1. 首先检测 @Scheduled 注解的 fixedRate,转化成毫秒
    • 判断下 processedSchedule 字段,如果前面已经设置 cron了,或者已经设置固定延时的方式了,那么这里就会抛出异常
    • 创建一个 ScheduledTask 添加到集合里面,这个任务包装类包装的是 FixedRateTask,里面设置了初始延时时间和固定速率
  2. 然后继续检测 @Scheduled 注解的 fixedRateString
    • 解析动态字符串
    • 判断下 processedSchedule 字段,如果前面已经设置 cron 或者设置了固定延时或者 fixedRate 字段了,那么这里就会抛出异常fixedRate 字段,这里就不能设置 fixedRateString了
    • 创建一个 ScheduledTask 添加到集合里面,这个任务包装类包装的是 FixedRateTask,里面设置了初始延时时间和固定速率

最后把定时任务注册到 scheduledTasks 中,这个属性是一个 map 任务集合,表示 bean -> Set<Task> 的映射。

private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16);

到这里任务就已经被解析完并添加到 scheduledTasks 集合中了,当 bean 注销的时候会从这个集合里面拿出任务来一个一个取消,这个后面再看。

4.1 createRunnable

这个方法一开始就创建了一个任务,我们这里就简单看下创建任务的逻辑。

/*** 根据传入的对象和方法来创建一个任务* @param target* @param method* @return*/
protected Runnable createRunnable(Object target, Method method) {// 如果方法设置了参数,就抛出异常Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");// 选择注解可调用的的同名方法,这个方法不能是 private 或者是 static,又或者是代理类型的Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());// 创建 ScheduledMethodRunnablereturn new ScheduledMethodRunnable(target, invocableMethod);
}

其实逻辑不难,就是从对象里面获取同名方法,这些方法不能是 private 或者是 static,同时这个对象的类不能是代理类。

5. 任务 Task 和子类

上面就是 postProcessAfterInitialization 的逻辑,但是有一些方法我们还没有细看,比如 this.registrar.scheduleCronTask,而这个方法只是把任务添加到集合中,那么这些方法是在哪被调用的呢?既然不是在 ScheduledAnnotationBeanPostProcessor 调度的,是在哪调度的呢?同时在上一篇文章通过接口动态设置 cron 表达式的时候我们不是通过 addTriggerTask 添加了定时任务吗,那么这个方法是在哪里设置任务的, 带着这些问题,下面我们先看下要调度的任务的结构。
不管是上面添加的 CronTaskFixedDelayTaskFixedRateTask,都是子类的实现,但是这些类的顶层结构式怎么样的呢?
在这里插入图片描述

  1. 首先顶层结构是 Task
  2. 下面实现类是 TriggerTask 和 IntervalTask
  3. 最后就是 CronTask、FixedDelayTask 和 FixedRateTask,这三个就是我们设置的 cron、固定延时和固定速率任务

下面来看下里面的源码结构,首先是 Task

public class Task {private final Runnable runnable;public Task(Runnable runnable) {Assert.notNull(runnable, "Runnable must not be null");this.runnable = runnable;}public Runnable getRunnable() {return this.runnable;}@Overridepublic String toString() {return this.runnable.toString();}}

顶层 Task 里面封装了具体的任务 Runnable

public class TriggerTask extends Task {private final Trigger trigger;public TriggerTask(Runnable runnable, Trigger trigger) {super(runnable);Assert.notNull(trigger, "Trigger must not be null");this.trigger = trigger;}public Trigger getTrigger() {return this.trigger;}}public class IntervalTask extends Task {private final long interval;private final long initialDelay;public IntervalTask(Runnable runnable, long interval, long initialDelay) {super(runnable);this.interval = interval;this.initialDelay = initialDelay;}public IntervalTask(Runnable runnable, long interval) {this(runnable, interval, 0);}public long getInterval() {return this.interval;}public long getInitialDelay() {return this.initialDelay;}}
  • TriggerTask 触发器任务是对 cron 任务的封装,里面创建了一个 Trigger 变量,这个 trigger 里面可以获取 cron 的下一次执行的时间。
  • IntervalTask 周期任务是固定速率任务和固定延时任务的父类,这个类里面需要设置 initialDelay(初始延时)interval(周期)

最后来看下底层的三个实现类:

public class CronTask extends TriggerTask {// cron 表达式private final String expression;public CronTask(Runnable runnable, String expression) {this(runnable, new CronTrigger(expression));}public CronTask(Runnable runnable, CronTrigger cronTrigger) {super(runnable, cronTrigger);this.expression = cronTrigger.getExpression();}/*** Return the cron expression defining when the task should be executed.*/public String getExpression() {return this.expression;}}public class FixedRateTask extends IntervalTask {public FixedRateTask(Runnable runnable, long interval, long initialDelay) {super(runnable, interval, initialDelay);}}public class FixedDelayTask extends IntervalTask {public FixedDelayTask(Runnable runnable, long interval, long initialDelay) {super(runnable, interval, initialDelay);}}

其实类里面的结构不复杂,直接看代码就能看明白了。

6. ScheduledTaskRegistrar

6.1 添加任务的逻辑

ScheduledAnnotationBeanPostProcessor 初始化完之后,被 @Scheduled 注解标注的方法会被封装成 Task 存到集合中,那么这些方法是什么时候被调度的,就在这个类里面会解答。在看里面的方法之前,还是先把目光 ScheduledAnnotationBeanPostProcessor 里面,processScheduled 方法中,不管是注册 cron、fixRate、fixDelay 类型的任务,都会通过 registrar 里面的方法去创建。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个 registrar 其实就是 ScheduledTaskRegistrar,那么我们就跟进这几个方法去看看是怎么处理任务的。

/**
* 调度 cron 表达式任务的逻辑* @param task* @return*/
@Nullable
public ScheduledTask scheduleCronTask(CronTask task) {// 从未解析任务集合中移除任务ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);boolean newTask = false;// 如果不存在 ScheduledTask,就创建一个if (scheduledTask == null) {// ScheduledTask 是对 Task 任务的包装scheduledTask = new ScheduledTask(task);// 表示新建了一个 ScheduledTask 任务newTask = true;}// 如果调度器不为空if (this.taskScheduler != null) {// 调度器调度任务scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());}else {// 此时调度器还没有初始化,先把任务存起来// 1.添加到 cronTasks 中// 2.添加到 unresolvedTasks 中addCronTask(task);this.unresolvedTasks.put(task, scheduledTask);}return (newTask ? scheduledTask : null);
}public void addCronTask(CronTask task) {if (this.cronTasks == null) {this.cronTasks = new ArrayList<>();}this.cronTasks.add(task);
}

首先是 scheduleCronTask 方法,这个方法会传入一个 cron 表达式任务,流程是这样的:

  1. 先从 unresolvedTasks 集合中获取这个任务对应的 ScheduledTask,Spring 里面的定时任务都是统一用 ScheduledTask 来调度的,ScheduledTask 里面会封装 CronTask、FixedRateTask、FixedDelayTask
  2. 如果不存在对应的 scheduleCronTask,就创建一个
  3. 如果还没有初始化调度器,说明定时任务还没能调度任务,这时候先把任务加入到 unresolvedTaskscronTasks 集合中
  4. 如果初始化调度器了,就通过调度器去调度 cron 任务
  5. 最后返回任务,如果是新建的就返回新建的任务

这个就是调度 CronTask 的流程,可以看到在里面如果调度器没有初始化的时候是不会调度任务的,只会把任务添加到集合中,等调度器 taskScheduler 初始化之后再调度任务。再看下其他两个调度方法。

/*** 调度固定延时任务* @param task* @return*/
@Nullable
public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) {// 从未解析任务集合中移除任务ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);boolean newTask = false;if (scheduledTask == null) {// 如果不存在就创建一个scheduledTask = new ScheduledTask(task);newTask = true;}// 如果设置调度器了if (this.taskScheduler != null) {if (task.getInitialDelay() > 0) {// 启动时间是当前时间 + 延时时间Date startTime = new Date(this.taskScheduler.getClock().millis() + task.getInitialDelay());// 通过调度器去调度固定延时任务scheduledTask.future =this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), startTime, task.getInterval());}else {// 如果没有设置初始延时时间,直接执行固定延时任务scheduledTask.future =this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), task.getInterval());}}else {// 如果调度器还没有初始化,就先把任务存起来addFixedDelayTask(task);this.unresolvedTasks.put(task, scheduledTask);}return (newTask ? scheduledTask : null);
}/*** Schedule the specified fixed-rate task, either right away if possible* or on initialization of the scheduler.* @return a handle to the scheduled task, allowing to cancel it* (or {@code null} if processing a previously registered task)* @since 5.0.2*/
@Nullable
public ScheduledTask scheduleFixedRateTask(FixedRateTask task) {// 从未调度任务中获取需要调度的固定速率任务ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);boolean newTask = false;if (scheduledTask == null) {// 如果获取不到就创建一个scheduledTask = new ScheduledTask(task);newTask = true;}// 使用调度器去调度任务if (this.taskScheduler != null) {if (task.getInitialDelay() > 0) {// 第一次执行的时间 = 当前时间 + @Scheduled 的 initialDelayDate startTime = new Date(this.taskScheduler.getClock().millis() + task.getInitialDelay());scheduledTask.future =// 进行调度this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), startTime, task.getInterval());}else {scheduledTask.future =this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval());}}else {// 调度器还没有设置,先存到没有调度集合中addFixedRateTask(task);this.unresolvedTasks.put(task, scheduledTask);}return (newTask ? scheduledTask : null);
}

这两个方法跟上面的 scheduleCronTask 逻辑是一样的。

6.2 调度器初始化

上面就是在 ScheduledTaskRegistrar 中的调度任务的逻辑,只是由于调度器还没有初始化,所以任务还不能被调度。那么调度器是在哪被初始化的呢?

public class ScheduledTaskRegistrar implements ScheduledTaskHolder, InitializingBean, DisposableBean {...
}

可以看到 ScheduledTaskRegistrar 实现了 InitializingBean,在初始化之后会调用 afterPropertiesSet

/*** Calls {@link #scheduleTasks()} at bean construction time.* 初始化之后调用*/
@Override
public void afterPropertiesSet() {scheduleTasks();
}/**
* Schedule all registered tasks against the underlying* {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}.*/
@SuppressWarnings("deprecation")
protected void scheduleTasks() {// 如果我们没有自己设置 taskSchedulerif (this.taskScheduler == null) {// 单线程的线程池this.localExecutor = Executors.newSingleThreadScheduledExecutor();// 这里默认就创建一个 ConcurrentTaskScheduler,使用单线程的线程池,其实这个线程池类型就是 ScheduledExecutorServicethis.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);}// 把需要调度的任务添加到 scheduleTasks 中if (this.triggerTasks != null) {for (TriggerTask task : this.triggerTasks) {addScheduledTask(scheduleTriggerTask(task));}}// 把需要调度的 cron 任务添加到 scheduleTasks 中if (this.cronTasks != null) {for (CronTask task : this.cronTasks) {addScheduledTask(scheduleCronTask(task));}}// 把需要调度的固定速率任务添加到 scheduleTasks 中if (this.fixedRateTasks != null) {for (IntervalTask task : this.fixedRateTasks) {addScheduledTask(scheduleFixedRateTask(task));}}// 把需要调度的固定延时任务添加到 scheduleTasks 中if (this.fixedDelayTasks != null) {for (IntervalTask task : this.fixedDelayTasks) {addScheduledTask(scheduleFixedDelayTask(task));}}
}

这个方法会调度所有的任务,可以看到,如果我们没有设置调度器 TaskScheduler,就设置一个单线程的 ScheduledExecutorService 作为任务执行的线程池。所以 @Scheduled 的核心调度是通过 ScheduledExecutorService 来调度的。
还记得上面 processScheduled 方法吗,在这个方法中

  1. CronTask 被添加到 ScheduledTaskRegistrar 的 cronTasks 集合中
  2. FixedDelayTask 被添加到 ScheduledTaskRegistrar 的 fixedDelayTasks 集合中
  3. FixedRateTask 被添加到 ScheduledTaskRegistrar 的 fixedRateTasks 集合中

在上面的 scheduleTasks 方法中,调度的任务顺序是:

  1. triggerTasks
  2. cronTasks
  3. fixedRateTasks
  4. fixedDelayTasks

triggerTasks 是接口实现类的扩展点,上一篇文章中我们通过接口实现动态配置定时任务的时候,就是通过 addTriggerTask 方法把任务添加到 triggerTasks 集合里面。

public abstract class ScheduledConfig implements SchedulingConfigurer {// 定时任务周期表达式private String cron;@Overridepublic void configureTasks(ScheduledTaskRegistrar taskRegistrar) {// 设置线程池,可开启多线程taskRegistrar.setScheduler(taskExecutor());taskRegistrar.addTriggerTask(// 执行定时任务() -> {taskService();},triggerContext -> {// 这里就是动态获取任务队列的逻辑cron = getCron();if(cron == null){throw new RuntimeException("cron not exist");}// 重新获取cron表达式CronTrigger trigger = new CronTrigger(cron);return trigger.nextExecutionTime(triggerContext);});}private Executor taskExecutor() {return BeanUtils.getBean(ThreadPoolTaskScheduler.class);}public abstract void taskService();public abstract String getCron();public abstract int getOpen();
}

所以我们通过 addTriggerTask 添加的任务会在这里被调度,先看调度的逻辑。

@Nullable
public ScheduledTask scheduleTriggerTask(TriggerTask task) {// 从未解析任务集合中获取 ScheduledTaskScheduledTask scheduledTask = this.unresolvedTasks.remove(task);boolean newTask = false;if (scheduledTask == null) {// 如果不存在就创建一个scheduledTask = new ScheduledTask(task);newTask = true;}// 这里调度器已经初始化了if (this.taskScheduler != null) {// 通过调度器去调度任务scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());}else {// 否则就把任务添加到集合中addTriggerTask(task);this.unresolvedTasks.put(task, scheduledTask);}return (newTask ? scheduledTask : null);
}

这个方法我们上面已经看过了,基本是一样的逻辑,只不过在这里 taskScheduler 已经被初始化了,所以这里会通过 ScheduledExecutorService 去调度任务。scheduleCronTaskscheduleFixedRateTaskscheduleFixedDelayTask 的逻辑上面已经说了,这里就不多说了。

6.3 调用时机

上面我们说了 ScheduledTaskRegistrar 的初始化逻辑,那么你有没有想过,ScheduledAnnotationBeanPostProcessor 是怎么和 ScheduledTaskRegistrar 联系起来的,要知道如果 ScheduledTaskRegistrar 是初始化之后调用的 afterPropertiesSet,那跟 ScheduledAnnotationBeanPostProcessor 的初始化就控制不了先后顺序,因为肯定要 ScheduledAnnotationBeanPostProcessor 先初始化解析任务,然后再通过 ScheduledTaskRegistrar 去调度的。并且 ScheduledAnnotationBeanPostProcessor 里面并没有通过 spring 的方式引入 ScheduledTaskRegistrar。
其实 ScheduledAnnotationBeanPostProcessor 里面的 registrar 和 Spring 初始化的 ScheduledTaskRegistrar 不是同一个。当 ScheduledAnnotationBeanPostProcessor 被创建出来时,构造器中就会通过 new 的方式创建出 ScheduledTaskRegistrar。

public ScheduledAnnotationBeanPostProcessor() {this.registrar = new ScheduledTaskRegistrar();}

好了,不卖关子,当 ScheduledAnnotationBeanPostProcessor 创建完成之后,Spring 上下文启动之后会回调 onApplicationEvent 方法。

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {if (event.getApplicationContext() == this.applicationContext) {finishRegistration();}
}

在这个 onApplicationEvent 中会调用 finishRegistration,下面就看下这个方法的逻辑。

private void finishRegistration() {// 如果设置了调度器if (this.scheduler != null) {// 把调度器设置到 registrar 里面this.registrar.setScheduler(this.scheduler);}// BeanFactory 一般都会是这个类型if (this.beanFactory instanceof ListableBeanFactory) {// 获取所有 SchedulingConfigurer 类型的 beanMap<String, SchedulingConfigurer> beans =((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());// 进行排序AnnotationAwareOrderComparator.sort(configurers);// 然后遍历这些实现类for (SchedulingConfigurer configurer : configurers) {// 调用 configureTasks 方法configurer.configureTasks(this.registrar);}}// 如果实现类里面添加了任务并且没有设置 taskSchedulerif (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");try {// 从 Spring 里面找到 TaskScheduler 类型的 bean,然后设置到当前调度器上面this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));}catch (NoUniqueBeanDefinitionException ex) {if (logger.isTraceEnabled()) {logger.trace("Could not find unique TaskScheduler bean - attempting to resolve by name: " +ex.getMessage());}try {this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));}catch (NoSuchBeanDefinitionException ex2) {if (logger.isInfoEnabled()) {logger.info("More than one TaskScheduler bean exists within the context, and " +"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +ex.getBeanNamesFound());}}}catch (NoSuchBeanDefinitionException ex) {if (logger.isTraceEnabled()) {logger.trace("Could not find default TaskScheduler bean - attempting to find ScheduledExecutorService: " +ex.getMessage());}// Search for ScheduledExecutorService bean next...try {this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));}catch (NoUniqueBeanDefinitionException ex2) {if (logger.isTraceEnabled()) {logger.trace("Could not find unique ScheduledExecutorService bean - attempting to resolve by name: " +ex2.getMessage());}try {this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));}catch (NoSuchBeanDefinitionException ex3) {if (logger.isInfoEnabled()) {logger.info("More than one ScheduledExecutorService bean exists within the context, and " +"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +ex2.getBeanNamesFound());}}}catch (NoSuchBeanDefinitionException ex2) {if (logger.isTraceEnabled()) {logger.trace("Could not find default ScheduledExecutorService bean - falling back to default: " +ex2.getMessage());}// Giving up -> falling back to default scheduler within the registrar...logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");}}}// 再调用 afterPropertiesSet 方法this.registrar.afterPropertiesSet();
}

在这个方法中会从 Spring 容器里面获取所有实现了 SchedulingConfigurer 的类,然后调用这些类的 configureTasks 方法,我们设置的 ExecutortriggerTask 就是在这里添加到调度器中的。最后调用调度器的 afterPropertiesSet 方法,在这个方法里面通过调度器调度任务。
好了,到这里我们就看到了,调度器是如何初始化的,同时也看到了添加的任务是如何被调度的。那么我们知道 ScheduledTaskRegistrar 里面的 taskScheduler 有很多种类型,这些类型有什么不同呢?

7. taskScheduler 类型

我们先来看下这个方法,在我们自己实现的 SchedulingConfigurer 接口类中,就通过 taskRegistrar.setScheduler(taskExecutor()) 添加了一个调度器。上面 6.3 的逻辑中如果没有设置这玩意,就会到 Spring 里面根据类型或者名字去找。如果设置了就不会,就用我们自己设置的。
当我们自己设置了 taskScheduler,ScheduledTaskRegistrar 就不会再创建一个默认的单线程的 ConcurrentTaskScheduler 作为 taskScheduler 去执行任务。

/**
* 设置任务调度器,当我们使用接口动态配置的时候* @param scheduler*/
public void setScheduler(@Nullable Object scheduler) {if (scheduler == null) {this.taskScheduler = null;}else if (scheduler instanceof TaskScheduler) {this.taskScheduler = (TaskScheduler) scheduler;}else if (scheduler instanceof ScheduledExecutorService) {// JDK 的 ScheduledExecutorService 类型,需要封装this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler));}else {throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());}
}

其实说到底 taskScheduler 也就是上面这三种类型。

7.1 ConcurrentTaskScheduler

在这里插入图片描述
我们直接看实现类,其实这几个都是差不多的,我们直接看 ConcurrentTaskScheduler 的。因为上面代码中如果 scheduler instanceof ScheduledExecutorService,就会封转成 ConcurrentTaskScheduler 类型,我们先看下构造器。

public ConcurrentTaskScheduler(ScheduledExecutorService scheduledExecutor) {// 父类是 ConcurrentTaskExecutorsuper(scheduledExecutor);// 初始化调度器initScheduledExecutor(scheduledExecutor);
}private void initScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) {if (scheduledExecutor != null) {// 设置调度器this.scheduledExecutor = scheduledExecutor;// ManagedScheduledExecutorService 是 Java 并发实用工具(Concurrency Utilities)为 Java EE 环境提供的一种可管理的调度执行服务// 扩展了 Java SE 的 ScheduledExecutorService,为在 Java EE 环境中提交延迟或周期性任务的执行提供了额外的方法和管理功能this.enterpriseConcurrentScheduler = (managedScheduledExecutorServiceClass != null &&managedScheduledExecutorServiceClass.isInstance(scheduledExecutor));}else {// 创建一个单线程的 ScheduledExecutorServicethis.scheduledExecutor = Executors.newSingleThreadScheduledExecutor();this.enterpriseConcurrentScheduler = false;}
}

这个方法接收了一个 ScheduledExecutorService ,设置到父类里面,然后初始化这个调度器。如果我们没有设置 scheduledExecutor,就会创建一个单线程的 ScheduledExecutorService
然后再看下核心的调度方法,也就是 schedule 方法:

/**
* 调度任务的方法* @param task the Runnable to execute whenever the trigger fires* @param trigger an implementation of the {@link Trigger} interface,* e.g. a {@link org.springframework.scheduling.support.CronTrigger} object* wrapping a cron expression* @return*/
@Override
@Nullable
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {try {// 如果设置了 ManagedScheduledExecutorService,就用 EnterpriseConcurrentTriggerScheduler 去调度,一般也用不上if (this.enterpriseConcurrentScheduler) {return new EnterpriseConcurrentTriggerScheduler().schedule(decorateTask(task, true), trigger);}else {// 使用 ReschedulingRunnable 去调度,这个 errorHandler 是处理异常的ErrorHandler errorHandler =(this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));return new ReschedulingRunnable(task, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule();}}catch (RejectedExecutionException ex) {throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);}
}/*** 调度任务* @param task the Runnable to execute whenever the trigger fires* @param startTime the desired execution time for the task* (if this is in the past, the task will be executed immediately, i.e. as soon as possible)* @return*/
@Override
public ScheduledFuture<?> schedule(Runnable task, Date startTime) {// 获取当前任务还有多久执行long delay = startTime.getTime() - this.clock.millis();try {// 使用 ScheduledExecutorService 去执行延时任务return this.scheduledExecutor.schedule(decorateTask(task, false), delay, TimeUnit.MILLISECONDS);}catch (RejectedExecutionException ex) {throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);}
}@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {long initialDelay = startTime.getTime() - this.clock.millis();try {return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), initialDelay, period, TimeUnit.MILLISECONDS);}catch (RejectedExecutionException ex) {throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);}
}@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {try {return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), 0, period, TimeUnit.MILLISECONDS);}catch (RejectedExecutionException ex) {throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);}
}

这里面 scheduleAtFixedRate 其实就是调用的 ScheduledExecutorService 的方法来调度任务的。而对于 schedule 方法,会通过创建 ReschedulingRunnable 去调度任务。那我们就看下 ReschedulingRunnableschedule 方法。

@Nullable
public ScheduledFuture<?> schedule() {synchronized (this.triggerContextMonitor) {// 计算出下一次的调用时间this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);if (this.scheduledExecutionTime == null) {return null;}// 获取下一次执行时间距离当前的延时long delay = this.scheduledExecutionTime.getTime() - this.triggerContext.getClock().millis();// 调用 ScheduledExecutorService 方法去调度,延时时间是 delaythis.currentFuture = this.executor.schedule(this, delay, TimeUnit.MILLISECONDS);return this;}
}

在这个方法中通过触发器获取下一次任务的执行事件,然后计算出当前时间距离下一次任务执行时间的延时,最后通过 ScheduledExecutorService 去调度任务。然后再来看下这个 ReschedulingRunnablerun 方法,因为调度任务其实就是执行 run 方法。

/*** 任务调度的核心逻辑*/
@Override
public void run() {// 获取当前的时间Date actualExecutionTime = new Date(this.triggerContext.getClock().millis());// 执行父类的方法,父类其实就是调用 task.run,也就是调用我们设置的 @Scheduled 方法super.run();// 任务的完成时间Date completionTime = new Date(this.triggerContext.getClock().millis());synchronized (this.triggerContextMonitor) {Assert.state(this.scheduledExecutionTime != null, "No scheduled execution");// 更新调度器上下文,也就是调度时间、实际的调度时间、完成时间this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);// 如果任务没有取消if (!obtainCurrentFuture().isCancelled()) {// 在里面通过 ScheduledExecutorService 进行方法的调度// 也就是说 spring 启动的时候会先调用一次 run 方法,后续再通过 ScheduledExecutorService 调度任务schedule();}}
}

方法很简单,就是执行具体的任务逻辑,然后更新调度器上下文,把调度时间、实际的调度时间、完成时间都存起来,接着再调用上面的 schedule() 方法,再把任务重新放到 ScheduledExecutorService 里面去执行。

7.2 ThreadPoolTaskScheduler

好了,上面的 ConcurrentTaskScheduler 核心逻辑已经说完了,下面再来看下 ThreadPoolTaskScheduler 的逻辑,因为我们在自定义 SchedulingConfigurer 实现类中设置了ThreadPoolTaskScheduler 作为调度类,所以这里简单看下这个调度类是怎么调度任务的。

public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupportimplements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler {...
}

ThreadPoolTaskScheduler 里面没有提供构造器,就是使用的无参构造器,我们看下自定义 SchedulingConfigurer 实现类中是怎么设置这玩意的。

@Configuration
public class BeanConfig {@Beanpublic ThreadPoolTaskScheduler taskScheduler() {ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();scheduler.setPoolSize(10); // 设置线程池大小scheduler.setThreadNamePrefix("MyTaskScheduler-"); // 设置线程名称前缀scheduler.initialize(); // 初始化调度器return scheduler;}
}private Executor taskExecutor() {return BeanUtils.getBean(ThreadPoolTaskScheduler.class);
}

我们在 BeanConfig 里面注册了类型为 ThreadPoolTaskScheduler 的 bean,然后在 ScheduledConfig 里面通过 BeanUtils.getBean 获取我们注册的 bean。
所以我们就看下 ThreadPoolTaskScheduler 的 父类 ExecutorConfigurationSupport 里面的 afterPropertiesSet()。因为 Spring 创建 ThreadPoolTaskScheduler 的时候会去创建父类 ExecutorConfigurationSupport,而父类 ExecutorConfigurationSupport 实现了 InitializingBean 接口,实现 afterPropertiesSet() 方法。

@Override
public void afterPropertiesSet() {initialize();
}public void initialize() {if (logger.isDebugEnabled()) {logger.debug("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));}if (!this.threadNamePrefixSet && this.beanName != null) {setThreadNamePrefix(this.beanName + "-");}this.executor = initializeExecutor(this.threadFactory, this.rejectedExecutionHandler);
}

其实 ThreadPoolTaskScheduler 里面调度任务用的就是 ExecutorConfigurationSupport 里面的 executor。我们看下这个 executor 的初始化逻辑。

@Override
protected ExecutorService initializeExecutor(ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {this.scheduledExecutor = createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler);if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor) {ScheduledThreadPoolExecutor scheduledPoolExecutor = (ScheduledThreadPoolExecutor) this.scheduledExecutor;if (this.removeOnCancelPolicy) {scheduledPoolExecutor.setRemoveOnCancelPolicy(true);}if (this.continueExistingPeriodicTasksAfterShutdownPolicy) {scheduledPoolExecutor.setContinueExistingPeriodicTasksAfterShutdownPolicy(true);}if (!this.executeExistingDelayedTasksAfterShutdownPolicy) {scheduledPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);}}return this.scheduledExecutor;
}protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler);
}

在初始化 ThreadPoolTaskScheduler 方法中会创建一个 ScheduledExecutorService 来调度任务。然后配置一些参数。

8. 小结

到这里我们终于把 @Scheduled 的原理学习完了,其实这个注解最底层还是用的 ScheduledExecutorService 来调度。







如有错误,欢迎指出!!!

相关文章:

定时/延时任务-万字解析Spring定时任务原理

文章目录 1. 概要2. EnableScheduling 注解3. Scheduled 注解4. postProcessAfterInitialization 解析4.1 createRunnable 5. 任务 Task 和子类6. ScheduledTaskRegistrar6.1 添加任务的逻辑6.2 调度器初始化6.3 调用时机 7. taskScheduler 类型7.1 ConcurrentTaskScheduler7.2…...

Android上传到Minio本地存储

Minio简介 MinIO是一个对象存储解决方案&#xff0c;它提供了与Amazon Web Services S3兼容的API&#xff0c;并支持所有核心S3功能。 MinIO有能力在任何地方部署 - 公有云或私有云&#xff0c;裸金属基础设施&#xff0c;编排环境&#xff0c;以及边缘基础设施。author: https…...

海量数据-Vastbase G100数据库安装

海量数据-Vastbase G100数据库安装 文章目录 海量数据-Vastbase G100数据库安装前期准备防火墙配置方案一&#xff1a;关闭防火墙方案二&#xff1a;开放数据库端口 SELINUX配置时间同步IPC参数配置 单机安装设置主机名创建数据库安装用户和目录(可选)修改资源限制 字符安装&am…...

Python | 对于DataFrame中所有行数据比较的几种方法

在数据分析中&#xff0c;比较DataFrame数据框中的行是一项基本操作&#xff0c;可应用于多种场景&#xff0c;包括&#xff1a; 查找重复项&#xff1a;标识所有相似或包含相同数据的行。相似性检查&#xff1a;确定某些选定因子的不相似行的相似程度。成对分析&#xff1a;非…...

React:闭包陷阱产生和解决

在 React 中&#xff0c;闭包陷阱是一个常见的问题&#xff0c;尤其是在处理异步操作、事件处理器、或是定时器时。理解闭包的工作原理以及它在 React 中如何与状态和渲染交互&#xff0c;可以帮助你避免陷入一些常见的错误。 一、闭包陷阱的产生 1、什么是闭包陷阱&#xff1…...

图的遍历|深度优先搜索|广度优先搜索(C)

图的基本操作 图的基本操作是独立于图的存储结构的。而对于不同的存储方式&#xff0c;操作算法的具体实现会有着不同的性能。在设计具体算法的实现时&#xff0c;应考虑采用何种存储方式的算法效率会更高。 图的基本操作主要包括&#xff08;仅抽象地考虑&#xff0c;所以忽略…...

Pytest-Bdd vs Behave:选择最适合的 Python BDD 框架

Pytest-Bdd vs Behave&#xff1a;选择最适合的 Python BDD 框架 Pytest BDD vs Behave&#xff1a;选择最适合的 Python BDD 框架BDD 介绍Python BDD 框架列表Python BehavePytest BDDPytest BDD vs Behave&#xff1a;关键区别Pytest BDD vs Behave&#xff1a;最佳应用场景结…...

python文字转语音

一、安装对应的包 pip install pyttsx3 二、代码实现 import pyttsx3# 初始化tts引擎 engine pyttsx3.init()# 设置要转换的文本 text "你好"# 设置语速&#xff0c;范围从-1到1&#xff0c;1是正常速度&#xff0c;-1是最慢速度 rate engine.getProperty(rate…...

【安全研究】某黑产网站后台滲透与逆向分析

文章目录 x01. 前言x02. 分析 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与…...

XSLT 编辑 XML

XSLT 编辑 XML 介绍 XSLT&#xff08;可扩展样式表语言转换&#xff09;是一种用于转换XML文档的语言。它允许开发人员将XML数据转换为其他格式&#xff0c;如HTML、PDF或纯文本。XSLT通过使用XPath查询来定位XML文档中的元素&#xff0c;并对这些元素应用转换规则。在本教程…...

计算机网络信息系统安全问题及解决策略

目 录 摘 要 前 言 一、计算机网络信息系统研究现状及安全技术 &#xff08;一&#xff09;计算机网络信息系统研究现状 &#xff08;二&#xff09;计算机网络信息系统全技术概述 二、计算机网络信息系统安全问题 &#xff08;一&#xff09;环境危害引发的安全问…...

112.【C语言】数据结构之排序(详解插入排序)

目录 1.排序定义 2.插入排序 "插入"的含义 代码 函数框架 函数设计思路 以升序为例,分析插入的三种可能 单趟排序代码 优化后 将单趟排序代码嵌入到循环中 错误代码 两种改法 运行结果 时间复杂度 1.排序定义 使一串记录,按照其中的某个或某些关键字的…...

洞察:OpenAI 全球宕机,企业应该如何应对 LLM 的不稳定性?

北京时间12月12日上午&#xff0c;OpenAI证实其聊天机器人ChatGPT正经历全球范围的宕机&#xff0c;ChatGPT、Sora及API受到影响。 OpenAI 更新事故报告称&#xff0c;已查明宕机原因&#xff0c;正努力以最快速度恢复正常服务&#xff0c;并对宕机表示歉意。 此次 OpenAI 故障…...

Git Bash Here 中文显示乱码的处理方法

在使用"open Git Bash Here"时&#xff0c;遇到中文显示乱码问题。 原因&#xff1a;通常是由于编码设置不正确导致的。 open Git Bash Here —>鼠标右击空白处&#xff0c;点击「选项」|或「Options」 在「文本」或 「Text」选项卡中&#xff0c;找到"local…...

【python因果库实战6】LaLonde 数据集

目录 LaLonde 数据集 数据 收入指示变量 教育年限的因子化 变量选择 模型 估计因果效应 未经调整的估计 LaLonde 数据集 经济学家长期以来假设培训项目可以改善参与者的劳动力市场前景。为了测试&#xff08;或证明&#xff09;这一点&#xff0c;国家支持性工作示范项…...

和Ente交流

今日去清华大学深圳国际研究生院能源环境大楼与研三师弟交流。 交流内容&#xff1a; 今年年初3月份的时候去实习&#xff0c;刚刚开始字节远程实习&#xff08;海投&#xff09;&#xff0c;然后远程了一个月&#xff0c;让去线下&#xff0c;然后就去线下待了一个月&#x…...

imx6ull qt多页面控制系统(正点原子imx系列驱动开发)

开题答辩完了也考完了四六级&#xff0c;赶紧来更新一下一个月前留下的坑吧 QAQ首先&#xff0c;因为毕业设计需要用到这些知识所以就从网络上找了一个智能车机系统&#xff0c;借鉴了一下大佬的项目思路&#xff0c;缝缝补补一个月终于完成了这一内容。 在这里先感谢从两位大佬…...

[C++]运算符重载

一、 什么是运算符重载&#xff1f; 运算符重载是 C 中的一种功能&#xff0c;它允许用户定义的类或数据类型重新定义或扩展运算符的行为&#xff0c;使运算符能够作用于用户定义的对象。 二、 通俗解释 在 C 中&#xff0c;运算符&#xff08;如 , -, *, 等&#xff09;默认…...

C++基础

01引用的本质 int a 10;/**引用本质是指针常量&#xff0c;指针指向不可更改,因此引用一旦初始化就不可以更改*自动转换为int* const ref&a;*/int &ref a;/*内部发现是引用&#xff0c;自动转换为*ref20;*/ref20; 02函数高级 2.1默认参数 #include <iostream&g…...

鸿蒙项目云捐助第七讲鸿蒙App应用的首页推荐模块布局的实现

鸿蒙项目云捐助第七讲鸿蒙App应用的首页推荐模块布局的实现 最后设置首页的推荐模块&#xff0c;参考模板如下图所示。 一、首页热门推荐模块的实现 对于热门推荐模块&#xff0c;先有上面的小标题栏&#xff0c;这里的标题栏也有一个小图标&#xff0c;首先从“百度图库”中…...

【网络安全设备系列】1、防火墙

0x00 前言 最近由于工作原因&#xff0c;需要详细如今各类网络安全设备&#xff0c;所以开了此系列文章&#xff0c;希望通过对每个网络安全设备进行整理总结&#xff0c;来详细了解各类网络安全设备作用功能以及实现原理、部署配置方法等。 0x01 定义&#xff1a;防火墙指的…...

C# 备份文件夹

C# 备份目标文件夹 方法1&#xff1a;通过 递归 或者 迭代 结合 C# 方法 参数说明&#xff1a; sourceFolder&#xff1a;源文件夹路径destinationFolder&#xff1a;目标路径excludeNames&#xff1a;源文件夹中不需备份的文件或文件夹路径哈希表errorLog&#xff1a;输出错…...

【sizeof】各种数据类型所占空间大小

各种数据类型所占空间大小 文章目录 前言 一、sizeof是什么&#xff1f; 二、使用步骤 1.整型 2.字符型 总结 前言 ‌sizeof在C语言中是一个运算符&#xff0c;用于获取数据类型或变量在内存中所占的字节数。‌它可以在编译时计算数据类型或变量的内存大小&#xff0c;而…...

水仙花数(流程图,NS流程图)

题目&#xff1a;打印出所有的100-999之间的"水仙花数"&#xff0c;并画出流程图和NS流程图。所谓"水仙花数"是指一个三位数&#xff0c;其各位数字立方和等于该数本身。例如&#xff1a;153是一个"水仙花数"&#xff0c;因为1531的三次方&#…...

wireshark捕获过滤和操作方式详解

大家觉得有用记得关注和点赞&#xff0c;谢谢。 一、Wireshark介绍 Wireshark&#xff08;前身是Ethereal&#xff09;是一个网络封包分析软件&#xff0c;目前是全球使用最广泛的开源抓包软件&#xff0c;别名小鲨鱼或者鲨鱼鳍。 网络封包分析软件的功能是截取网卡进出的网络…...

ChatGPT Search开放:实时多模态搜索新体验

点击访问 chatTools 免费体验GPT最新模型&#xff0c;包括o1推理模型、GPT4o、Claude、Gemini等模型&#xff01; ChatGPT Search&#xff1a;功能亮点解析 本次更新的ChatGPT Search带来了多项令人瞩目的功能&#xff0c;使其在搜索引擎市场中更具竞争力。 1. 高级语音模式&…...

【docker】docker swarm常用命令以及电商平台构建案例

1. 初始化Swarm集群 用于初始化一个Swarm集群&#xff0c;并将当前节点设置为Manager节点。 docker swarm init 用法&#xff1a; docker swarm init --advertise-addr <Manager节点IP>示例&#xff1a; docker swarm init --advertise-addr 192.168.1.100这会将当前节…...

3D计算机视觉概述

3D计算机视觉 3D计算机视觉概述 像机标定 文章目录 3D计算机视觉前言一、人类视觉二、计算机视觉2.1 计算机视觉的研究目的2.2 计算机视觉的研究任务2.3 计算机视觉的研究方法2.4 视觉计算理论2.5 马尔框架中计算机视觉表达的四个层次2.5.1 图像&#xff08;像素表达&#xff…...

23. 合并 K 个升序链表(java)

题目描述&#xff1a; 给你一个链表数组&#xff0c;每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中&#xff0c;返回合并后的链表。 示例 1&#xff1a; 输入&#xff1a;lists [[1,4,5],[1,3,4],[2,6]] 输出&#xff1a;[1,1,2,3,4,4,5,6] 解释&#xff…...

与乐鑫相约 CES 2025|创新技术引领物联网与嵌入式未来

2025 国际消费电子产品展览会 (International Consumer Electronics Show, CES) 将于 2025 年 1 月 7 至 10 日在美国拉斯维加斯盛大开幕。作为全球规模最大、水准最高&#xff0c;且影响力最广的消费电子类科技盛会&#xff0c;CES 每年都吸引着全球行业领袖、开发者和技术爱好…...

MIPS指令集(一)基本操作

目录 计算机硬件的操作数 存储器操作数 常数或立即数操作数 有符号数和无符号数 指令的格式 逻辑操作 决策指令 循环 计算机硬件的操作数 先从一条C语句入手 a b c; 将其翻译为MIPS add a, b, c 其中a&#xff0c;b&#xff0c;c就是这条指令的操作数。表示将b与c…...

半导体数据分析(二):徒手玩转STDF格式文件 -- 码农切入半导体系列

一、概述 在上一篇文章中&#xff0c;我们一起学习了STDF格式的文件&#xff0c;知道了这是半导体测试数据的标准格式文件。也解释了为什么码农掌握了STDF文件之后&#xff0c;好比掌握了切入半导体行业的金钥匙。 从今天开始&#xff0c;我们一起来一步步地学习如何解构、熟…...

在window环境下安装openssl生成钥私、证书和签名,nodejs利用express实现ssl的https访问和测试

在生成我们自己的 SSL 证书之前&#xff0c;让我们创建一个简单的 Express应用程序。 要创建一个新的 Express 项目&#xff0c;让我们创建一个名为node-ssl -server 的目录&#xff0c;用终端cmd中进入node-ssl-server目录。 cd node-ssl-server 然后初始化一个新的 npm 项目…...

C++中类和对象的细节原理

文章目录 一、C中的构造函数二、C中的析构函数三、两者的配合与注意事项四、C中的静态成员变量五、C中的静态成员函数六、C中普通成员函数和静态成员函数的区别七、C中的const成员变量八、C中的const 成员函数九、C中构造函数的初始化列表十、C中的浅拷贝操作十一、C中的深拷贝…...

解决并发情况下调用 Instruct-pix2pix 模型推理错误:index out of bounds 问题

解决并发情况下调用 Instruct-pix2pix 模型推理错误&#xff1a;index out of bounds 问题 背景介绍 在对 golang 开发的 图像生成网站 进行并发测试时&#xff0c;调用基于 Instruct-pix2pix 模型和 FastAPI 的图像生成 API 遇到了以下错误&#xff1a; Model inference er…...

OpenXLSX开源库在 Ubuntu 18.04 的编译、交叉编译与使用教程

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…...

windows和LINUX下校验文件的哈希值(MD5、SHA256)

可以通过两个文件的哈希值来对比两个文件是不是一模一样&#xff0c;有没有缺失 1、windows CertUtil -hashfile 文件路径 MD5 CertUtil -hashfile 文件路径 SHA256 2、Liunx 校验当前目录下所有文件 sha256sum . 校验指定文件名 sha256sum 文件名...

〔 MySQL 〕视图

以下是上述文章的目录&#xff1a; 一、视图概述 视图的定义 二、基本使用 创建视图查询视图修改视图影响基表查询验证删除视图 三、视图规则和限制 命名规则数量限制索引和触发器安全性ORDER BY规则与表一起使用 四、实战案例 牛客实战OJ修改基表影响视图查询验证删除…...

嵌入式硬件产品:CC254x 蓝牙升级

目录 固件更新 OAD原理 作者简介 固件更新 支持固件更新的CC2541芯片中, 包括三段代码: Boot Image Manager(BIM)、ImageA、Im...

Drag and Drop API 实现 JavaScript 中的原生拖放功能

理解什么是拖放&#xff0c;我们先做个简单的实验。鼠标移动到页面左上角“CSDN” 图片上方&#xff0c;点击左键不放开&#xff0c;拖动鼠标&#xff0c;发现图片随着鼠标移动&#xff0c;松开鼠标时&#xff0c;图片消失。 一、拖放&#xff08;Drag and Drop&#xff09;有…...

人脸检测的若干思考!!!

1.目前主要有人脸检测方法分类&#xff1f; 主要包含两类&#xff1a;传统人脸检测算法和基于深度学习的人脸检测算法。 传统人脸检测算法主要可以分为4类&#xff1a; 基于知识、模型、特征和外观的人脸检测方法&#xff1b; 基于深度学习的方法&#xff1a;基于级联CNN的人脸…...

【时间序列分析】斯皮尔曼(Spearman)相关系数理论基础及python代码实现

文章目录 1. 斯皮尔曼相关系数1.1 公式定义1.2 计算过程1.3 计算示例1.4 注意事项&#xff08;当有重复值时&#xff09; 2. 优缺点2.1 优点2.2 缺点 3. 适用场景4. Python代码实现4.1 调用scipy库 5 思考5.1 什么是单调关系&#xff1f;与线性关系的区别是什么&#xff1f;5.2…...

python | linux | ModuleNotFoundError: No module named ‘WFlib‘ |找不到模块

问题&#xff1a; (base) beautyby521-7:~/Website-Fingerprinting-Library-master$ bash scripts/NetCLR.sh Traceback (most recent call last):File "/home/beauty/Website-Fingerprinting-Library-master/exp/pretrain.py", line 8, in <module>from WFli…...

B-TREE详解

B - tree 的详细结构特点 节点结构细节 关键字存储方式&#xff1a;B - tree 节点中的关键字是按照一定顺序排列的&#xff0c;这个顺序可以是升序或者降序。例如&#xff0c;在一个以数字为关键字的 B - tree 中&#xff0c;关键字从左到右依次增大。每个关键字都有一个分隔作…...

Kotlin复习

一、Kotlin类型 1.整数 2.浮点 显示转换&#xff1a; 所有数字类型都支持转换为其他类型&#xff0c;但是转换前会检测长度。 toByte(): Byte toShort(): Short toInt(): Int toLong(): Long toFloat(): Float toDouble(): Double 不同进制的数字表示方法&#xff08;为了提高…...

批处理文件的创建与编辑方法

批处理命令保存在以BAT为扩展名地文本文件中&#xff0c;因此可以使用任何字处理软件创建、编辑批处理文件&#xff0c;如Word、WinHex、Editpuls等。 案例一 使用copy con命令创建批处理文件 ”copy con“是一个功能简单、易于使用的创建文本文件命令&#xff0c;命令中”con…...

Spring Boot集成Kafka:最佳实践与详细指南

文章目录 一、生产者1.引入库2.配置文件3.配置类PublicConfig.javaMessageProducer.java 4.业务处理类 三、消费者1.引入库2.配置类PublicConfig.javaMessageConsumer.java 3.业务类 一、生产者 1.引入库 引入需要依赖的jar包&#xff0c;引入POM文件&#xff1a; <depend…...

maven 中 有历史模块缓存 怎么清

Maven 在运行时会将一些数据保存在本地仓库中&#xff0c;以加快构建过程。其中一部分是项目的依赖项&#xff0c;还有就是“历史模块缓存”。这些缓存信息保存在本地仓库的 _remote.repositories 文件中。 解决方案&#xff1a; 手动删除缓存文件&#xff1a; 进入你的Maven本…...

云计算HCIP-OpenStack04

书接上回&#xff1a; 云计算HCIP-OpenStack03-CSDN博客 12.Nova计算管理 Nova作为OpenStack的核心服务&#xff0c;最重要的功能就是提供对于计算资源的管理。 计算资源的管理就包含了已封装的资源和未封装的资源。已封装的资源就包含了虚拟机、容器。未封装的资源就是物理机提…...

【信息系统项目管理师-论文真题】2015下半年论文详解

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 论题一:大项目或多项目的成本管理解题思路写作要点论题二:项目的采购管理解题思路写作要点论题一:大项目或多项目的成本管理 随着移动互联网、物联网、云计算、大数据等新一代信息技术的广泛应用,我国目前…...