Spring定时任务的秘密

在 Spring 框架中,定时任务主要通过 @Scheduled 注解或 TaskScheduler 接口实现。

1.基本使用

在 Spring Boot 项目中,通过 @EnableScheduling 注解启用定时任务功能:

@SpringBootApplication
@MapperScan("com.feng.tackle.dao")
@EnableScheduling
public class DateApplication {
public static void main(String[] args) {
SpringApplication.run(DateApplication.class, args);
}
}

然后在spring的组件中,使用 @Scheduled 注解标记任务方法

@Component
public class MyScheduler {
// 固定频率(每隔 5 秒执行一次)
@Scheduled(fixedRate = 5000)
public void task1() {
// 业务逻辑
} // 固定延迟(任务结束后延迟 3 秒再执行)
@Scheduled(fixedDelay = 3000)
public void task2() {
// 业务逻辑
} // Cron 表达式(每天 12 点执行)
@Scheduled(cron = "0 0 12 * * ?")
public void task3() {
// 业务逻辑
}
}

其实用起来特别地简单。

2. 原理解析

核心部分:

  1. @EnableScheduling

    入口注解,触发 SchedulingConfiguration 配置类,注册核心后置处理器 ScheduledAnnotationBeanPostProcessor
  2. ScheduledAnnotationBeanPostProcessor

    负责扫描 Bean 中的 @Scheduled 注解方法,解析并注册定时任务。
  3. TaskScheduler

    任务调度接口,默认实现为 ThreadPoolTaskScheduler(基于 ScheduledExecutorService)。
  4. ScheduledTaskRegistrar

    任务注册中心,管理所有定时任务的注册和执行。

一、构建任务

@EnableScheduling,在启动类上面加了这么一个注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class) //============
@Documented
public @interface EnableScheduling {
}

看过SpringBoot原理分析的都知道,@Import(SchedulingConfiguration.class)这个就是突破口了嘛。

继续往下

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration { @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor(); // 往容器里面放进了这样一个bean
} }

想必那个类就是核心了,ScheduledAnnotationBeanPostProcessor源码中最开头的英文的翻译如下

Bean 后处理器,它注册带有 @Scheduled 注释的方法,以便由TaskScheduler根据通过 Comments 提供的“fixedRate”、“fixedDelay”或“cron”表达式调用。这个后处理器由 Spring 的 <task:annotation-driven> XML 元素以及 @EnableScheduling 注释自动注册。自动检测容器中的任何 SchedulingConfigurer 实例,允许自定义要使用的调度器或对任务注册进行精细控制(例如,注册 Trigger 任务)

既然是xxxBeanPostProcessor了,那么肯定是实现了BeanPostProcessor接口,重写了postProcessAfterInitialization方法

下面来解析这个实现类的具体方法

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 忽略 AOP 基础设施类(如 ScopedProxy)和任务调度器自身
if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
bean instanceof ScheduledExecutorService) {
return bean;
} Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (!this.nonAnnotatedClasses.contains(targetClass) &&
AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
// 返回 Map<Method, Set<Scheduled>>,键为方法对象,值为该方法上的所有 @Scheduled 注解集合。
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
});
// 如果没有
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(targetClass); //将不包含 @Scheduled 注解的类加入缓存,后续跳过扫描以提高性能。
if (logger.isTraceEnabled()) {
logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
}
}
else { // 如果有-------------------------------
annotatedMethods.forEach((method, scheduledAnnotations) ->
scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
if (logger.isTraceEnabled()) {
logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
"': " + annotatedMethods);
}
}
}
return bean;
}

经过上面的大致分析,发现else里面,才是我们想看的,各位也可以打断点调试。

annotatedMethods.forEach(
(method, scheduledAnnotations) ->
scheduledAnnotations.forEach(
scheduled -> processScheduled(scheduled, method, bean)
)
);

processScheduled()方法,构建任务。

private final ScheduledTaskRegistrar registrar;
private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16); protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
....
// 可以看出来了把,任务其实就是个runnable
Runnable runnable = createRunnable(bean, method);
.....
Set<ScheduledTask> tasks = new LinkedHashSet<>(4);
// 检查注解的延迟属性
long initialDelay = convertToMillis(scheduled.initialDelay(), scheduled.timeUnit());
.....
//cron表达式
String cron = scheduled.cron();
.....
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
....
// 检查周期属性
long fixedDelay = convertToMillis(scheduled.fixedDelay(), scheduled.timeUnit());
...
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
... String fixedDelayString = scheduled.fixedDelayString();
..........
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
.........
// Finally register the scheduled tasks
synchronized (this.scheduledTasks) {
Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
regTasks.addAll(tasks); // 全加进去咯
}
.....
}

