当前位置: 首页 > article >正文

SpringBoot手动注册定时任务

一、背景

项目存在这样一个场景。程序启动过程中,在Spring的Bean组件注册完毕后,会初始化一些基础数据到数据库中,而项目中有部分定时任务需要依赖这些基础数据才能正常运行。如果直接使用@Scheduled注解标注定时任务方法,会导致定时任务提前执行且执行失败。
基于以上背景,需要将定时任务的注册执行放在数据初始化以后,那么这部分定时任务就需要手动注册且与基础数据初始化操作保持同步执行,保证定时任务的执行一定晚于数据初始化。

二、方案

自定义配置一个定时任务注册器和定时任务执行器,用于处理我们的需求场景。

三、编码实现

通过阅读Spring实现的定时任务源码(篇幅有限,不展开),得知其底层使用的执行器org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler,使用方法org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler#schedule(java.lang.Runnable, org.springframework.scheduling.Trigger)来配置启动定时任务。
因此,我们也使用这个类来启动需要控制执行时机的定时任务,实现代码如下:

package com.jacks.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;

/**
 * 配置自定义定时任务执行器
 *
 * @author Jacks丶
 * @since 2025-03-16
 */
@Configuration
@Slf4j
public class ScheduledConfig implements SchedulingConfigurer {
    private TaskScheduler scheduler;

    /**
     * 配置定时任务注册器,注册器中自定义执行器用于手动执行定时任务
     *
     * @param taskRegistrar the registrar to be configured
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        scheduler = buildScheduler();
        taskRegistrar.setScheduler(scheduler);
    }

    /**
     * 自定义定时任务执行器,用于执行定时任务
     *
     * @return 定时任务执行器对象
     */
    @Bean
    public TaskScheduler buildScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("thread-task-t-");
        scheduler.setPoolSize(2);
        return scheduler;
    }


    /**
     * 初始化定时任务,提供给其他Bean对象手动注册定时任务
     */
    public void initScheduledTask() {
        scheduler.schedule(() -> System.out.println("定时任务执行了..."), new CronTrigger("*/2 * * * * *"));
    }
}

此时我们的初始化数据的逻辑如下:

package com.jacks.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;

import java.time.Duration;

/**
 * SpringBoot启动阶段执行
 *
 * @author Jacks丶
 * @since 2025-03-16
 */
@Slf4j
public class SpringTask implements SpringApplicationRunListener {
    public SpringTask(SpringApplication application, String[] args) {
        log.info("hello init SpringTask.");
    }

    /**
     * Springboot应用已就绪,可在此初始化基础数据
     *
     * @param context   上下文对象,包含Bean对象,可用于获取定时任务配置Bean,然后调用init方法
     * @param timeTaken 应用准备所花费时间,可用于了解性能
     */
    @Override
    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        log.info("开始初始化 ....");
        try {
            // 模拟数据初始化耗时
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("结束初始化 ...");
        // 获取定时任务配置类bean对象执行初始化方法启动定时任务
        context.getBean(ScheduledConfig.class).initScheduledTask();
    }
}

启动类如下:

package com.jacks;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * 启动入口类
 *
 * @author Jacks丶
 * @since 2025-03-16
 */
@EnableScheduling
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

此时可见控制台输出:

2025-03-16 18:43:10.489  INFO 17176 --- [           main] com.jacks.DemoApplication                : Started DemoApplication in 0.88 seconds (JVM running for 1.185)
2025-03-16 18:43:10.490  INFO 17176 --- [           main] com.jacks.task.SpringTask                : 开始初始化 ....
2025-03-16 18:43:12.503  INFO 17176 --- [           main] com.jacks.task.SpringTask                : 结束初始化 ...
2025-03-16 18:43:14.006  INFO 17176 --- [thread-task-t-1] com.jacks.task.ScheduledConfig           : 定时任务执行了...
2025-03-16 18:43:16.001  INFO 17176 --- [thread-task-t-1] com.jacks.task.ScheduledConfig           : 定时任务执行了...
2025-03-16 18:43:18.014  INFO 17176 --- [thread-task-t-1] com.jacks.task.ScheduledConfig           : 定时任务执行了...

四、代码优化

如何优雅的将以上逻辑写入到项目中呢?那就需要引入依赖倒置、单一职责等思想。以上代码中,定时任务是直接定义在initScheduledTask方法中的,当我们需要添加定时任务时,那么就会侵入式的修改com.jacks.task.ScheduledConfig#initScheduledTask
基于单一职责思想,定时任务配置类就只负责初始化注册器、执行器和启动定时任务就好,定时任务的定义逻辑就需要提取出去。
基于依赖倒置的思想,我们需要将注册时依赖的具体实现类优化成接口,所以需要抽取接口,让注册依赖接口,而不是具体实现,我们在新增定时任务时,只需要继承接口编写定时任务逻辑即可,无需侵入式的修改原逻辑。

1、定义接口获取定时任务

package com.jacks.service;

import org.springframework.scheduling.config.CronTask;

