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及开放平台技术实践

    本文由携程技术Jim分享,原题"日访问过亿,办公IM及开放式平台在携程的实践",下文进行了排版和内容优化. 1.引言 携程内部的办公IM项目最早在2016年立项,经历了初期简单办公 ...

  2. 百度统一socket长连接组件从0到1的技术实践

    本文由百度消息中台团队分享,引用自百度Geek说,原题"百度iOS端长连接组件建设及应用实践",为了帮助理解,本文有修订和改动. 1.引言 在过去的十年里,移动端互联网技术飞速发展 ...

  3. 一套十万级TPS的IM综合消息系统的架构实践与思考

    本文由作者jhon_11分享,有大量修订和改动. 1.引言 如何设计一款高性能.高并发.高可用的im综合消息平台是很多公司发展过程中会碰到且必须要解决的问题.比如一家公司内部的通讯系统.各个互联网平台 ...

  4. clip-retrieval检索本地数据集

    clip-retrieval检索本地数据集 from clip_retrieval.clip_client import ClipClient, Modality from tqdm import t ...

  5. Java验证邮箱是否有用的实现与解析

    在现代互联网应用中,邮箱验证是一个常见的需求.通过邮箱验证,开发者可以确保用户提供的邮箱地址是有效的,从而在后续的操作中,如密码重置.通知发送等,依赖这些有效的邮箱地址.本文将详细介绍如何使用Java ...

  6. Elasticsearch(5) --- 基本命令(集群相关命令、索引CRUD命令、文档CRUD命令)

    这篇博客的命令分为ES集群相关命令,索引CRUD命令,文档CRUD命令.这里不包括Query查询命令,它单独写一篇博客. 一.ES集群相关命令 ES集群相关命令主要是_cat命令,所以这里详细讲解下该 ...

  7. Chrony:让你的服务器时间精准到微秒级的神器!

    在现代计算机系统中,时间同步是至关重要的.无论是分布式系统.数据库集群,还是日志记录,时间不一致都可能导致严重的问题.而 Chrony,作为一款高性能的时间同步工具,正在成为越来越多系统管理员的首选. ...

  8. refs转发

    ref 转发不但可以转发指向具体的dom组件,也可以指向class组件的实例 import React from 'react' import ReactDOM from 'react-dom'; / ...

  9. Kotlin:【接口】【抽象类】

  10. VulNyx - Responder靶场

    靶机ip 192.168.200.9 先nmap 扫描全端口 这个22端口不知道有没有开 被过滤了 我们 收集一下靶机的ipv6地址 nmap用ipv6地址扫他的端口就能绕过 他的端口过滤 ping6 ...