前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >彻底弄懂Spring Schedule加载和执行流程

彻底弄懂Spring Schedule加载和执行流程

作者头像
石奈子
发布2020-06-28 12:13:18
1.5K0
发布2020-06-28 12:13:18
举报
文章被收录于专栏:石奈子的Java之路

Spring Scheduled

Spring定时任务源码分析

入口,启用定时任务注解 @EnableScheduling
代码语言:javascript
复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}
org.springframework.scheduling.annotation.SchedulingConfiguration

线程调度配置

代码语言:javascript
复制
@Configuration
@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();
	}
}
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor
代码语言:javascript
复制
public class ScheduledAnnotationBeanPostProcessor
		implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
		Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
		SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {f

我们重点关注下 MergedBeanDefinitionPostProcessor、SmartInitializingSingleton, ApplicationListener这三个通知器。

ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization

MergedBeanDefinitionPostProcessor-->BeanPostProcessor-->postProcessAfterInitialization

  • 查找Bean对象中有@Scheduled方法的类,并开启任务
代码语言:javascript
复制
	@Override
	public Object postProcessAfterInitialization(final Object bean, String beanName) {
	// AOP原因,可能被代理,这里通过aop工具获取到真实Class
		Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
		// nonAnnotatedClasses集合中只记录没有Scheduled和Schedules注解的对象Class  
		// !contains如果为true则说明没在集合中,可能是没被加入进来,尝试加入,不符合的话就尝试开启Scheduled对象实例
		if (!this.nonAnnotatedClasses.contains(targetClass)) {
			Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
					new MethodIntrospector.MetadataLookup<Set<Scheduled>>() {
						@Override
						public Set<Scheduled> inspect(Method method) {
							Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
									method, Scheduled.class, Schedules.class);
							return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
						}
					});
			if (annotatedMethods.isEmpty()) {
				this.nonAnnotatedClasses.add(targetClass);
				if (logger.isTraceEnabled()) {
					logger.trace("No @Scheduled annotations found on bean class: " + bean.getClass());
				}
			}
			else {
				//循环组件对象(Bean)中包含的@Scheduled方法,并开启
				// Non-empty set of methods
				for (Map.Entry<Method, Set<Scheduled>> entry : annotatedMethods.entrySet()) {
					Method method = entry.getKey();
					for (Scheduled scheduled : entry.getValue()) {
						processScheduled(scheduled, method, bean);
					}
				}
				if (logger.isDebugEnabled()) {
					logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
							"': " + annotatedMethods);
				}
			}
		}
		return bean;
	}
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#processScheduled

生成任务Runnable线程,根据注解属性不同(initialDelay、fixedDelay、cron(我们会以此属性来分析,其他两个相对简单)),设置不同的trigger、task,并交给taskScheduler执行。ps:taskScheduler是在SmartInitializingSingleton, ApplicationListener的方法中完成赋值。我们后边再讲。

