Scheduled失效原因[通俗易懂]

Scheduled失效原因[通俗易懂]引言在一些业务场景中需要执行定时操作来完成一些周期性的任务,比如每隔一周删除一周前的某些历史数据以及定时进行某项检测任务等等。在日常开发中比较简单的实现方式就是使用Spring的@Scheduled(具体使用方法不再赘述)注解。但是在修改服务器时间时会导致定时任务不执行情况的发生,解决的办法是当修改服务器时间后,将服务进行重启就可以避免此现象的发生。本文将主要探讨服务器时间修改导致@Schedu…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

转载自开发踩坑记录之二:谨慎使用Spring中的@Scheduled注解
https://blog.csdn.net/Diamond_Tao/article/details/80628440
引言
在一些业务场景中需要执行定时操作来完成一些周期性的任务,比如每隔一周删除一周前的某些历史数据以及定时进行某项检测任务等等。在日常开发中比较简单的实现方式就是使用Spring的@Scheduled(具体使用方法不再赘述)注解。但是在修改服务器时间时会导致定时任务不执行情况的发生,解决的办法是当修改服务器时间后,将服务进行重启就可以避免此现象的发生。本文将主要探讨服务器时间修改导致@Scheduled注解失效的原因,同时找到在修改服务器时间后不重启服务的情况下,定时任务仍然正常执行的方法。

@Scheduled失效原因分析

解析流程图

使用新的方法

1.@Scheduled失效原因

(1)首先我们一起看一下@Scheduled注解的源码,主要说明了注解可使用的参数形式,在注解中使用了Schedules这个类。

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

/**
 * A cron-like expression, extending the usual UN*X definition to include
 * triggers on the second as well as minute, hour, day of month, month
 * and day of week.  e.g. {@code "0 * * * * MON-FRI"} means once per minute on
 * weekdays (at the top of the minute - the 0th second).
 * @return an expression that can be parsed to a cron schedule
 * @see org.springframework.scheduling.support.CronSequenceGenerator
 */
String cron() default "";

/**
 * A time zone for which the cron expression will be resolved. By default, this
 * attribute is the empty String (i.e. the server's local time zone will be used).
 * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)},
 * or an empty String to indicate the server's default time zone
 * @since 4.0
 * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone)
 * @see java.util.TimeZone
 */
String zone() default "";

/**
 * Execute the annotated method with a fixed period in milliseconds between the
 * end of the last invocation and the start of the next.
 * @return the delay in milliseconds
 */
long fixedDelay() default -1;

/**
 * Execute the annotated method with a fixed period in milliseconds between the
 * end of the last invocation and the start of the next.
 * @return the delay in milliseconds as a String value, e.g. a placeholder
 * @since 3.2.2
 */
String fixedDelayString() default "";

/**
 * Execute the annotated method with a fixed period in milliseconds between
 * invocations.
 * @return the period in milliseconds
 */
long fixedRate() default -1;

/**
 * Execute the annotated method with a fixed period in milliseconds between
 * invocations.
 * @return the period in milliseconds as a String value, e.g. a placeholder
 * @since 3.2.2
 */
String fixedRateString() default "";

/**
 * Number of milliseconds to delay before the first execution of a
 * {@link #fixedRate()} or {@link #fixedDelay()} task.
 * @return the initial delay in milliseconds
 * @since 3.2
 */
long initialDelay() default -1;

/**
 * Number of milliseconds to delay before the first execution of a
 * {@link #fixedRate()} or {@link #fixedDelay()} task.
 * @return the initial delay in milliseconds as a String value, e.g. a placeholder
 * @since 3.2.2
 */
String initialDelayString() default "";

Jetbrains全家桶1年46,售后保障稳定

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
(2)接下来我们来看下,Spring容器是如何解析@Scheduled注解的。

public class ScheduledAnnotationBeanPostProcessor
implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
SmartInitializingSingleton, ApplicationListener, DisposableBean {


}
1
2
3
4
5
6
Spring容器加载完bean之后,postProcessAfterInitialization将拦截所有以@Scheduled注解标注的方法。