二、自动配置

熟悉springboot自动配置原理的都知道,spring-boot-autoconfigure里面官方定义了超级多的场景。我们去找找看。

一下子就找到了 task目录下面的TaskSchedulingAutoConfiguration. 【默认是单线程的

@ConditionalOnClass(ThreadPoolTaskScheduler.class)
@AutoConfiguration(after = TaskExecutionAutoConfiguration.class)
@EnableConfigurationProperties(TaskSchedulingProperties.class)
public class TaskSchedulingAutoConfiguration { @Bean
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
// 如果容器中有我们自定义的这种bean,这个就失效了,@ConditionalOnMissingBean注解的作用
@ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { // 下面构建的bean,拿来这里用了
return builder.build();
} @Bean
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() {
return new ScheduledBeanLazyInitializationExcludeFilter();
} @Bean
// 如果容器中有我们自定义的这种bean,这个就失效了,@ConditionalOnMissingBean注解的作用
@ConditionalOnMissingBean
public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, // 从properties读取的
ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
builder = builder.poolSize(properties.getPool().getSize()); // 发现这里是 size = 1, 默认是单线程的!!!!!
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(taskSchedulerCustomizers);
return builder;
}
}
@ConfigurationProperties("spring.task.scheduling") // 说明我们可以根据这个配置来设置线程数那些参数
public class TaskSchedulingProperties { }

三、任务调度

public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler { @Nullable
private ScheduledExecutorService scheduledExecutor; // 内部持有一个线程池!!!! initializeExecutor()方法会初始化它
.........
// 主要看这个方法
@Override
@Nullable
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
ScheduledExecutorService executor = getScheduledExecutor(); // 先得到executor
try {
ErrorHandler errorHandler = this.errorHandler;
if (errorHandler == null) {
errorHandler = TaskUtils.getDefaultErrorHandler(true);
}
// 执行这一个
return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule();
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}
..........
}

3.案例分析

①: 什么都不配置,单线程

