@Scheduled源码解析 | 小册免费学

518 阅读5分钟

JDK定时任务的实现:

`public class JDKTask {

public static void main(String[] args) {

    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("nihao shijie");
        }
    },1,5000);
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
    executorService.scheduleAtFixedRate(()-> System.out.println("我是大帅哥"),5,5, TimeUnit.SECONDS);
}

}`

定时任务池ScheduledThreadPool会不断的、定时的执行提交的任务。 且命名方式基本上都叫scheduleXxx()

Spring是如何实习那@Schedule的?

看源码后可以发现,@Scheduled注解是由ScheduledAnnotationBeanPostProcessor后置处理器处理的。

当PostProcessor负责注册哪些加了@Scheduled注解的方法,然后根据@Scheduled注解的fixedRate、fixedDeley等属性,分别交给TaskScheduler定时任务线程池执行。

两个问题:

这个后置处理器如何被加入Spring容器的? @Scheduled标注的方法是如何被TaskScheduler定时任务线程池处理的?

两种方式:

使用XML形式的task:annotation-driven标签或@EnableScheduling导入到Spring容器。

@EnableScheduling:

在@EnableScheduling导入了一个配置类: 在SchedulingConfigration中通过@Bean的方式把ScheduledAnnotationBeanPostProcessor加入到Spring容器中。

@Scheduled标注的方法是如何被定时执行的?

分两步:

  • 收集@Scheduled方法
  • 执行定时任务

收集@Scheduled方法:

每个Bean在注入Spring容器时,会经过@EnableScheduling导入的 ScheduledAnnotationBeanPostProcessor的postProcessAfterInitiallization方法,遍历类中的每一个方法,收集带有@Scheduled注解的方法。然后把收集到的方法forEach调用本类中额的processScheduled方法,他会根据每个定时任务@Scheduled注解中的fixedRate、fixedDelay以及cron属性,分别把每个任务存储在自己成员变量ScheduledTaskRegistrar的不同TaskList中。

至此所有@Scheduled方法都已经被包装成Task根据@Scheduled的注解不同存储到不同的任务列表中了,那么这些任务怎么被定时执行的呢?

执行定时任务

使用jdk的ScheduleThreadPool执行定时任务时,都存在一个提交任务的动作(scheduleXxx),Spring的底层不例外,也是使用线程池的。 我们可以看到后处理器实现了ApplicationListener接口的onApplicationEvent方法,所以在整个Spring容器启动完毕后,最终会调用其中的方法finishRegistration(),在所有定时任务注册完毕后做一些事情。

你会发现在这个方法里会调用收集了定时任务列表的ScheduledTaskRegistrar的afterPropertiesSet方法,接下来就是registrar把任务组个提交给taskScheduler线程池即可。afterPropertiesSet调用了scheduleTasks,这个方法会把每个任务提交给线程池执行。

在这个方法里可以看到当taskscheduler为空时,会默认赋值一个单线程的定时任务池给它。

addScheduledTask()内部会再次调用scheduleCronTask,把任务提交给线程池。第一次是收集任务列表进registrar时,此时的线程池对象不为空,就会把任务交给线程池,并执行

所以SpringBoot的定时任务池应该如何去配置呢?

我们之前直接配置了一个线程池,并在定时任务上加上了@Async,实际上这并不是一个好的选择 其实我们自己配置的线程池并未赋值给taskScheduler

但为什么效果上,像是被赋值了呢? 确实走了我们自定义的线程池的线程,但这是因为我们在方法上加入@Async,在执行这个方法时会被异步线程调用,但此时定时任务的线程池还是单线程,只不过这个方法在执行时,因为加了@Async,所以被异步线程调用,SpringBoot的定时任务只需要把每个任务推到异步线程池就可以执行下一个任务了,时间很短,看起来好像不是串行的。

怎样替换默认的SingleThreadScheduledExecutor呢?

回到finishRegistration方法

