文章目录
1、背景说明
其实定时任务的执行周期一般是没必要动态修改的。但有时候在上线跑一段时间之后,可能会发现之前的执行频率不合适,需要调整。以往的方式就是修改代码重新上线,但是如果能够基于配置动态修改就方便很多了。
2、代码示例
@Component
@EnableScheduling
public class ScheduleExecutor {
private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Scheduled(cron = "*/5 * * * * ?")
public void print() {
System.out.println(formatter.format(new Date()) + "----定时任务执行----");
}
}
3、源码探究
基于上面的代码示例进行debug
来分析源码。
只贴关键代码,详细源码自行阅读。
3.1 任务调度器初始化
第一个初始化的地方在ScheduledAnnotationBeanPostProcessor.java
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext() == this.applicationContext) {
// IOC容器初始化完成以后进行任务注册器初始化
finishRegistration();
}
}
进入finishRegistration
方法
if (this.scheduler != null) {
// 如果任务调度器不为空,则直接设置
this.registrar.setScheduler(this.scheduler);
}
// 这里主要是执行自定义任务配置逻辑。可以自己实现SchedulingConfigurer接口,然后会在这里被执行到
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);
}
}
// 如果当前注册器里有任务并且任务调度器为空,则寻找并设置调度器
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
try {
// Search for TaskScheduler bean...
this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
}
}
如果没找到,则进入ScheduledTaskRegistrar.java
处理
// 实例化以后则执行任务调度
@Override
public void afterPropertiesSet() {
scheduleTasks();
}
protected void scheduleTasks() {
// 如果调度器为空,则设置默认调度器
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
}
由于我们没有自定义任务调度器,所以会生成默认调度器即ConcurrentTaskScheduler
。
3.2 任务注册
首先还是在ScheduledAnnotationBeanPostProcessor.java
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 如果扫描到了基于@Schedule声明的定时任务,则进行调度处理
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);
}
}
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
// 如果存在cron表达式,则将任务封装为CronTask,尝试去调度任务,然后将任务加入任务列表
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}
然后又进入ScheduledTaskRegistrar.java
处理
public ScheduledTask scheduleCronTask(CronTask task) {
ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
boolean newTask = false;
// 判断是否是新任务
if (scheduledTask == null) {
scheduledTask = new ScheduledTask(task);
newTask = true;
}
// 如果调度器不为空,则执行任务
if (this.taskScheduler != null) {
scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
}
// 如果调度器为空,则假如任务列表,暂不执行
else {
addCronTask(task);
this.unresolvedTasks.put(task, scheduledTask);
}
return (newTask ? scheduledTask : null);
}
3.3 任务调度
上面3.2已经提到了任务的最终执行是scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
而在3.1里提到了使用的任务调度器是ConcurrentTaskScheduler
。
进入ConcurrentTaskScheduler.java
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
try {
// 将任务、调度器等等封装为ReschedulingRunnable,再执行调度
ErrorHandler errorHandler =
(this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
return new ReschedulingRunnable(task, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule();
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
}
}
进入 ReschedulingRunnable.java
,注意ReschedulingRunnable
实现了Runnable
接口
public ScheduledFuture<?> schedule() {
synchronized (this.triggerContextMonitor) {
// 根据Cron表达式计算下一次执行的时间
this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
if (this.scheduledExecutionTime == null) {
return null;
}
// 距离下一次执行还有多久
long initialDelay = this.scheduledExecutionTime.getTime() - this.triggerContext.getClock().millis();
// 使用调度器内部封装的定时任务线程池调度当前任务(也就是执行下面的run()方法)
this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
return this;
}
}
// 任务的执行逻辑
@Override
public void run() {
Date actualExecutionTime = new Date(this.triggerContext.getClock().millis());
super.run();
Date completionTime = new Date(this.triggerContext.getClock().millis());
synchronized (this.triggerContextMonitor) {
Assert.state(this.scheduledExecutionTime != null, "No scheduled execution");
this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
if (!obtainCurrentFuture().isCancelled()) {
// 划重点!!!如果任务没被取消,则继续执行上面的schedule方法,如此循环往复就实现了定时任务的效果
schedule();
}
}
}
4、流程梳理
定时任务的初始化工作主要在IOC初始化时完成,主要包括:调度器初始化、任务注册、任务调度。
- 任务调度器初始化。首先会寻找用户自定义的任务调度器,即
TaskScheduler
类型的Bean;如果找不到,则创建默认的任务调度器ConcurrentTaskScheduler
。任务调度器其实就是对定时任务线程池的封装。需要注意默认调度器对应的线程池只有一个线程。 - 注册任务。扫描自定义的定时任务,将其封装成
ScheduleTask
,并放到任务列表。
1)将扫描到的自定义定时任务所在的Bean以及Method对象封装为ScheduledMethodRunnable
,ScheduledMethodRunnable
实现了Runnable
接口。
2)将上一步得到的ScheduledMethodRunnable
和自定义的执行周期Cron表达式以其封装为CronTask
。
3)将上一步得到的CronTask
和定时任务提交结果ScheduledFuture
一起封装为ScheduledTask
。可以通过ScheduledFuture
获取任务执行情况以及取消当前任务。 - 任务调度。将第2步得到的任务交给第1步得到的任务调度器去执行。底层就是向JDK的定时线程池(
ScheduledExecutorService
)提交任务(Runnable
)。
上面是整体执行流程,对于示例代码具体执行过程如下:
- 启动Springboot应用。
- 后置处理器
ScheduledAnnotationBeanPostProcessor#processScheduled
扫描定时任务封装为CronTask
,调用ScheduledTaskRegistrar#scheduleCronTask
尝试进行任务调度。 ScheduledTaskRegistrar#scheduleCronTask
中发现还没有任务调度器,就先把任务放到任务列表里。- Spring
ApplicationContext
准备好以后,调用ScheduledAnnotationBeanPostProcessor#finishRegistration
寻找合适的任务调度器,最终没有找到。 ScheduledTaskRegistrar
实例化完成以后,调用ScheduledTaskRegistrar#scheduleTasks
来调度任务,发现任务调度器为空,则创建一个默认的调度器,再把任务提交给调度器内部的定时任务线程池去执行。
5、思路分析
经过以上的梳理,就有了大致思路:
- 取消当前的任务,
- 以新的执行周期新建任务,
- 执行新建的任务
6、解决方案
6.1 基于Apollo配置Cron表达式
Apollo配置:
配置名称:schedule_corn_config
key:ScheduleExecutor#print
value:*/2 * * * * ?
...(可配置多个key-value)
Apollo配置类:
/**
* apollo配置
*/
public class ApolloConfig {
/**
* 配置内容
*/
private Map<String/*类名#方法名*/, String/*cron表达式*/> confs;
public Map<String, String> stringValues() {
Map<String, String> allValues = new HashMap<>();
for (Entry<String, String> entry : this.confs.entrySet()) {
allValues.put(entry.getKey(), entry.getValue());
}
return allValues;
}
}
6.2 对Apollo做变更监听
@Component
public class ApolloChangeProcessor {
@Autowired
private ApplicationContext applicationContext;
/**
* 定时任务corn表达式变更
*
* @param newConfig
*/
@ApolloConfigChangeListener(namespace = "xxx", config = "schedule_corn_config")
public void scheduleCornConfigChanged(ApolloConfig newConfig) {
applicationContext.publishEvent(newConfig);
}
}
这里监听Apollo配置变更(可参考Apollo开发文档),然后将最新的配置通过Spring Publish-Event-Listener
功能发布出去
6.3 重建任务
/**
* 定时任务cron表达式变更处理器
*
* @author jarryxu
* @version 1.0
* @title ScheduleCornChangeHandler.java
* @package com.didi.bane.business.schedule
* @date 2023/6/27 17:39
* @description
*/
@Component
public class ScheduleCornChangeHandler implements SchedulingConfigurer {
@Autowired
private ScheduledAnnotationBeanPostProcessor processor;
private ScheduledTaskRegistrar registrar;
@EventListener
public void cornChanged(ApolloConfig newConfig) {
Map<String, String> newConfigMap = newConfig.stringValues();
if (newConfigMap == null || newConfigMap.isEmpty()) {
return;
}
// 获取所有定时任务
Set<ScheduledTask> scheduledTasks = processor.getScheduledTasks();
for (ScheduledTask scheduledTask : scheduledTasks) {
Task task = scheduledTask.getTask();
if (task instanceof CronTask) {
CronTask cronTask = (CronTask) task;
Runnable runnable = cronTask.getRunnable();
if (runnable instanceof ScheduledMethodRunnable) {
// ScheduledMethodRunnable.target字段为定时任务所在的bean,ScheduledMethodRunnable.method为定时任务对应的方法
ScheduledMethodRunnable scheduledMethodRunnable = (ScheduledMethodRunnable) runnable;
Map.Entry<String, String> entry = newConfigMap.entrySet().stream().filter(e -> {
// 定时任务名称:类名#方法名
String scheduleName = e.getKey();
if (ObjectUtils.isEmpty(scheduleName)) {
return false;
}
// 从定时任务名称拆分出类名和方法名
String[] split = scheduleName.split("#");
String className = split[0];
String methodName = null;
if (split.length >= 2) {
methodName = split[1];
}
return AopUtils.getTargetClass(scheduledMethodRunnable.getTarget()).getSimpleName().equals(className)
&& scheduledMethodRunnable.getMethod().getName().equals(methodName);
}).findAny().orElse(null);
if (entry != null) {
String newCorn = entry.getValue();
// 如果cron表达式没变化则不处理
if (!cronTask.getExpression().equals(newCorn)) {
// 取消当前任务(注意:如果正在执行则会中断)
scheduledTask.cancel();
System.out.println("----切换定时任务[" + entry.getKey() + "]执行频率为" + newCorn + "----");
// 重新注册新的定时任务
registrar.scheduleCronTask(new CronTask(runnable, newCorn));
}
}
}
}
}
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 目的是拿到ScheduledTaskRegistrar对象来注册定时任务(注意:该对象无法直接通过自动注入获得)
registrar = taskRegistrar;
}
}
- 获取到最新的Cron表达式
- 找到要修改的定时任务
- 取消该任务
- 以最新的执行周期新建任务
- 将该任务添加到任务调度器
7、总结
带着问题阅读源码比通篇无脑阅读源码更容易,找到问题、解决问题以后也更有成就感。