【必看!】动态修改SpringBoot定时任务执行周期

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初始化时完成,主要包括:调度器初始化、任务注册、任务调度。

  1. 任务调度器初始化。首先会寻找用户自定义的任务调度器,即TaskScheduler类型的Bean;如果找不到,则创建默认的任务调度器ConcurrentTaskScheduler。任务调度器其实就是对定时任务线程池的封装。需要注意默认调度器对应的线程池只有一个线程
  2. 注册任务。扫描自定义的定时任务,将其封装成ScheduleTask,并放到任务列表。
    在这里插入图片描述
    1)将扫描到的自定义定时任务所在的Bean以及Method对象封装为ScheduledMethodRunnableScheduledMethodRunnable实现了Runnable接口。
    2)将上一步得到的ScheduledMethodRunnable和自定义的执行周期Cron表达式以其封装为CronTask
    3)将上一步得到的CronTask和定时任务提交结果ScheduledFuture一起封装为ScheduledTask。可以通过ScheduledFuture获取任务执行情况以及取消当前任务
  3. 任务调度。将第2步得到的任务交给第1步得到的任务调度器去执行。底层就是向JDK的定时线程池(ScheduledExecutorService)提交任务(Runnable)。

上面是整体执行流程,对于示例代码具体执行过程如下:

  1. 启动Springboot应用。
  2. 后置处理器ScheduledAnnotationBeanPostProcessor#processScheduled扫描定时任务封装为CronTask,调用ScheduledTaskRegistrar#scheduleCronTask尝试进行任务调度。
  3. ScheduledTaskRegistrar#scheduleCronTask中发现还没有任务调度器,就先把任务放到任务列表里。
  4. SpringApplicationContext准备好以后,调用ScheduledAnnotationBeanPostProcessor#finishRegistration寻找合适的任务调度器,最终没有找到。
  5. ScheduledTaskRegistrar实例化完成以后,调用ScheduledTaskRegistrar#scheduleTasks来调度任务,发现任务调度器为空,则创建一个默认的调度器,再把任务提交给调度器内部的定时任务线程池去执行。

5、思路分析

经过以上的梳理,就有了大致思路:

  1. 取消当前的任务,
  2. 以新的执行周期新建任务,
  3. 执行新建的任务

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;
    }
}
  1. 获取到最新的Cron表达式
  2. 找到要修改的定时任务
  3. 取消该任务
  4. 以最新的执行周期新建任务
  5. 将该任务添加到任务调度器

7、总结

带着问题阅读源码比通篇无脑阅读源码更容易,找到问题、解决问题以后也更有成就感。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringBoot中,可以使用定时任务来定时更新数据库。首先,你需要创建一个定时任务线程池,可以通过创建一个配置类来实现。在这个配置类中,你可以设置线程池的大小、线程名称前缀等参数。\[3\] 接下来,你需要创建一个定时任务的服务类,可以通过实现接口SchedulingConfigurer来实现。在这个服务类中,你可以定义定时任务执行逻辑。你可以从数据库中读取指定时间来动态执行定时任务。\[1\] 然后,在项目启动时,你可以通过实现ApplicationRunner接口,在run方法中初始化定时任务。你可以注入定时任务的服务类,并调用初始化方法来启动定时任务。\[2\] 这样,当项目启动时,定时任务就会被初始化,并按照设定的时间周期执行更新数据库的操作。 #### 引用[.reference_title] - *1* [springboot定时任务结合数据库](https://blog.csdn.net/weixin_47063459/article/details/120992304)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [基于springboot定时任务的后台管理,使用数据库配置定时任务相关信息,修改,关闭定时任务不需重启项目](https://blog.csdn.net/yaoyipeng/article/details/129482305)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值