代码语言:javascript
复制
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
		try {
			Assert.isTrue(method.getParameterTypes().length == 0,
					"Only no-arg methods may be annotated with @Scheduled");
			//拿到Bean对象中含有@Scheduled的Method实例 ;bean则是method要invoke时传递的obj  method.invoke(obj,args)  ;一般定时任务的方法入参是Null。
			Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass());
			//创建定时任务Runnable实例(下文中有说明)
			Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod);
			boolean processedSchedule = false;
			String errorMessage =
					"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

			Set<ScheduledTask> tasks = new LinkedHashSet<ScheduledTask>(4);

			// Determine initial delay
			long initialDelay = scheduled.initialDelay();
			String initialDelayString = scheduled.initialDelayString();
			if (StringUtils.hasText(initialDelayString)) {
				Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
				if (this.embeddedValueResolver != null) {
					initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
				}
				try {
					initialDelay = Long.parseLong(initialDelayString);
				}
				catch (NumberFormatException ex) {
					throw new IllegalArgumentException(
							"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
				}
			}
			// cron 属性的检查 如果cron有值。且合法,
			// Check cron expression
			String cron = scheduled.cron();
			if (StringUtils.hasText(cron)) {
				Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
				processedSchedule = true;
				String zone = scheduled.zone();
				if (this.embeddedValueResolver != null) {
					cron = this.embeddedValueResolver.resolveStringValue(cron);
					zone = this.embeddedValueResolver.resolveStringValue(zone);
				}
				TimeZone timeZone;
				if (StringUtils.hasText(zone)) {
					timeZone = StringUtils.parseTimeZoneString(zone);
				}
				else {
					timeZone = TimeZone.getDefault();
				}
				// 添加 到tasks中,并放入到时org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleCronTask(CronTask task)中,条件适宜的情况下,直接调用runnable执行。
				tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
			}

			// At this point we don't need to differentiate between initial delay set or not anymore
			if (initialDelay < 0) {
				initialDelay = 0;
			}

			// Check fixed delay
			long fixedDelay = scheduled.fixedDelay();
			if (fixedDelay >= 0) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)));
			}
			String fixedDelayString = scheduled.fixedDelayString();
			if (StringUtils.hasText(fixedDelayString)) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				if (this.embeddedValueResolver != null) {
					fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
				}
				try {
					fixedDelay = Long.parseLong(fixedDelayString);
				}
				catch (NumberFormatException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
				}
				tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)));
			}

			// Check fixed rate
			long fixedRate = scheduled.fixedRate();
			if (fixedRate >= 0) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)));
			}
			String fixedRateString = scheduled.fixedRateString();
			if (StringUtils.hasText(fixedRateString)) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				if (this.embeddedValueResolver != null) {
					fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
				}
				try {
					fixedRate = Long.parseLong(fixedRateString);
				}
				catch (NumberFormatException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
				}
				tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)));
			}

			// Check whether we had any attribute set
			Assert.isTrue(processedSchedule, errorMessage);

			// Finally register the scheduled tasks
			synchronized (this.scheduledTasks) {
				Set<ScheduledTask> registeredTasks = this.scheduledTasks.get(bean);
				if (registeredTasks == null) {
					registeredTasks = new LinkedHashSet<ScheduledTask>(4);
					this.scheduledTasks.put(bean, registeredTasks);
				}
				registeredTasks.addAll(tasks);
			}
		}
		catch (IllegalArgumentException ex) {
			throw new IllegalStateException(
					"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
		}
	}
org.springframework.scheduling.support.ScheduledMethodRunnable

定时方法任务的Runnable对象,传入bean实例(target)和bean中使用@Scheduled注解的方法(method),由taskScheduler开启执行。

代码语言:javascript
复制
public class ScheduledMethodRunnable implements Runnable {

	private final Object target;

	private final Method method;

	//传入bean实例(target)和bean中使用@Scheduled注解的方法(method)
	public ScheduledMethodRunnable(Object target, Method method) {
		this.target = target;
		this.method = method;
	}

	public ScheduledMethodRunnable(Object target, String methodName) throws NoSuchMethodException {
		this.target = target;
		this.method = target.getClass().getMethod(methodName);
	}


	public Object getTarget() {
		return this.target;
	}

	public Method getMethod() {
		return this.method;
	}

	//taskScheduler会调用此处 
	@Override
	public void run() {
		try {
			ReflectionUtils.makeAccessible(this.method);
			this.method.invoke(this.target);
		}
		catch (InvocationTargetException ex) {
			ReflectionUtils.rethrowRuntimeException(ex.getTargetException());
		}
		catch (IllegalAccessException ex) {
			throw new UndeclaredThrowableException(ex);
		}
	}

}
org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleCronTask

如果未处理的任务集合中有,且taskScheduler不为空,直接执行,否则依旧放入未处理的任务集合中。ps:从postProcessAfterInitialization过来时,taskScheduler为空。

代码语言:javascript
复制
	public ScheduledTask scheduleCronTask(CronTask task) {
		ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
		boolean newTask = false;
		if (scheduledTask == null) {
			scheduledTask = new ScheduledTask();
			newTask = true;
		}
		if (this.taskScheduler != null) {
			scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
		}
		else {
			addCronTask(task);
			this.unresolvedTasks.put(task, scheduledTask);
		}
		return (newTask ? scheduledTask : null);
	}
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#afterSingletonsInstantiated