@Override
public Object postProcessAfterInitialization(final Object bean, String beanName) {
	Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
	if (!this.nonAnnotatedClasses.contains(targetClass)) {
		//获取含有@Scheduled注解的方法
		Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
				(MethodIntrospector.MetadataLookup<Set<Scheduled>>) 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 {
		
			// 循环处理包含@Scheduled注解的方法
			annotatedMethods.forEach((method, scheduledMethods) ->
					scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
			if (logger.isDebugEnabled()) {
				logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
						"': " + annotatedMethods);
			}
		}
	}
	return bean;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
再往下继续看,Spring是如何处理带有@Schedule注解的方法的。processScheduled获取scheduled类参数,之后根据参数类型、相应的延时时间、对应的时区将定时任务放入不同的任务列表中。

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {

try {

Assert.isTrue(method.getParameterCount() == 0,
“Only no-arg methods may be annotated with @Scheduled”);
//获取调用的方法
Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass());
//处理线程
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<>(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);
			}
			if (StringUtils.hasLength(initialDelayString)) {
				try {
					initialDelay = parseDelayAsLong(initialDelayString);
				}
				catch (RuntimeException ex) {
					throw new IllegalArgumentException(
							"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
				}
			}
		}

		// 获取cron参数
		String cron = scheduled.cron();
		if (StringUtils.hasText(cron)) {
			String zone = scheduled.zone();
			if (this.embeddedValueResolver != null) {
				cron = this.embeddedValueResolver.resolveStringValue(cron);
				zone = this.embeddedValueResolver.resolveStringValue(zone);
			}
			if (StringUtils.hasLength(cron)) {
				Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
				processedSchedule = true;
				TimeZone timeZone;
				if (StringUtils.hasText(zone)) {
					timeZone = StringUtils.parseTimeZoneString(zone);
				}
				else {
					timeZone = TimeZone.getDefault();
				}
				//加入到定时任务列表中
				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 FixedDelayTask(runnable, fixedDelay, initialDelay)));
		}
		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 = parseDelayAsLong(fixedDelayString);
				}
				catch (RuntimeException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
				}
				tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
			}
		}

		// 执行频率的类型为long
		long fixedRate = scheduled.fixedRate();
		if (fixedRate >= 0) {
			Assert.isTrue(!processedSchedule, errorMessage);
			processedSchedule = true;
			tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
		}
		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 = parseDelayAsLong(fixedRateString);
				}
				catch (RuntimeException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
				}
				tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(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<>(4);
				this.scheduledTasks.put(bean, registeredTasks);
			}
			registeredTasks.addAll(tasks);
		}
	}
	catch (IllegalArgumentException ex) {
		throw new IllegalStateException(
				"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
满足条件时将定时任务添加到定时任务列表中,在加入任务列表的同时对定时任务进行注册。ScheduledTaskRegistrar这个类为Spring容器的定时任务注册中心。以下为ScheduledTaskRegistrar部分源码,主要说明该类中包含的属性。Spring容器通过线程处理注册的定时任务。

public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean {

private TaskScheduler taskScheduler;

private ScheduledExecutorService localExecutor;

private List<TriggerTask> triggerTasks;

private List<CronTask> cronTasks;

private List<IntervalTask> fixedRateTasks;

private List<IntervalTask> fixedDelayTasks;

private final Map<Task, ScheduledTask> unresolvedTasks = new HashMap<Task, ScheduledTask>(16);

private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<ScheduledTask>(16);

......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ScheduledTaskRegistrar类中在处理定时任务时会调用scheduleCronTask方法初始化定时任务。

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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在ThreadPoolTaskShcedule这个类中,进行线程池的初始化。在创建线程池时会创建 DelayedWorkQueue()阻塞队列,定时任务会被提交到线程池,由线程池进行相关的操作,线程池初始化大小为1。当有多个线程需要执行时,是需要进行任务等待的,前面的任务执行完了才可以进行后面任务的执行。

@Override
protected ExecutorService initializeExecutor(
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {

	this.scheduledExecutor = createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler);

	if (this.removeOnCancelPolicy) {
		if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor) {
			((ScheduledThreadPoolExecutor) this.scheduledExecutor).setRemoveOnCancelPolicy(true);
		}
		else {
			logger.info("Could not apply remove-on-cancel policy - not a Java 7+ ScheduledThreadPoolExecutor");
		}
	}

	return this.scheduledExecutor;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
根本原因,jvm启动之后会记录系统时间,然后jvm根据CPU ticks自己来算时间,此时获取的是定时任务的基准时间。如果此时将系统时间进行了修改,当Spring将之前获取的基准时间与当下获取的系统时间进行比对时,就会造成Spring内部定时任务失效。因为此时系统时间发生变化了,不会触发定时任务。

public ScheduledFuture<?> schedule() {

synchronized (this.triggerContextMonitor) {

this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
if (this.scheduledExecutionTime == null) {

return null;
}
//获取时间差
long initialDelay = this.scheduledExecutionTime.getTime() – System.currentTimeMillis();
this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
return this;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2.解析流程图

3.使用新的方法
为了避免使用@Scheduled注解,在修改服务器时间导致定时任务不执行情况的发生。在项目中需要使用定时任务场景的情况下,使ScheduledThreadPoolExecutor进行替代,它任务的调度是基于相对时间的,原因是它在任务的内部 存储了该任务距离下次调度还需要的时间(使用的是基于 System.nanoTime实现的相对时间 ,不会因为系统时间改变而改变,如距离下次执行还有10秒,不会因为将系统时间调前6秒而变成4秒后执行)。

作者:枫之慕
来源:CSDN
原文:https://blog.csdn.net/Diamond_Tao/article/details/80628440
版权声明:本文为博主原创文章,转载请附上博文链接!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/234756.html原文链接:https://javaforall.net

(0)
上一篇 2025年5月27日 上午9:15
下一篇 2025年5月27日 上午9:43


相关推荐

  • goland 2021激活码【2021最新】

    (goland 2021激活码)最近有小伙伴私信我,问我这边有没有免费的intellijIdea的激活码,然后我将全栈君台教程分享给他了。激活成功之后他一直表示感谢,哈哈~IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html…

    2022年3月30日
    42
  • 码农的自我修养-冰冻三尺,非一日之寒

    1.数据结构和算法目录索引(1)数据结构系列①线性表部分:线性表(上){数组、单链表和双链表}线性表(下){循环链表、约瑟夫问题}②栈与队列部分:栈{LIFO、Stack<T

    2021年12月30日
    40
  • 免杀工具汇总_一键免杀

    免杀工具汇总_一键免杀今天整理以前的笔记,把这部分工具整理了一下,虽然没有白利用稳,但这些工具也能在一定程度起到一定的免杀作用。DKMC项目地址:https://github.com/Mr-Un1k0d3r/DKMC可以创建一个outputs文件夹存放shellcode启动pythondkmc.py操作顺序:Sc:是将msf生成的raw文件转换位shellcode代码…

    2022年4月19日
    87
  • java获取服务器路径_Java获取此次请求URL以及服务器根路径的方法「建议收藏」

    java获取服务器路径_Java获取此次请求URL以及服务器根路径的方法「建议收藏」本文介绍了Java获取此次请求URL以及获取服务器根路径的方法,并且进行举例说明,感兴趣的朋友可以学习借鉴下文的内容。一、获取此次请求的URLStringrequestUrl=request.getScheme()//当前链接使用的协议+”://”+request.getServerName()//服务器地址+”:”+request.getServerPort()//端口号+…

    2022年7月11日
    46
  • 你不知道的PreparedStatement预编译[通俗易懂]

    你不知道的PreparedStatement预编译[通俗易懂]大家都知道,Mybatis内置参数,形如#{xxx}的,均采用了sql预编译的形式,大致知道mybatis底层使用PreparedStatement,过程是先将带有占位符(即”?”)的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。如果你的基本结论也是如此,那你就大错特错了。目录1.mysql是否默认开启了预编译功…

    2022年5月30日
    34
  • servlet到底是什么?[通俗易懂]

    servlet到底是什么?[通俗易懂]servlet到底是什么?对于这个问题一直云里雾里的,今天打算刨根问底。一、Servlet简介  Servlet是sun公司提供的一门用于开发动态web资源的技术。  Sun公司在其API中提供了一个servlet接口,用户若想用发一个动态web资源(即开发一个Java程序向浏览器输出数据),需要完成以下2个步骤:  1、编写一个Java类,实现servlet接口。

    2022年6月25日
    27

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号