springboot定时任务源码解析

我们在开发的时候经常会用到 @Scheduled 这个注解,通过这个注解我们可以使用spring自带的轻量的定时任务功能。我们还可以自行扩展,来实现动态增加定时任务、取消任务,配置线程池大小等等一系列需求。

1. 分析源码

从哪里开始看呢?

首先,我们使用spring自带定时任务的时候会加上 @EnableScheduling 注解,只有使用了这个注解之后,定时任务才会生效。我们就从这里开始。

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

}

可以看到这个注解会导入 SchedulingConfiguration 配置,然后我们进入SchedulingConfiguration 这个类,它new了一个ScheduledAnnotationBeanPostProcessor这个对象,并交给spring托管,我们再进入ScheduledAnnotationBeanPostProcessor这个列,可以发现它实现了几个和spring上下文、bean生命周期相关的接口。其中比较重要的是继承自DestructionAwareBeanPostProcessor,继承自BeanPostProcessor,spring会针对每一个需要放入上下文的bean执行它的前置处理方法(postProcessBeforeInitialization)、后置处理方法(postProcessAfterInitialization)。并且还通过实现ApplicationListener<ContextRefreshedEvent>,订阅了spring上下文刷新的事件。

我们先看BeanPostProcessor的实现,其中postProcessBeforeInitialization方法没有做任何处理,直接将bean返回。再来看postProcessAfterInitialization方法。当有一个bean被spring创建时,此方法会被调用,首先通过ultimateTargetClass方法获取它的字节码对象(如果是cglib代理的,就获取它原本的字节码),然后判断这个字节码对象是否存在于nonAnnotatedClasses这个集合,如果存在于这个集合,就直接返回bean结束,如果不存在,进入下一步。nonAnnotatedClasses这个集合一开始为空。如果不存在于这个集合,这里会搜索这个class对象中所有被@Schduled修饰的所有方法,将搜索结果封装为一个map,key为方法的method对象,value为@Schduled注解实例集合。

为什么这里的类型是 Map<Method, Set<Scheduled>> 而不是 Map<Method, Scheduled> ? 因为一个方法是可以被多个@Schduled注解修饰的。关于查找的工具类 MethodIntrospector.selectMethods的方法,这里不展开了。

得到当前对象被@Schduled注解修饰的方法集合(annotatedMethods)后,进入下一步,如果 annotatedMethods 这个集合为空,就把它加入未被修饰的字节码对象集合nonAnnotatedClasses。如果 annotatedMethods 不为空,遍历 annotatedMethods 中被注解的方法进行定时任务的处理。通过 processScheduled 方法处理被@Schduled注解修饰的方法,先将Mehtod封装为 Runable(ScheduledMethodRunnable ),它的run方法其实就是通过反射调用method的invoke方法来执行我们的业务方法。然后再将这个Runable对象委托给ScheduledTaskRegistrar去调度执行,比如cron表达式的定时任务通过ScheduledTaskRegistrar.scheduleCronTask()方法完成调度,ScheduledTaskRegistrar.unresolvedTasks这个集合里面放了未进行处理的任务,此时ScheduledTaskRegistrar.taskScheduler为null,所以先暂时放入unresolvedTasks这个集合。

到这里postProcessAfterInitialization方法就结束了。

接着,等spring容器初始化完成,发布事件,会调用onApplicationEvent方法,这个方法用来结束定时任务的注册,主要用来将所有还未进行调度的定时任务(unresolvedTasks集合)调度起来。

我们进入finishRegistration()方法。首先获取上下文中所有实现了SchedulingConfigurer接口的bean实例,然后进行排序,随后一次调用这些bean的configureTasks方法,对当前的ScheduledTaskRegistrar进行配置。ScheduledTaskRegistrar类是定时任务的核心底层类,我们可以通过这个类来进行动态定时任务的配置、线程池的的配置,如果你需要这么做,你可以创建一个bean实现SchedulingConfigurer接口,通过这个接口的configureTasks方法可以操作ScheduledTaskRegistrar。

下面是例子:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

@Configuration
public class MyConfig implements SchedulingConfigurer {

    private ScheduledTaskRegistrar scheduledTaskRegistrar;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        this.scheduledTaskRegistrar = taskRegistrar;
        //配置一个线程池
        taskRegistrar.setScheduler(taskScheduler());
    }
    /**
     * 运行期间动态增加一个cron任务
     * @param runnable
     * @param expression
     * @return
     */
    public ScheduledTask addCronTask(Runnable runnable, String expression){
        CronTask cronTask = new CronTask(runnable, expression);

        //这一步其实没什么意义,因为spring会把所有添加过的任务放在triggerTasks、cronTasks、fixedRateTasks、fixedDelayTasks这四个集合里
        //这一步的纯粹是为了遵循他的逻辑,没有这一步,也可以调度任务
        scheduledTaskRegistrar.addCronTask(cronTask);

        //这一步才是将任务调度起来
        return scheduledTaskRegistrar.scheduleCronTask(cronTask);
    }

    /**
     * 取消cron任务
     * @param scheduledTask
     */
    public void cancelTask(ScheduledTask scheduledTask){
        scheduledTask.cancel();
    }

    @Bean(destroyMethod = "shutdown")
    public ScheduledExecutorService taskScheduler() {
        return Executors.newScheduledThreadPool(42);
    }
}

