我勸你謹(jǐn)慎使用Spring中的@Scheduled注解
引言
在一些業(yè)務(wù)場景中需要執(zhí)行定時操作來完成一些周期性的任務(wù),比如每隔一周刪除一周前的某些歷史數(shù)據(jù)以及定時進(jìn)行某項(xiàng)檢測任務(wù)等等。
在日常開發(fā)中比較簡單的實(shí)現(xiàn)方式就是使用Spring的@Scheduled(具體使用方法不再贅述)注解。
但是在修改服務(wù)器時間時會導(dǎo)致定時任務(wù)不執(zhí)行情況的發(fā)生,解決的辦法是當(dāng)修改服務(wù)器時間后,將服務(wù)進(jìn)行重啟就可以避免此現(xiàn)象的發(fā)生。
本文將主要探討服務(wù)器時間修改導(dǎo)致@Scheduled注解失效的原因,同時找到在修改服務(wù)器時間后不重啟服務(wù)的情況下,定時任務(wù)仍然正常執(zhí)行的方法。
- @Scheduled失效原因分析
- 解析流程圖
- 使用新的方法
1.@Scheduled失效原因
(1)首先我們一起看一下@Scheduled注解的源碼,主要說明了注解可使用的參數(shù)形式,在注解中使用了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 "";
}
(2)接下來我們來看下,Spring容器是如何解析@Scheduled注解的。
public class ScheduledAnnotationBeanPostProcessor
implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
...
}
Spring容器加載完bean之后,postProcessAfterInitialization將攔截所有以@Scheduled注解標(biāo)注的方法。
@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 {
// 循環(huán)處理包含@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;
}
再往下繼續(xù)看,Spring是如何處理帶有@Schedule注解的方法的。processScheduled獲取scheduled類參數(shù),之后根據(jù)參數(shù)類型、相應(yīng)的延時時間、對應(yīng)的時區(qū)將定時任務(wù)放入不同的任務(wù)列表中。
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
Assert.isTrue(method.getParameterCount() == 0,
"Only no-arg methods may be annotated with @Scheduled");
//獲取調(diào)用的方法
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參數(shù)
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();
}
//加入到定時任務(wù)列表中
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)));
}
}
// 執(zhí)行頻率的類型為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());
}
}
滿足條件時將定時任務(wù)添加到定時任務(wù)列表中,在加入任務(wù)列表的同時對定時任務(wù)進(jìn)行注冊。ScheduledTaskRegistrar這個類為Spring容器的定時任務(wù)注冊中心。以下為ScheduledTaskRegistrar部分源碼,主要說明該類中包含的屬性。Spring容器通過線程處理注冊的定時任務(wù)。
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);
......
}
ScheduledTaskRegistrar類中在處理定時任務(wù)時會調(diào)用scheduleCronTask方法初始化定時任務(wù)。
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);
}
在ThreadPoolTaskShcedule這個類中,進(jìn)行線程池的初始化。在創(chuàng)建線程池時會創(chuàng)建 DelayedWorkQueue()阻塞隊(duì)列,定時任務(wù)會被提交到線程池,由線程池進(jìn)行相關(guān)的操作,線程池初始化大小為1。當(dāng)有多個線程需要執(zhí)行時,是需要進(jìn)行任務(wù)等待的,前面的任務(wù)執(zhí)行完了才可以進(jìn)行后面任務(wù)的執(zhí)行。
@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;
}
根本原因,jvm啟動之后會記錄系統(tǒng)時間,然后jvm根據(jù)CPU ticks自己來算時間,此時獲取的是定時任務(wù)的基準(zhǔn)時間。如果此時將系統(tǒng)時間進(jìn)行了修改,當(dāng)Spring將之前獲取的基準(zhǔn)時間與當(dāng)下獲取的系統(tǒng)時間進(jìn)行比對時,就會造成Spring內(nèi)部定時任務(wù)失效。因?yàn)榇藭r系統(tǒng)時間發(fā)生變化了,不會觸發(fā)定時任務(wù)。
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;
}
}
2.解析流程圖

3.使用新的方法
為了避免使用@Scheduled注解,在修改服務(wù)器時間導(dǎo)致定時任務(wù)不執(zhí)行情況的發(fā)生。在項(xiàng)目中需要使用定時任務(wù)場景的情況下,使ScheduledThreadPoolExecutor進(jìn)行替代,它任務(wù)的調(diào)度是基于相對時間的,原因是它在任務(wù)的內(nèi)部 存儲了該任務(wù)距離下次調(diào)度還需要的時間(使用的是基于 System.nanoTime實(shí)現(xiàn)的相對時間 ,不會因?yàn)橄到y(tǒng)時間改變而改變,如距離下次執(zhí)行還有10秒,不會因?yàn)閷⑾到y(tǒng)時間調(diào)前6秒而變成4秒后執(zhí)行)。
schedule定時任務(wù)修改表達(dá)式無效
真是鬼了。 就那么個cron表達(dá)式,難道還能錯了。
對了無數(shù)遍,cron表達(dá)式?jīng)]問題。 但就是無效。
擴(kuò)展下思路,有沒有用到zookeeper,zookeeper是會緩存配置信息的。
看了下,果然是緩存了。 清空后,重啟項(xiàng)目有效了。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
- spring @Scheduled注解的使用誤區(qū)及解決
- spring task @Scheduled注解各參數(shù)的用法
- java使用@Scheduled注解執(zhí)行定時任務(wù)
- SpringBoot中使用@Scheduled注解創(chuàng)建定時任務(wù)的實(shí)現(xiàn)
- spring-boot通過@Scheduled配置定時任務(wù)及定時任務(wù)@Scheduled注解的方法
- 詳解在Spring3中使用注解(@Scheduled)創(chuàng)建計(jì)劃任務(wù)
- @Scheduled注解不能同時執(zhí)行多個定時任務(wù)的解決方案
相關(guān)文章
通過@Resource注解實(shí)現(xiàn)屬性裝配代碼詳解
這篇文章主要介紹了通過@Resource注解實(shí)現(xiàn)屬性裝配代碼詳解,具有一定借鑒價值,需要的朋友可以參考下2018-01-01
java swing實(shí)現(xiàn)的掃雷游戲及改進(jìn)版完整示例
這篇文章主要介紹了java swing實(shí)現(xiàn)的掃雷游戲及改進(jìn)版,結(jié)合完整實(shí)例形式對比分析了java使用swing框架實(shí)現(xiàn)掃雷游戲功能與相關(guān)操作技巧,需要的朋友可以參考下2017-12-12
Java數(shù)據(jù)結(jié)構(gòu)徹底理解關(guān)于KMP算法
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)關(guān)于KMP算法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09
Java 入門圖形用戶界面設(shè)計(jì)之列表框JList
圖形界面(簡稱GUI)是指采用圖形方式顯示的計(jì)算機(jī)操作用戶界面。與早期計(jì)算機(jī)使用的命令行界面相比,圖形界面對于用戶來說在視覺上更易于接受,本篇精講Java語言中關(guān)于圖形用戶界面的列表框JList2022-02-02

