Spring:定时任务@Scheduled cron 的实现原理


文章目录

  • 前言
  • [一、Scheduled 对cron 的解析](#一、Scheduled 对cron 的解析)
    • [1.1 流程图:](#1.1 流程图:)
    • [1.2 Scheduled 注解扫描](#1.2 Scheduled 注解扫描)
  • [二、cron 定时任务的调度和执行](#二、cron 定时任务的调度和执行)
    • [2.1 调度器启动](#2.1 调度器启动)
    • [2.2 注册任务到调度器](#2.2 注册任务到调度器)
    • [2.3 :调度器计算下次执行时间(CronTrigger 核心)](#2.3 :调度器计算下次执行时间(CronTrigger 核心))
    • [2.4 线程池延迟执行任务(ScheduledExecutorService)](#2.4 线程池延迟执行任务(ScheduledExecutorService))
  • [三、ThreadPoolTaskScheduler 和 延时队列](#三、ThreadPoolTaskScheduler 和 延时队列)
  • 总结

前言

定时任务 @Scheduled 中配置cron 的表达式,但是spring 如何感知到 改任务的具体执行时机呢,本文对此进行分析。


一、Scheduled 对cron 的解析

1.1 流程图:

初始阶段:

任务触发:

任务执行:

1.2 Scheduled 注解扫描

项目启动时,@EnableScheduling 注解会导入 SchedulingConfiguration 配置类;

该配置类会注册 ScheduledAnnotationBeanPostProcessor(核心后置处理器),它的作用是扫描所有标注 @Scheduled 的方法。

(1) 启动类 @EnableScheduling 注解

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

@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() {
		// bean 后置处理
		return new ScheduledAnnotationBeanPostProcessor();
	}

}

(2)对应@Scheduled 的方法扫描

ScheduledAnnotationBeanPostProcessor 中的 postProcessAfterInitialization 扫描对应的bean

java 复制代码
public Object postProcessAfterInitialization(Object bean, String beanName) {
    // ========== 步骤1:过滤特殊Bean,无需处理定时任务 ==========
    if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
            bean instanceof ScheduledExecutorService) {
        // 忽略AOP基础设施(如代理工厂)、任务调度器本身、JDK定时执行器
        return bean;
    }

    // ========== 步骤2:获取Bean的最终目标类(穿透代理) ==========
    // AopProxyUtils.ultimateTargetClass:获取代理对象背后的真实类(如CGLIB/JDK代理的目标类)
    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
    
    // ========== 步骤3:快速跳过无@Scheduled注解的类(缓存优化) ==========
    // nonAnnotatedClasses:缓存已确认无@Scheduled注解的类,避免重复扫描
    // AnnotationUtils.isCandidateClass:快速判断类是否可能包含@Scheduled注解(避免全量反射)
    if (!this.nonAnnotatedClasses.contains(targetClass) &&
            AnnotationUtils.isCandidateClass(targetClass, List.of(Scheduled.class, Schedules.class))) {
        
        // ========== 步骤4:扫描类中所有标注@Scheduled的方法 ==========
        // MethodIntrospector.selectMethods:遍历类的所有方法,筛选符合条件的方法
        Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
                    // 解析方法上的@Scheduled注解(支持@Schedules批量注解)
                    Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                            method, Scheduled.class, Schedules.class);
                    // 有注解则返回,无则返回null(过滤掉无注解的方法)
                    return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
                });

        // ========== 分支A:无@Scheduled方法,缓存标记 ==========
        if (annotatedMethods.isEmpty()) {
            // 加入缓存,后续无需重复扫描该类
            this.nonAnnotatedClasses.add(targetClass);
            if (logger.isTraceEnabled()) {
                logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
            }
        }
        // ========== 分支B:有@Scheduled方法,处理并注册任务 ==========
        else {
            // 遍历所有标注了@Scheduled的方法 + 方法上的所有@Scheduled注解(一个方法可多个@Scheduled)
            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);
            }

            // ========== 步骤5:处理非单例Bean,标记需手动取消任务 ==========
            // 判断条件:Bean不是单例 或 是FactoryBean创建的单例 → 需手动取消
            if ((this.beanFactory != null &&
                    (!this.beanFactory.containsBean(beanName) || !this.beanFactory.isSingleton(beanName)) ||
                    (this.beanFactory instanceof SingletonBeanRegistry sbr && sbr.containsSingleton(beanName)))) {
                // 将Bean加入手动取消列表,上下文关闭时触发cancel,避免内存泄漏
                this.manualCancellationOnContextClose.add(bean);
            }
        }
    }
    // 返回原Bean(后置处理器不修改Bean本身,仅处理注解)
    return bean;
}

(3)注解解析 :processScheduled

java 复制代码
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
    // ========== 步骤1:判断是否为响应式任务(核心前置条件) ==========
    // REACTIVE_STREAMS_PRESENT:是否引入了Reactive Streams依赖(spring-webflux/reactor-core)
    // ScheduledAnnotationReactiveSupport.isReactive(method):判断方法是否是响应式类型(返回Mono/Flux)
    if (REACTIVE_STREAMS_PRESENT && ScheduledAnnotationReactiveSupport.isReactive(method)) {
        // ========== 分支1:响应式任务 → 异步处理 ==========
        // 1. 校验响应式方法合法性(如不能是Kotlin挂起函数,返回值必须是deferred Publisher)
        // 2. 注册响应式定时任务(适配Reactor的调度逻辑)
        processScheduledAsync(scheduled, method, bean);
        return;
    }
    // ========== 分支2:普通同步任务 → 同步处理 ==========
    // 处理传统定时任务(void返回值/非响应式返回值),如cron/fixedRate等
    processScheduledSync(scheduled, method, bean);
}

private void processScheduledSync(Scheduled scheduled, Method method, Object bean) {
		Runnable task;
		try {
			// 保证要调用的原始方法
			task = createRunnable(bean, method, scheduled.scheduler());
		}
		catch (IllegalArgumentException ex) {
			throw new IllegalStateException("Could not create recurring task for @Scheduled method '" +
					method.getName() + "': " + ex.getMessage());
		}
		// 
		processScheduledTask(scheduled, task, method, bean);
	}

(4)任务 的解析:

bash 复制代码
private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) {
    try {
        // 标记是否处理了有效的调度属性(cron/fixedDelay/fixedRate)
        boolean processedSchedule = false;
        // 核心错误提示:cron/fixedDelay/fixedRate 必须且只能指定一个
        String errorMessage = "Exactly one of the 'cron', 'fixedDelay' or 'fixedRate' attributes is required";

        // 存储当前方法解析出的所有定时任务(一个方法可多个@Scheduled,每个注解对应一个任务)
        Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

        // ========== 步骤1:解析初始延迟(initialDelay) ==========
        // 1.1 解析硬编码的initialDelay(如initialDelay = 1000,timeUnit = TimeUnit.MILLISECONDS)
        Duration initialDelay = toDuration(scheduled.initialDelay(), scheduled.timeUnit());
        // 1.2 解析字符串形式的initialDelay(支持占位符,如initialDelayString = "${initial.delay:1000}")
        String initialDelayString = scheduled.initialDelayString();
        if (StringUtils.hasText(initialDelayString)) {
            // 校验:不能同时指定initialDelay(数字)和initialDelayString(字符串)
            Assert.isTrue(initialDelay.isNegative(), "Specify 'initialDelay' or 'initialDelayString', not both");
            // 解析占位符(如${initial.delay} → 实际值)
            if (this.embeddedValueResolver != null) {
                initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
            }
            if (StringUtils.hasLength(initialDelayString)) {
                try {
                    // 将字符串转为Duration(支持"1s"/"5m"等格式)
                    initialDelay = toDuration(initialDelayString, scheduled.timeUnit());
                }
                catch (RuntimeException ex) {
                    throw new IllegalArgumentException(
                            "Invalid initialDelayString value \"" + initialDelayString + "\"; " + ex);
                }
            }
        }

        // ========== 步骤2:解析Cron表达式 ==========
        String cron = scheduled.cron();
        if (StringUtils.hasText(cron)) {
            String zone = scheduled.zone();
            // 解析占位符(如cron = "${cron.expression}", zone = "${time.zone}")
            if (this.embeddedValueResolver != null) {
                cron = this.embeddedValueResolver.resolveStringValue(cron);
                zone = this.embeddedValueResolver.resolveStringValue(zone);
            }
            if (StringUtils.hasLength(cron)) {
                // 校验:cron不支持initialDelay(cron本身是基于时间表达式的,无需初始延迟)
                Assert.isTrue(initialDelay.isNegative(), "'initialDelay' not supported for cron triggers");
                processedSchedule = true; // 标记已处理有效调度属性
                // 排除禁用cron的情况(cron = "-",即Scheduled.CRON_DISABLED)
                if (!Scheduled.CRON_DISABLED.equals(cron)) {
                    // 创建CronTrigger(核心:解析cron表达式+时区)
                    CronTrigger trigger;
                    if (StringUtils.hasText(zone)) {
                        trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone));
                    }
                    else {
                        trigger = new CronTrigger(cron);
                    }
                    // 封装为CronTask并注册到调度器
                    tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger)));
                }
            }
        }

        // ========== 步骤3:处理初始延迟(统一为Duration.ZERO或实际值) ==========
        // 若未指定initialDelay → 延迟0;指定了则用实际值(后续fixedDelay/fixedRate共用)
        Duration delayToUse = (initialDelay.isNegative() ? Duration.ZERO : initialDelay);

        // ========== 步骤4:解析fixedDelay(固定延迟) ==========
        // 4.1 解析硬编码的fixedDelay(如fixedDelay = 5000)
        Duration fixedDelay = toDuration(scheduled.fixedDelay(), scheduled.timeUnit());
        if (!fixedDelay.isNegative()) {
            // 校验:不能同时指定cron(processedSchedule=true)和fixedDelay
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            // 封装为FixedDelayTask并注册(固定延迟:上一次执行完成后延迟delayToUse执行)
            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
        }
        // 4.2 解析字符串形式的fixedDelay(支持占位符,如fixedDelayString = "${fixed.delay:5000}")
        String fixedDelayString = scheduled.fixedDelayString();
        if (StringUtils.hasText(fixedDelayString)) {
            if (this.embeddedValueResolver != null) {
                fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
            }
            if (StringUtils.hasLength(fixedDelayString)) {
                Assert.isTrue(!processedSchedule, errorMessage);
                processedSchedule = true;
                try {
                    fixedDelay = toDuration(fixedDelayString, scheduled.timeUnit());
                }
                catch (RuntimeException ex) {
                    throw new IllegalArgumentException(
                            "Invalid fixedDelayString value \"" + fixedDelayString + "\"; " + ex);
                }
                tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
            }
        }

        // ========== 步骤5:解析fixedRate(固定频率) ==========
        // 5.1 解析硬编码的fixedRate(如fixedRate = 3000)
        Duration fixedRate = toDuration(scheduled.fixedRate(), scheduled.timeUnit());
        if (!fixedRate.isNegative()) {
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            // 封装为FixedRateTask并注册(固定频率:上一次开始执行后延迟delayToUse执行)
            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
        }
        // 5.2 解析字符串形式的fixedRate(支持占位符)
        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 {
                    fixedRate = toDuration(fixedRateString, scheduled.timeUnit());
                }
                catch (RuntimeException ex) {
                    throw new IllegalArgumentException(
                            "Invalid fixedRateString value \"" + fixedRateString + "\"; " + ex);
                }
                tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
            }
        }

        // ========== 步骤6:处理一次性任务(仅指定initialDelay,无cron/fixedDelay/fixedRate) ==========
        if (!processedSchedule) {
            // 校验:一次性任务必须指定initialDelay
            if (initialDelay.isNegative()) {
                throw new IllegalArgumentException("One-time task only supported with specified initial delay");
            }
            // 封装为OneTimeTask(仅执行一次,延迟delayToUse)
            tasks.add(this.registrar.scheduleOneTimeTask(new OneTimeTask(runnable, delayToUse)));
        }

        // ========== 步骤7:缓存注册的任务(用于上下文关闭时取消) ==========
        synchronized (this.scheduledTasks) {
            // 按Bean缓存任务列表:key=Bean实例,value=该Bean下的所有定时任务
            Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
            regTasks.addAll(tasks);
        }
    }
    catch (IllegalArgumentException ex) {
        // 包装异常,添加方法名,方便定位问题
        throw new IllegalStateException(
                "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
    }
}
  • processScheduledTask 是同步定时任务注册的最终入口,解析 @Scheduled 所有核心属性;
  • 按「cron → fixedDelay → fixedRate → 一次性任务」的优先级解析,确保互斥性;
  • 支持硬编码值和占位符解析,适配配置中心动态调整;
  • 将不同类型的任务封装为 CronTask/FixedDelayTask/FixedRateTask/OneTimeTask,注册到调度器并缓存;