一般情况下,因为继承了ApplicationContext接口,applicationContext不为空,不会执行finishRegistration();方法

代码语言:javascript
复制
	public void afterSingletonsInstantiated() {
		// Remove resolved singleton classes from cache
		this.nonAnnotatedClasses.clear();

		if (this.applicationContext == null) {
			// Not running in an ApplicationContext -> register tasks early...
			finishRegistration();
		}
	}
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#onApplicationEvent

ApplicationEvent的refresh事件触发这里的操作,执行finishRegistration();方法。

代码语言:javascript
复制
	public void onApplicationEvent(ContextRefreshedEvent event) {
		if (event.getApplicationContext() == this.applicationContext) {
			// Running in an ApplicationContext -> register tasks this late...
			// giving other ContextRefreshedEvent listeners a chance to perform
			// their work at the same time (e.g. Spring Batch's job registration).
			finishRegistration();
		}
	}
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#finishRegistration

注入TaskScheduler,并调用ScheduledTaskRegistrar的afterProperties方法开启一些未处理的定时任务。

代码语言:javascript
复制
	private void finishRegistration() {
		//第一次进来this.scheduler为空
		if (this.scheduler != null) {
			this.registrar.setScheduler(this.scheduler);
		}

		if (this.beanFactory instanceof ListableBeanFactory) {
			Map<String, SchedulingConfigurer> beans =
					((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
			List<SchedulingConfigurer> configurers = new ArrayList<SchedulingConfigurer>(beans.values());
			AnnotationAwareOrderComparator.sort(configurers);
			for (SchedulingConfigurer configurer : configurers) {
				configurer.configureTasks(this.registrar);
			}
		}
		// 有task(cron、fixed任务等)且scheduler为空时
		if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {

			try {
				// 如果定义的有TaskScheduler类型的Bean,会直接设置进来(根据名字查),后边会根据类型查,再没有就查找ScheduledExecutorService的name和类型的
				this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, false));
			}
			catch (NoUniqueBeanDefinitionException ex) {
				
				try {
					this.registrar.setTaskScheduler(resolveSchedulerBean(TaskScheduler.class, true));
				}
				catch (NoSuchBeanDefinitionException ex2) {
					
				}
			}
			catch (NoSuchBeanDefinitionException ex) {
				
				// Search for ScheduledExecutorService bean next...
				try {
					this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, false));
				}
				catch (NoUniqueBeanDefinitionException ex2) {
					
					try {
						this.registrar.setScheduler(resolveSchedulerBean(ScheduledExecutorService.class, true));
					}
					catch (NoSuchBeanDefinitionException ex3) {

					}
				}
				catch (NoSuchBeanDefinitionException ex2) {
					
				}
			}
		}
		//最后执行 ScheduledTaskRegistrar的afterPropertiesSet方法
		this.registrar.afterPropertiesSet();
	}
org.springframework.scheduling.config.ScheduledTaskRegistrar#afterPropertiesSet
代码语言:javascript
复制
	public void afterPropertiesSet() {
		scheduleTasks();
	}
org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
代码语言:javascript
复制
	protected void scheduleTasks() {
	//如果为空 则自建处理器
		if (this.taskScheduler == null) {
			this.localExecutor = Executors.newSingleThreadScheduledExecutor();
			this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
		}
		if (this.triggerTasks != null) {
			for (TriggerTask task : this.triggerTasks) {
				addScheduledTask(scheduleTriggerTask(task));
			}
		}
		//处理cron类型的任务
		if (this.cronTasks != null) {
			for (CronTask task : this.cronTasks) {
				addScheduledTask(scheduleCronTask(task));
			}
		}
		if (this.fixedRateTasks != null) {
			for (IntervalTask task : this.fixedRateTasks) {
				addScheduledTask(scheduleFixedRateTask(task));
			}
		}
		if (this.fixedDelayTasks != null) {
			for (IntervalTask task : this.fixedDelayTasks) {
				addScheduledTask(scheduleFixedDelayTask(task));
			}
		}
	}