` private void finishRegistration() {

if (this.scheduler != null) {
    this.registrar.setScheduler(this.scheduler);
}
 // 1 --- 查找是否有SchedulingConfigurer类型的自定义的bean(后面解释)
if (this.beanFactory instanceof ListableBeanFactory) {
    Map<String, SchedulingConfigurer> beans =
            ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
    List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
    AnnotationAwareOrderComparator.sort(configurers);
    for (SchedulingConfigurer configurer : configurers) {
        configurer.configureTasks(this.registrar);
    }
}

// 2 --- 如果经过上一步registrar中的taskScheduler仍然未被赋值
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
    Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
    // 2.1 --- 查找Spring容器中TaskScheduler类型的Bean
    try {
        this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
    }
    // 2.2 --- 存在多个TaskScheduler类型的Bean
    catch (NoUniqueBeanDefinitionException ex) {
        logger.debug("Could not find unique TaskScheduler bean", ex);
        // 2.1.2 --- 由于存在多个,这次改为按名字来确定,DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
        try {
            this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
        }
        // 2.1.3 --- 找不到名为"taskScheduler"的Bean,抛异常
        catch (NoSuchBeanDefinitionException ex2) {
            // 注意下面的异常信息,解释得很清楚了,甚至告诉我们如何自定义定时任务线程池
            if (logger.isInfoEnabled()) {
                logger.info("More than one TaskScheduler bean exists within the context, and " +
                        "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
                        "(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
                        "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
                        ex.getBeanNamesFound());
            }
        }
    }
    // 2.3 --- 不存在TaskScheduler类型的Bean
    catch (NoSuchBeanDefinitionException ex) {
        logger.debug("Could not find default TaskScheduler bean", ex);
        // 2.3.1 --- 查找Spring容器中ScheduledExecutorService类型的Bean
        try {
            this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
        }
        // 2.3.2 ---  存在多个ScheduledExecutorService类型的Bean
        catch (NoUniqueBeanDefinitionException ex2) {
            logger.debug("Could not find unique ScheduledExecutorService bean", ex2);
            // 2.3.2 --- 由于存在多个,这次改为按名字来确定,DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
            try {
                this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
            }
            // 2.3.3 --- 找不到名为"taskScheduler"的Bean,抛异常
            catch (NoSuchBeanDefinitionException ex3) {
                // 注意下面的异常信息,解释得很清楚了,甚至告诉我们如何自定义定时任务线程池
                if (logger.isInfoEnabled()) {
                    logger.info("More than one ScheduledExecutorService bean exists within the context, and " +
                            "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
                            "(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
                            "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
                            ex2.getBeanNamesFound());
                }
            }
        }
        catch (NoSuchBeanDefinitionException ex2) {
            logger.debug("Could not find default ScheduledExecutorService bean", ex2);
            // Giving up -> falling back to default scheduler within the registrar...
            // 翻译:放弃,沿用registrar内部默认的scheduler(单线程)
            logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
        }
    }
}

this.registrar.afterPropertiesSet();

}`

从源码上可以看出来SpringBoot对外暴露了三种自定义定时任务池的方法:

如果存在SchedulingConfigurer类型的Bean,用SchedulingConfigurer 如果存在TaskSheduler类型的Bean,用TaskScheduler 如果存在ScheduledExecutorService的Bean,用ScheduledExecutorService 以上都没有用默认的 上面的任意一种定时任务线程池都会调用ScheduledTaskRegistrar#setScheduler为taskScheduler赋值。

配置定时任务线程池的三种方式:

方式一:重写SchedulingConfigurer#configuerTasks

@Component @Slf4j public class TaskConfigurer implements SchedulingConfigurer {

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setPoolSize(3);
    taskScheduler.setThreadNamePrefix("schedule-task-");
    taskScheduler.setRejectedExecutionHandler(
            new RejectedExecutionHandler() {
                /**
                 * 自定义线程池拒绝策略(模拟发送告警邮件)
                 */
                @Override
                public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                    log.info("发送告警邮件======>:嘿沙雕,线上定时任务卡爆了, 当前线程名称为:{}, 当前线程池队列长度为:{}",
                            r.toString(),
                            executor.getQueue().size());
                }
            });
    taskScheduler.initialize();
    taskRegistrar.setScheduler(taskScheduler);
}

}

方式二:@Bean + ThreadPoolTaskScheduler

方式三:@Bean + ScheduledThreadPoolExecutor

@Configuration @EnableAsync @Slf4j public class ThreadPoolTaskConfig {

@Bean("taskScheduler")
public Executor getExecutor(){
    ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();

// ThreadPoolTaskScheduler executor = new ScheduledThreadPoolTaskExecutor(); executor.setPoolSize(3); executor.setThreadNamePrefix("executor-task-");

    // 线程池对拒绝任务的处理策略
    executor.setRejectedExecutionHandler((r, executor1) -> {
        log.info("发送告警邮件======>:嘿沙雕,线上定时任务卡爆了, 当前线程名称为:{}, 当前线程池队列长度为:{}",
                r.toString(),
                executor1.getQueue().size());
    });
    // 初始化
    executor.initialize();
    return executor;
}

}

方式1的另一个作用可以实现动态配置定时任务的时间 详情可参考:mbcoder.com/dynamic-tas…

其他公众号上看到的动态配置:mp.weixin.qq.com/s/lFUReSuVo…

通过配置文件改变定时任务线程数 自定义线程池好处,除了可以改变线程数还重写了拒绝策略等,如果仅仅想要增加定时任务线程池的线程数,可以直接在配置文件中更改

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情