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


相关推荐

  • ReleaseMutex用法

    ReleaseMutex用法Mutex中提供了WiteOne,ReleaseMutex两个实例方法~WiteOne的作用是”阻塞当前线程,提供对该线程的原子操作”也就是说当一个线程遇到WiteOne的时候,如果在WiteOne里面没有线程在操作,则此线程进去操作而里面有线程的时候,所有到此的线程均需要排队等候里面的线程执行完毕~而控制这样操作的结束标记就是使用ReleaseMutex方法!就好比WiteO…

    2022年6月26日
    51
  • share topology_search索引器

    share topology_search索引器如果你知道如何在Rapidshare上搜索的话它就是一个金矿。这里有两个基本方法可以进行搜索,一是使用Google搜索参数对Rapidshare进行搜索,一些网站提供一个基本搜索界面但不如你自己添加参数进行搜索要好。还有一种网站提供自己的搜索数据库进行搜索。第二种网站的搜索更广泛,因为他们使用不同的来源去发现Rapidshare上的新文件包括用户上传的。我不喜欢第一种网站来搜索Rapidshar…

    2025年9月29日
    3
  • ArrayDeque in Java[通俗易懂]

    ArrayDeque in Java[通俗易懂]ArrayDequeinJavaArrayDequeinJavaprovidesawaytoapplyresizable-arrayinadditiontotheimplementationoftheDequeinterface.ItisalsoknownasArrayDoubleEndedQueueorArrayDeck.Thi…

    2022年9月20日
    4
  • xdoj递归数列_递归求数组元素之和

    xdoj递归数列_递归求数组元素之和标题:递归数列类别函数与递归程序类型:代码片段时间限制:2S内存限制10000Kb问题描述一个数列A定义如下A(1)=1,A(2)=1/(1+A(1)),A(3)=1/(1+A(2)),……A(n)=1/(1+A(n-1))。定义一个函数function用来计算数列的第第n项的值,函数声明如下:doublefunction(intn);输入说明:输入为1个正整数n,n<=10。输出说明函数输出数列A第n项的值,…

    2025年10月31日
    1
  • 什么是JPS「建议收藏」

    Linux下安装好了jdk,输入jps时,系统提示“-bash:jps:commandnotfound”。几经查找,得知jps命令是jdk下bin目录中的一个可执行文件,但发现自己安装的jre-6u17-linux-i586.bin里面根本没有,需要安装jre-6u24-linux-i586.bin。据说在JDK1.5之后的版本里就有了这个jps,但居然在1.6_17中

    2022年4月8日
    40
  • 添加了ValidateRequest=”false”仍然报错的解决办法[通俗易懂]

    添加了ValidateRequest=”false”仍然报错的解决办法[通俗易懂]在文本框传递HTML代码时默认是不允许的,会提示有潜在危险字符,只要在页头的指令中加ValidateRequest=”false”就可以解决,如下所示:或查看配置文件中是否有同样的设置,如:若上述操作后仍然报错,可在配置文件中加入节如下:

    2022年6月9日
    32

发表回复

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

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