二、cron 定时任务的调度和执行

spring 把定时任务缓存到 Map<Object, Set> scheduledTasks 后,这些任务的执行过程未「任务注册→调度器启动→定时触发→任务执行→循环调度」,scheduledTasks 本质是「任务缓存容器」(用于管理 / 取消任务),而非直接触发执行的组件,真正的触发逻辑由 Spring 内置的 TaskScheduler 调度器实现

2.1 调度器启动

ThreadPoolTaskScheduler 会被初始化并启动,若未自定义调度器,Spring 会自动创建默认的 ThreadPoolTaskScheduler(核心线程数默认 1,可通过 spring.task.scheduling.pool.size 配置);调度器初始化时,会创建 ScheduledExecutorService(JDK 核心定时线程池),作为任务触发的底层线程池;

调度器启动后,监听「上下文刷新完成事件(ContextRefreshedEvent)」,确认所有任务注册完成,开始调度逻辑

调度器初始化 ExecutorConfigurationSupport #initialize

c 复制代码
private @Nullable ExecutorService executor;

public void initialize() {
		if (logger.isDebugEnabled()) {
			logger.debug("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
		}
		if (!this.threadNamePrefixSet && this.beanName != null) {
			setThreadNamePrefix(this.beanName + "-");
		}
		ThreadFactory factory = (this.virtualThreads ?
				new VirtualThreadTaskExecutor(getThreadNamePrefix()).getVirtualThreadFactory() : this.threadFactory);
		// 线程池初始化
		this.executor = initializeExecutor(factory, this.rejectedExecutionHandler);
		this.lifecycleDelegate = new ExecutorLifecycleDelegate(this.executor);
	}

ThreadPoolTaskScheduler 初始化 ThreadPoolTaskScheduler #initializeExecutor

java 复制代码
protected ExecutorService initializeExecutor(
			ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
		//  创建核心线程池
		this.scheduledExecutor = createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler);

		if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor threadPoolExecutor) {
			if (this.removeOnCancelPolicy) {
				threadPoolExecutor.setRemoveOnCancelPolicy(true);
			}
			if (this.continueExistingPeriodicTasksAfterShutdownPolicy) {
				threadPoolExecutor.setContinueExistingPeriodicTasksAfterShutdownPolicy(true);
			}
			if (!this.executeExistingDelayedTasksAfterShutdownPolicy) {
				threadPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
			}
		}

		return this.scheduledExecutor;
	}

