我的编程空间,编程开发者的网络收藏夹
学习永远不晚

我劝你谨慎使用Spring中的@Scheduled注解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

我劝你谨慎使用Spring中的@Scheduled注解

引言

在一些业务场景中需要执行定时操作来完成一些周期性的任务,比如每隔一周删除一周前的某些历史数据以及定时进行某项检测任务等等。

在日常开发中比较简单的实现方式就是使用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 {
 
 String cron() default "";
 
 String zone() default "";
 
 long fixedDelay() default -1;
 
 String fixedDelayString() default "";
 
 long fixedRate() default -1;
 
 String fixedRateString() default "";
 
 long initialDelay() default -1;
 
 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注解标注的方法。


 @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;
 }

再往下继续看,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());
  }
 }

满足条件时将定时任务添加到定时任务列表中,在加入任务列表的同时对定时任务进行注册。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);
 
 ......
}

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);
 }

在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;
 }

根本原因,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;
  }
 }

2.解析流程图

这里写图片描述

3.使用新的方法

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

schedule定时任务修改表达式无效

真是鬼了。 就那么个cron表达式,难道还能错了。

对了无数遍,cron表达式没问题。 但就是无效。

扩展下思路,有没有用到zookeeper,zookeeper是会缓存配置信息的。

看了下,果然是缓存了。 清空后,重启项目有效了。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

我劝你谨慎使用Spring中的@Scheduled注解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Spring Security中的权限注解如何使用

今天小编给大家分享一下Spring Security中的权限注解如何使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。Spr
2023-06-30

@ModelAttribute注解在spring mvc中的使用方法

@ModelAttribute注解在spring mvc中的使用方法?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。在spring mvc中非常重要的注解@Mod
2023-05-31

Spring中Transactional注解使用的心得(推荐)

这篇文章主要介绍了Spring中Transactional注解使用的心得,事务是用来控制数据的ACID特性的,用于保证数据的正确性和完整性,需要的朋友可以参考下
2022-11-13

如何使用Spring中的scope配置和@scope注解

本文小编为大家详细介绍“如何使用Spring中的scope配置和@scope注解”,内容详细,步骤清晰,细节处理妥当,希望这篇“如何使用Spring中的scope配置和@scope注解”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一
2023-07-02

一文带你了解SpringBoot中常用注解的原理和使用

这篇文章主要介绍了一文带你了解SpringBoot中常用注解的原理和使用
2022-11-13

在Spring Boot项目中如何实现使用 Mybatis中的@ Annotation注解

在Spring Boot项目中如何实现使用 Mybatis中的@ Annotation注解?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一、运行 springboot-myba
2023-05-31

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录