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)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 一些模板代码

    一些模板代码jdbc模板代码nio读写模板代码publicclassNewBufferTest{publicstaticvoidmain(String[]args)throwsIOExce

    2022年7月3日
    29
  • java.util.Map——Map集合的常用方法「建议收藏」

    java.util.Map——Map集合的常用方法「建议收藏」Java技术交流群:817997079,欢迎“有志之士”的加入。开发中最常用的就是List集合和Map集合,Map集合是基于java核心类——java.util中的;Map集合用于储存元素对,Map储存的是一对键值(key和value),是通过key映射到它的value;下面介绍的是Map集合的一些经常用到的方法以及代码示例。1.map.size();方法作用:获取map集合类的大小(m…

    2022年5月7日
    47
  • flask_migrate数据库迁移遇到的问题

    flask_migrate数据库迁移遇到的问题

    2021年11月23日
    39
  • C语言小游戏——贪吃蛇—-小白专用

    C语言小游戏——贪吃蛇—-小白专用C语言贪吃蛇小游戏个人小白,后期也做了一些改进,附原视频地址(点击即可)废话在后面直接上程序该程序在VS2019上可完美运行。#include<stdio.h>#include<stdlib.h>#include<Windows.h>#include<time.h>#include<conio.h>constexprautomaphigh=28,mapwide=84;structvirus{ intx;

    2022年5月12日
    48
  • HTTP默认端口_http协议使用的端口号

    HTTP默认端口_http协议使用的端口号HTTP默认端口80是http协议的默认端口,是在输入网站的时候其实浏览器(非IE)已经帮你输入协议了,所以你输入http://baidu.com,其实是访问http://baidu.com:80。而8080,一般用与webcahe,完全不一样的两个,比如linux服务器里apache默认跑80端口,而apache-tomcat默认跑8080端口,其实端口没有实际意义只是一个接口,主要是看服务的监听端口。443是https的默认端口端口号标识了一个主机上进行通信的不同的应用程序。 H.

    2025年12月5日
    2
  • java setdaemon_Java Thread setDaemon()方法

    java setdaemon_Java Thread setDaemon()方法Thread 类的 setDaemon 方法用于将线程标记为守护程序线程或用户线程 它的生命依赖于用户线程 即当所有用户线程都消亡时 JVM 会自动终止该线程 必须在线程启动之前调用它 如果在声明线程后调用 setDaemon 方法 则此方法将抛出 IllegalThrea 语法 publicfinalv booleanon 参数 on 如

    2025年6月13日
    5

发表回复

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

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