2.2 注册任务到调度器

java 复制代码
// 注册CronTask到调度器
private @Nullable TaskScheduler taskScheduler;

public ScheduledTask scheduleCronTask(CronTask cronTask) {
    ScheduledTask scheduledTask = new ScheduledTask(cronTask);
    // 核心:调度器根据CronTrigger计算下次执行时间,提交任务
    scheduledTask.future = this.taskScheduler.schedule(
        cronTask.getRunnable(), cronTask.getTrigger()
    );
    return scheduledTask;
}

2.3 :调度器计算下次执行时间(CronTrigger 核心)

java 复制代码
// CronTrigger.nextExecutionTime() 核心逻辑
public Date nextExecutionTime(TriggerContext triggerContext) {
    // 解析Cron表达式,计算下次执行时间
	  Instant timestamp = determineLatestTimestamp(triggerContext);
		ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone());
		ZonedDateTime zonedTimestamp = timestamp.atZone(zone);
		ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp);
		return (nextTimestamp != null ? nextTimestamp.toInstant() : null);
}

2.4 线程池延迟执行任务(ScheduledExecutorService)

ThreadPoolTaskScheduler #schedule

java 复制代码
	@Override
	public @Nullable ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
		ScheduledExecutorService executor = getScheduledExecutor();
		try {
			ErrorHandler errorHandler = this.errorHandler;
			if (errorHandler == null) {
				errorHandler = TaskUtils.getDefaultErrorHandler(true);
			}
			// 封装任务为ScheduledFutureTask(实现Runnable+Future)
			return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule();
		}
		catch (RejectedExecutionException ex) {
			throw new TaskRejectedException(executor, task, ex);
		}
	}