又回到前边的 ScheduledTaskRegistrar#scheduleTriggerTask 方法

又回到了这里,会执行this.taskScheduler.schedule(task.getRunnable(), task.getTrigger())逻辑。开启任务完成!!!

代码语言:javascript
复制
if (this.taskScheduler != null) {
			scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
		}
小彩蛋Cron的解析

回到 ScheduledAnnotationBeanPostProcessor#processScheduled,我们来看下CronTrigger的逻辑。

代码语言:javascript
复制
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
org.springframework.scheduling.support.CronTrigger#CronTrigger(java.lang.String, java.util.TimeZone)

我们看下CronSequenceGenerator是怎么解析cron表达式的

代码语言:javascript
复制
	public CronTrigger(String expression, TimeZone timeZone) {
		this.sequenceGenerator = new CronSequenceGenerator(expression, timeZone);
	}
org.springframework.scheduling.support.CronSequenceGenerator#CronSequenceGenerator(java.lang.String, java.util.TimeZone)

下边我们一层层找解析过程。最后发现,cron的前六位依次为 “秒 分 时 天 月 星期占位”。使用说明如下。

  • 支持?,*,/字符。
  • 秒位0-59,支持0/5这种格式
  • 分位0-59,支持0/5这种格式
  • 时位0-23,支持0/5这种格式
  • 天位0-31
  • 月位 0-12 ,以及"FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC" 这种格式。
  • 星期位 0-7,以及"SUN,MON,TUE,WED,THU,FRI,SAT" 这种格式。
代码语言:javascript
复制
	private final BitSet months = new BitSet(12);

	private final BitSet daysOfMonth = new BitSet(31);

	private final BitSet daysOfWeek = new BitSet(7);

	private final BitSet hours = new BitSet(24);

	private final BitSet minutes = new BitSet(60);

	private final BitSet seconds = new BitSet(60);
	
	public CronSequenceGenerator(String expression, TimeZone timeZone) {
		this.expression = expression;
		this.timeZone = timeZone;
		parse(expression);
	}
	public CronSequenceGenerator(String expression, TimeZone timeZone) {
		this.expression = expression;
		this.timeZone = timeZone;
		parse(expression);
	}

	// 核心解析过程  注意看 cron的前六位依次对应到时为 “秒 分 时 天 月 星期占位”的bitSet对象中。
	private void doParse(String[] fields) {
		setNumberHits(this.seconds, fields[0], 0, 60);
		setNumberHits(this.minutes, fields[1], 0, 60);
		setNumberHits(this.hours, fields[2], 0, 24);
		setDaysOfMonth(this.daysOfMonth, fields[3]);
		setMonths(this.months, fields[4]);
		setDays(this.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8);

		if (this.daysOfWeek.get(7)) {
			// Sunday can be represented as 0 or 7
			this.daysOfWeek.set(0);
			this.daysOfWeek.clear(7);
		}
	}
	// 解析星期相关值(支持0-8,?,*等)
	private void setDays(BitSet bits, String field, int max) {
		if (field.contains("?")) {
			field = "*";
		}
		setNumberHits(bits, field, 0, max);
	}