说一下关于取消任务,这里其实有个不足之处,ScheduledTaskRegistrar并没有提供相关的方法让我们判断要取消的ScheduledTask是否在spring定时任务管理范围之内。ScheduledTaskRegistrar为我们提供了编程式添加Task的方法。

再说一下ScheduledTaskRegistrar这个类,其中的4个List类型的成员变量triggerTasks、cronTasks、fixedRateTasks、fixedDelayTasks,这些集合存放的所有被添加过的任务,unresolvedTasks中存放的是所有未开始调度的任务,scheduledTasks中存放的是所有已经开始调度的任务。ScheduledTaskRegistrar调度的核心方法是scheduleTasks()。首先判断TaskScheduler(任务调度器)是否为null,如果为null就创建一个ConcurrentTaskScheduler并给他分配一个单线程的线程池。然后遍历triggerTasks、cronTasks、fixedRateTasks、fixedDelayTasks,将这里的所有任务都调度一次。此方法如果被多次调用,会出现任务执行多次的情况,spring默认在容器初始化完成调用一次。

再回到 ScheduledAnnotationBeanPostProcessor.finishRegistration()方法。处理完所有的SchedulingConfigurer实现类之后,判断当前的任务注册类ScheduledTaskRegistrar中是否有任务,如果有,并且当前的TaskScheduler(调度器)还未进行初始化,这里就会去spring上下文中去寻找是否有TaskScheduler类型的bean,如果有唯一的一个,直接配置;如果有多个,判断是否有name为taskScheduler的TaskScheduler类型的bean,如果有就拿到这个去配置。如果没有TaskScheduler类型的bean,则去寻找ScheduledExecutorService这个类型的bean,逻辑还是一样,如果找到了,并且只有一个,就直接配置上去;如果有多个,判断是否有名字为taskScheduler的ScheduledExecutorService这个类型的bean,如果有,也配置上去。其他情况,一律什么也不配置。所以如果我们如果只要进行线程池的配置,最简单的做法是,创建一个ScheduledExecutorService类型的bean,另外,为了兼容(可能别的框架、jar包也会在spring中注册ScheduledExecutorService类型的bean),最佳的做法是:

    @Bean(name = "taskScheduler",destroyMethod = "shutdown")
    public ScheduledExecutorService taskScheduler() {
        return Executors.newScheduledThreadPool(42);
    }

还是回到ScheduledAnnotationBeanPostProcessor.finishRegistration()方法,在方法的最后,调用ScheduledTaskRegistrar.afterPropertiesSet(),一路跟踪到scheduleTasks(),这个方法前面已经说过了,会在spring容器初始化完成之后,对ScheduledTaskRegistrar它的成员变量triggerTasks、cronTasks、fixedRateTasks、fixedDelayTasks中的所有任务调度一次。到这里启动就结束了。

接着再来看一看具体的调度方法,以CronTask为例,方法为ScheduledTaskRegistrar.scheduleCronTask(),其中,成员变量taskScheduler的默认实例为ConcurrentTaskScheduler,所以调用的应该是ConcurrentTaskScheduler.schedule(Runnable task, Trigger trigger),在ConcurrentTaskScheduler实例化的时候会去判断使用哪一种ConcurrentScheduler,因为我这里没有导入javax.enterprise.concurrent-api依赖,所以在静态块里加载不到javax.enterprise.concurrent.ManagedScheduledExecutorService,因此managedScheduledExecutorServiceClass为null,在实例化得时候设置的enterpriseConcurrentScheduler为false,然后调用的时候就会去使用ReschedulingRunnable,跟踪到ReschedulingRunnable.schedule(),这里先解析出了下一次的执行时间,然后算出下一次执行时间距离现在相差的毫秒数,交给传入的ScheduledExecutorService去延迟执行,具体要执行的任务就是ReschedulingRunnable.run(),如下:

	@Override
	public void run() {
		Date actualExecutionTime = new Date();
                //执行具体的业务方法(即被@schduled注解的方法)

                //1.在一开始将业务方法封装成method.invoke()的Runable对象,
                //  具体处理流程在前文提到过的ScheduledAnnotationBeanPostProcessor.processScheduled()方法中

                //2.实例化ReschedulingRunnable的时候通过父类构造器将Runable赋值给ReschedulingRunnable.delegate

                //3.super.run()会调用ReschedulingRunnable.delegate的run方法,这样就达到了执行业务方法的目的。

		super.run();
		Date completionTime = new Date();
		synchronized (this.triggerContextMonitor) {
			this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
			if (!this.currentFuture.isCancelled()) {
				schedule();
			}
		}
	}

然后更新SimpleTriggerContext中的上一次执行时间、上一次实际执行时间、这一次执行完成时间,如果调度任务未被取消,则调用schedule(),计算出下一次的执行时间,并提交给ScheduledExecutorService。

2.结论

(1).所有调度的下一次时间总是在一次调度完成之后进行计算。

(2).由于默认单线程的情况,如果一个时间点有多个任务要执行,任务将会排队进行。

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值