public @Nullable ScheduledFuture<?> schedule() {
		synchronized (this.triggerContextMonitor) {
			this.scheduledExecutionTime = this.trigger.nextExecution(this.triggerContext);
			if (this.scheduledExecutionTime == null) {
				return null;
			}
			Duration delay = Duration.between(this.triggerContext.getClock().instant(), this.scheduledExecutionTime);
			// 提交到线程池队列,等待延迟执行
			this.currentFuture = this.executor.schedule(this, delay.toNanos(), TimeUnit.NANOSECONDS);
			return this;
		}
	}
  • scheduledTasks 是任务缓存容器,仅用于「注册 / 取消」任务,不参与触发执行;
  • 任务触发的核心是 ThreadPoolTaskScheduler(封装 JDK 定时线程池),通过「触发器计算下次执行时间→线程池延迟执行→执行完成后重新调度」形成循环;
  • 任务执行的本质是反射调用 @Scheduled 方法,执行完成后重新计算下次时间,直到应用停止

三、ThreadPoolTaskScheduler 和 延时队列

ThreadPoolTaskScheduler 是 Spring 对 JDK 定时线程池(ScheduledThreadPoolExecutor)的封装,而 ScheduledThreadPoolExecutor 的核心就是基于「延时队列(DelayedWorkQueue)」实现任务的延迟触发,因此 ThreadPoolTaskScheduler 本质是通过「延时队列」来管理定时任务的执行时机。