@Slf4j
@Component
public class TestJob {
@Scheduled(cron = "1-59 * * * * ?")
public void hello() {
try {
log.info("hello---任务开始");
TimeUnit.SECONDS.sleep(5);
log.info("hello---任务------------结束hello");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} }
@Scheduled(cron = "1-59 * * * * ?")
public void world() {
try {
log.info("world---任务开始");
TimeUnit.SECONDS.sleep(2);
log.info("world---任务--------------结束world");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

运行结果

world任务,两个之间间隔了7秒钟。!!! 同理,两个hello()任务也间隔了七秒钟。也就是说,任务之间互相影响了。【因为是单线程的嘛】

②:配置调度线程是多线程的

@Configuration
public class SchedulerConfig {
// 配置多线程的调度器, 默认是单线程
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置调度线程池大小
scheduler.setThreadNamePrefix("ScheduledTask-");
return scheduler;
}
}

然后任务如下

@Scheduled(cron = "20/40 * * * * ? ") // 从每分钟的20秒开始,每40秒执行一次,----  xx:xx:20  xx:xx+1:20...
public void hello() {
try {
log.info("hello---任务开始");
TimeUnit.SECONDS.sleep(20);
log.info("hello---任务结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Scheduled(cron = "30/20 * * * * ? ") // 从每分钟的30开始,每20秒执行一次,---- xx:xx:30 xx:xx:50 xx:xx+1:30 xx:xx+1:50...
public void world() {
try {
log.info("world---任务开始");
TimeUnit.SECONDS.sleep(10);
log.info("world---任务结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

每一分钟内来看,应该是hello()在第20秒先执行,耗时20秒,在40秒结束, world()在第三十秒执行,耗时10秒,在40秒结束。 如果被阻塞的话,world()任务将会在第40秒执行。

但是看执行效果,发现并没有被阻塞,二者是按规定正常执行的,并没有互相影响。故我们的配置生效了,现在定时任务调度是多线程的。【图中也可以看到 执行的线程名字不一样】

Spring定时任务的秘密的更多相关文章

  1. 摆脱Spring 定时任务的@Scheduled cron表达式的困扰

    一.背景 最近因为需要,需要适用Spring的task定时任务进行跑定时任务,以前也接触过,但是因为懒没有好好地理解@Scheduled的cron表达式,这次便对它做了一个全方位的了解和任务,记录下来 ...

  2. spring 定时任务配置

    1.(易)如何在spring中配置定时任务? spring的定时任务配置分为三个步骤: 1.定义任务 2.任务执行策略配置 3.启动任务 (程序中一般我们都是到过写的,直观些) 1.定义任务 < ...

  3. Spring 定时任务2

    转载自http://www.cnblogs.com/nick-huang/p/4864737.html > 版本说明 <dependencies> <dependency> ...

  4. 关于Spring定时任务(定时器)用法

    Spring定时任务的几种实现 Spring定时任务的几种实现 一.分类 从实现的技术上来分类,目前主要有三种技术(或者说有三种产品): 从作业类的继承方式来讲,可以分为两类: 从任务调度的触发时机来 ...

  5. Cron和Spring定时任务

    1.Java Spring spring定时任务cronExpression的值(配置定时时间)格式说明: 一个cronExpression表达式有至少6个(也可能是7个)由空格分隔的时间元素.从左至 ...

  6. spring 定时任务的 执行时间设置规则(转)

     spring 定时任务的 执行时间设置规则 单纯针对时间的设置规则org.springframework.scheduling.quartz.CronTriggerBean允许你更精确地控制任务的运 ...

  7. (3)Spring定时任务的几种实现

    Spring定时任务的几种实现 近日项目开发中需要执行一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息,借此机会整理了一下定时任务的几种实现方式,由于项目采用spring框架,所以我都将 ...

  8. Spring定时任务,Spring4整合quartz2.2,quartz-scheduler定时任务

    Spring4整合quartz2.2,quartz-scheduler定时任务,Spring定时任务 >>>>>>>>>>>>& ...

  9. spring计划任务,springMvc计划任务,Spring@Scheduled,spring定时任务

    spring计划任务,springMvc计划任务,Spring@Scheduled,spring定时任务 >>>>>>>>>>>> ...

  10. spring定时任务的几种实现方式

    Spring定时任务的几种实现 近日项目开发中需要执行一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息,借此机会整理了一下定时任务的几种实现方式,由于项目采用spring框架,所以我都将 ...

随机推荐

  1. 基于开源IM即时通讯框架MobileIMSDK:RainbowChat v11.5版已发布

    关于MobileIMSDK MobileIMSDK 是一套专门为移动端开发的开源IM即时通讯框架,超轻量级.高度提炼,一套API优雅支持UDP .TCP .WebSocket 三种协议,支持iOS.A ...

  2. 【狂神说Java】Java零基础学习笔记-Java数组

    [狂神说Java]Java零基础学习笔记-Java数组 Java数组01:数组的定义 数组是相同类型数据的有序集合. 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成. 其中,每一个数 ...

  3. 注册中心如何选型?Eureka、Zookeeper、Nacos怎么选

    这是小卷对分布式系统架构学习的第9篇文章,第8篇时只回答了注册中心的工作原理的内容,面试官的第二个问题还没回答,今天再来讲讲各个注册中心的原理,以及区别,最后如何进行选型 上一篇文章:如何设计一个注册 ...

  4. C#数据结构与算法入门实战指南

    前言 在编程领域,数据结构与算法是构建高效.可靠和可扩展软件系统的基石.它们对于提升程序性能.优化资源利用以及解决复杂问题具有至关重要的作用.今天大姚分享一些非常不错的C#数据结构与算法实战教程,希望 ...

  5. Superset 用户集成完整方案(iframe方式)

    本次集成方案经过个人测试,根据前面2个集成方案的资料,撰写,相关说明由于个人知识水平有限不一定理解准确,有错误的地方环境评论区评论: 1.用户集成方式: A系统用户,通过A的某个界面,iframe嵌入 ...

  6. Billyboss pg walkthough Intermediate window

    nmap ┌──(root㉿kali)-[/home/ftpuserr/nc.exe] └─# nmap -p- -A -sS 192.168.219.61 Starting Nmap 7.94SVN ...

  7. 喜讯!天翼云斩获NLP国际顶会比赛两项荣誉

    近日,NLP国际顶会ACL(The Association for Computational Linguistics)进行的国际赛事WASSA 2023(13th Workshop on Compu ...

  8. 文本处理命令head tail more less tr cut paste wc

    文本处理命令 命令**head tail more less tr cut paste wc** 磁盘分区利用率 df|tr -s ' ' :|cut -d : -f5 df|tr -s ' ' :| ...

  9. linux mint安装Scala

    Scala由java编写,需要前期安装jdk 面向函数式编程 1.下载 Scala 二进制包2.11.8 http://www.scala-lang.org/downloads 解压到/usr/loc ...

  10. Luogu P7077 CSP-S2020 函数调用 题解 [ 蓝 ] [ 拓扑排序 ] [ 动态规划 ] [ 数学 ]

    函数调用:个人非常喜欢的一道拓扑题. 转化 这题一共有三种操作,不太好搞.而第一个函数看起来就比较可做,第三个函数显然就是让你拓扑转移,于是我们考虑第二个操作怎么处理. 当我们进行一个操作一后,假设当 ...