Spring定时任务的秘密

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秒执行。

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

posted @   别来无恙✲  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示