组件 角色定位 核心关系
ThreadPoolTaskScheduler Spring 封装的上层调度器(面向开发者) 依赖 JDK ScheduledThreadPoolExecutor,间接使用其内置的延时队列
延时队列(DelayedWorkQueue JDK ScheduledThreadPoolExecutor 内置的阻塞队列(底层数据结构) 存储待执行的定时任务,按「下次执行时间」排序,是延迟触发的核心载体
ScheduledThreadPoolExecutor JDK 定时线程池(ThreadPoolTaskScheduler 的底层实现) 核心逻辑:将任务放入延时队列,线程轮询队列,仅当任务到达执行时间才取出执行
  • ThreadPoolTaskScheduler 的核心属性是 ScheduledExecutorService,而默认实现就是 ScheduledThreadPoolExecutor;
  • ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的子类,它重写了「任务提交逻辑」和「队列类型」------ 强制使用内置的 DelayedWorkQueue(延时队列),且不允许外部替换;
  • 队列中的元素是 ScheduledFutureTask(实现 Delayed 接口),队列会按「任务的下次执行时间(getDelay ())」从小到大排序,保证先到期的任务排在队首。

总结

spring通过 bean的后置处理器 扫描有@Scheduled 的注解并封装为ScheduledTask ,并通过对应的Trigger 触发器获取到下一次执行任务的时间,任务放入延时队列,线程轮询队列,仅当任务到达执行时间才取出执行 。

相关推荐
郑州光合科技余经理2 小时前
源码部署同城O2O系统:中台架构开发指南
java·开发语言·后端·架构·系统架构·uni-app·php
阿波罗尼亚2 小时前
Java框架中的分层架构
java·开发语言·架构
Marktowin2 小时前
访问控制权限模型分析梳理
后端
爬山算法2 小时前
Hibernate(59)Hibernate的SQL日志如何查看?
java·sql·hibernate
野生技术架构师2 小时前
【面试题】为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
java·开发语言
Leo July2 小时前
【Java】Java设计模式实战指南:从原理到框架应用
java·开发语言·设计模式
Anastasiozzzz2 小时前
力扣hot100 20.有效的括号 解析
java·算法·面试·力扣
brave_zhao2 小时前
如何解决 Spoon 与 Carte 互联的 UTF-8 编码问题
java
重生之我是Java开发战士2 小时前
【数据结构】Map、Set与哈希表底层原理
java·数据结构·散列表