private void setNumberHits(BitSet bits, String value, int min, int max) {
		String[] fields = StringUtils.delimitedListToStringArray(value, ",");
		for (String field : fields) {
			if (!field.contains("/")) {
				// Not an incrementer so it must be a range (possibly empty)
				int[] range = getRange(field, min, max);
				bits.set(range[0], range[1] + 1);
			}
			else {
				String[] split = StringUtils.delimitedListToStringArray(field, "/");
				if (split.length > 2) {
					throw new IllegalArgumentException("Incrementer has more than two fields: '" +
							field + "' in expression \"" + this.expression + "\"");
				}
				int[] range = getRange(split[0], min, max);
				if (!split[0].contains("-")) {
					range[1] = max - 1;
				}
				int delta = Integer.parseInt(split[1]);
				if (delta <= 0) {
					throw new IllegalArgumentException("Incrementer delta must be 1 or higher: '" +
							field + "' in expression \"" + this.expression + "\"");
				}
				for (int i = range[0]; i <= range[1]; i += delta) {
					bits.set(i);
				}
			}
		}
	}
	private int[] getRange(String field, int min, int max) {
		int[] result = new int[2];
		if (field.contains("*")) {
			result[0] = min;
			result[1] = max - 1;
			return result;
		}
		if (!field.contains("-")) {
			result[0] = result[1] = Integer.valueOf(field);
		}
		else {
			String[] split = StringUtils.delimitedListToStringArray(field, "-");
			if (split.length > 2) {
				throw new IllegalArgumentException("Range has more than two fields: '" +
						field + "' in expression \"" + this.expression + "\"");
			}
			result[0] = Integer.valueOf(split[0]);
			result[1] = Integer.valueOf(split[1]);
		}
		if (result[0] >= max || result[1] >= max) {
			throw new IllegalArgumentException("Range exceeds maximum (" + max + "): '" +
					field + "' in expression \"" + this.expression + "\"");
		}
		if (result[0] < min || result[1] < min) {
			throw new IllegalArgumentException("Range less than minimum (" + min + "): '" +
					field + "' in expression \"" + this.expression + "\"");
		}
		if (result[0] > result[1]) {
			throw new IllegalArgumentException("Invalid inverted range: '" + field +
					"' in expression \"" + this.expression + "\"");
		}
		return result;
	}
下次执行time
  • org.springframework.scheduling.support.CronTrigger#nextExecutionTime

this.sequenceGenerator.next(date)

  • org.springframework.scheduling.support.CronSequenceGenerator#next

定时任务最终会调用此处,此处使用的逻辑为你在注解中cron给定的值 。

代码语言:javascript
复制
public Date next(Date date) {
		/*
		The plan:

		1 Start with whole second (rounding up if necessary)

		2 If seconds match move on, otherwise find the next match:
		2.1 If next match is in the next minute then roll forwards

		3 If minute matches move on, otherwise find the next match
		3.1 If next match is in the next hour then roll forwards
		3.2 Reset the seconds and go to 2

		4 If hour matches move on, otherwise find the next match
		4.1 If next match is in the next day then roll forwards,
		4.2 Reset the minutes and seconds and go to 2
		*/

		Calendar calendar = new GregorianCalendar();
		calendar.setTimeZone(this.timeZone);
		calendar.setTime(date);

		// First, just reset the milliseconds and try to calculate from there...
		calendar.set(Calendar.MILLISECOND, 0);
		long originalTimestamp = calendar.getTimeInMillis();
		doNext(calendar, calendar.get(Calendar.YEAR));

		if (calendar.getTimeInMillis() == originalTimestamp) {
			// We arrived at the original timestamp - round up to the next whole second and try again...
			calendar.add(Calendar.SECOND, 1);
			doNext(calendar, calendar.get(Calendar.YEAR));
		}

		return calendar.getTime();
	}

结语

分析使用的Spring版本为4.3.18。代码上如有出入,请自行区分。最后如有不对,欢迎指正。 作者:ricky QQ交流群:244930845。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 入口,启用定时任务注解 @EnableScheduling
  • org.springframework.scheduling.annotation.SchedulingConfiguration
  • org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor
  • ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization
    • org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#processScheduled
      • org.springframework.scheduling.support.ScheduledMethodRunnable
        • org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleCronTask
        • org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#afterSingletonsInstantiated
        • org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#onApplicationEvent
          • org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#finishRegistration
            • org.springframework.scheduling.config.ScheduledTaskRegistrar#afterPropertiesSet
              • org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
                • 又回到前边的 ScheduledTaskRegistrar#scheduleTriggerTask 方法
                • 小彩蛋Cron的解析
                  • org.springframework.scheduling.support.CronTrigger#CronTrigger(java.lang.String, java.util.TimeZone)
                    • org.springframework.scheduling.support.CronSequenceGenerator#CronSequenceGenerator(java.lang.String, java.util.TimeZone)
                      • 下次执行time
                      • 结语
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档