import java.util.List;

/**
 * 定时任务接口,用于提供可执行的定时任务对象
 *
 * @author Jacks丶
 * @since 2025-03-16
 */
public interface IScheduled {
    /**
     * 提供定时任务执行对象
     *
     * @return 定时任务执行对象列表
     */
    List<CronTask> getScheduledTasks();
}

2、在实现类定义定时任务逻辑

package com.jacks.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.config.CronTask;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 定时任务方法
 *
 * @author Jacks丶
 * @since 2025-03-16
 */
@Slf4j
@Service
public class MyService implements IScheduled {
    /**
     * 定时任务逻辑
     */
    public void task() {
        log.info("task1 exec..");
    }

    /**
     * 提供定时任务执行对象,用于初始化注册
     *
     * @return 定时任务列表
     */
    @Override
    public List<CronTask> getScheduledTasks() {
        List<CronTask> cronTasks = new ArrayList<>();
        cronTasks.add(new CronTask(this::task, "*/2 * * * * *"));
        return cronTasks;
    }
}

3、单一职责,定时任务抽取为方法入参

package com.jacks.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.List;

/**
 * 配置自定义定时任务执行器
 *
 * @author Jacks丶
 * @since 2025-03-16
 */
@Configuration
@Slf4j
public class ScheduledConfig implements SchedulingConfigurer {
    private TaskScheduler scheduler;

    /**
     * 配置定时任务注册器,注册器中自定义执行器用于手动执行定时任务
     *
     * @param taskRegistrar the registrar to be configured
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        scheduler = buildScheduler();
        taskRegistrar.setScheduler(scheduler);
    }

    /**
     * 自定义定时任务执行器,用于执行定时任务
     *
     * @return 定时任务执行器对象
     */
    @Bean
    public TaskScheduler buildScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("thread-task-t-");
        scheduler.setPoolSize(2);
        return scheduler;
    }


    /**
     * 初始化定时任务,提供给其他Bean对象手动注册定时任务
     *
     * @param cronTasks 待执行的定时任务列表
     */
    public void initScheduledTask(List<CronTask> cronTasks) {
        cronTasks.forEach(task -> scheduler.schedule(task.getRunnable(), task.getTrigger()));
    }
}

4、依赖倒置,注册时依赖接口,而不是具体实现

注册定时任务时获取所有com.jacks.service.IScheduled的实现类,并调用getScheduledTasks方法获取定时任务。

package com.jacks.task;

import com.jacks.service.IScheduled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.config.CronTask;

import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * SpringBoot启动阶段执行
 *
 * @author Jacks丶
 * @since 2025-03-16
 */
@Slf4j
public class SpringTask implements SpringApplicationRunListener {
    public SpringTask(SpringApplication application, String[] args) {
        log.info("hello init SpringTask.");
    }

    /**
     * Springboot应用已就绪,可在此初始化基础数据
     *
     * @param context   上下文对象,包含Bean对象,可用于获取定时任务配置Bean,然后调用init方法
     * @param timeTaken 应用准备所花费时间,可用于了解性能
     */
    @Override
    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        log.info("开始初始化 ....");
        try {
            // 模拟数据初始化耗时
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("结束初始化 ...");
        // 获取定时任务配置类bean对象执行初始化方法启动定时任务
        Collection<IScheduled> scheduledServiceList = context.getBeansOfType(IScheduled.class).values();
        List<CronTask> cronTasks = scheduledServiceList.stream()
                .flatMap(service -> service.getScheduledTasks().stream())
                .collect(Collectors.toList());
        context.getBean(ScheduledConfig.class).initScheduledTask(cronTasks);
    }
}

五、结语

以上就是手动控制注册定时任务时机的一种实现方案,并且这种方案不会影响@Scheduled注解注册的定时任务。
希望本篇博客对你有所帮助。


http://www.kler.cn/a/588736.html

相关文章:

  • ActiveMQ监听器在MQ重启后不再监听问题
  • Pytorch:Dataset的加载
  • 百度贴吧IP和ID是什么意思?怎么查看
  • NPU、边缘计算与算力都是什么啊?
  • [leetcode] 面试经典 150 题——篇3:滑动窗口
  • 一分钟了解深度学习
  • Lisp语言的网络管理
  • 利用Java爬虫根据关键词获取商品列表:实战指南
  • 一份C#的笔试题及答案
  • 【NLP】 4. NLP项目流程与上下文窗口大小参数的影响
  • Kafka可视化工具KafkaTool工具的使用
  • Lua语言的嵌入式调试
  • qt 自带虚拟键盘的编译使用记录
  • 深入解析 React Diff 算法:原理、优化与实践
  • C或C++中实现数据结构课程中的链表、数组、树和图
  • matlab 模糊pid实现温度控制
  • nginx请求限流设置:常见的有基于 IP 地址的限流、基于请求速率的限流以及基于连接数的限流
  • Windows系统中安装Rust工具链方法
  • 数据结构篇——树(1)
  • 人工智能中神经网络是如何进行学习的