基于Spring Boot的动态定时任务

150 阅读5分钟

前言

在开发中,定时任务是一个常见且重要的需求。但是,之前我在开发时碰到数据库连接初始化在Spring Boot提供的 @Scheduled 注解初始化定时任务之后(使用低代码开发),导致定时任务执行报错,虽然当时不影响项目正常启动,但是考虑不可控情况,导致项目无法正常启动。

为了解决这个问题,我开发了这个基于Spring Boot的动态定时任务功能,它不仅解决了初始化顺序的问题,还提供了更灵活的定时任务管理机制。

这个功能适用于单体项目,我不想额外引用其他框架,增加代码体积;

分布式项目可以使用XXL-Job等调度平台;

项目我放到GitHub上了,可自行拉取代码并修改。

基于当前项目可增加拓展功能:

  1. 结合数据库实现可视化管理定时任务;
  2. 结合Spring Boot 动态配置文件实现热更新定时任务执行时间,具体实现逻辑参考这个:SpringBoot 动态加载配置文件及刷新Bean - 如.若 - 博客园

maven配置

<dependency>
    <groupId>io.github.lin0306</groupId>
    <artifactId>dynamic-task</artifactId>
    <version>1.0.1</version>
</dependency>

技术背景

传统定时任务的局限性

Spring Boot内置的@Scheduled注解虽然使用简单,但存在以下局限:

  1. Cron表达式在编译时固定,无法动态修改
  2. 无法在运行时动态添加或取消任务
  3. 任务管理不够灵活,难以实现复杂的调度需求

解决方案

为了解决上述问题,我开了一个基于Spring Boot的动态定时任务功能,该框架具有以下特点:

  1. 通过自定义注解方式快速定义定时任务
  2. 支持多种Cron表达式格式(标准格式、预定义注解、配置文件引用)
  3. 提供API实现定时任务的动态管理(添加、取消、清理)
  4. 自动任务ID生成,简化任务标识
  5. 基于线程池的任务调度执行
  6. 应用停止时自动清理任务,实现优雅关闭

框架设计与实现

技术栈

  • Java 1.8
  • Spring Boot 2.7.18

核心代码

  1. @DynamicTask注解:用于标记定时任务方法
  2. DynamicTaskProcessor:负责扫描和注册带有@DynamicTask注解的方法
  3. DynamicTaskConfig:提供动态管理定时任务的API

实现原理

1. 自定义注解

首先,我们定义了@DynamicTask注解,用于标记需要定时执行的方法:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicTask {
    // 任务标识,默认使用类名+#+方法名
    String taskId() default "";
    
    // cron表达式
    String cron();
}

2. 任务处理器

DynamicTaskProcessor类实现了ApplicationListener接口,在应用启动完成后自动扫描和注册定时任务:

@Slf4j
@Component
public class DynamicTaskProcessor implements ApplicationListener<ApplicationReadyEvent> {
    
    @Resource
    private DynamicTaskConfig dynamicTaskConfig;
    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 注册定时任务
        dynamicTaskConfig.addCronTask(...);
    }
    
    /**
     * 服务停止时销毁所有定时任务
     */
    @PreDestroy
    public void destroy() {
        DynamicTaskConfig.clearTask();
    }
}

3. 任务配置管理

DynamicTaskConfig类负责任务的添加、取消和清理:

@Slf4j
@Component
public class DynamicTaskConfig {

    @Resource
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    
    private static final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
    
    // 添加定时任务
    public void addCronTask(String taskId, String cronExpression, Runnable task) {
        // ... 实际定时任务逻辑 ...
    }
    
    // 取消任务
    public boolean cancelTask(String taskId) {
        // ... 实际取消任务逻辑 ...
    }
    
    // 清理所有任务
    public static void clearTask() {
        // ... 清理所有任务逻辑 ...
    }
}

使用示例

1. 启用定时任务

在application.properties或application.yml中添加配置:

# 启用动态定时任务
dynamic.schedule.enable=true

2. 使用注解定义任务

@Component
public class TaskDemo {

    // 使用标准Cron表达式(每分钟执行一次)
    @DynamicTask(cron = "0 * * * * *")
    public void standardCronTask() {
        System.out.println("标准Cron表达式任务执行:" + LocalDateTime.now());
    }
    
    // 使用预定义注解(每天执行一次)
    @DynamicTask(cron = "@daily")
    public void annotationTask() {
        System.out.println("预定义注解任务执行:" + LocalDateTime.now());
    }
    
    // 从配置文件读取Cron表达式
    @DynamicTask(cron = "${task.cron.expression}")
    public void configTask() {
        System.out.println("配置文件任务执行:" + LocalDateTime.now());
    }
    
    // 自定义任务ID
    @DynamicTask(taskId = "customTaskId", cron = "0 0 12 * * ?")
    public void customIdTask() {
        System.out.println("自定义ID任务执行:" + LocalDateTime.now());
    }
}

3. 动态管理任务

@Service
public class TaskService {
    
    @Resource
    private DynamicTaskConfig dynamicTaskConfig;
    
    // 动态添加任务
    public void addDynamicTask() {
        dynamicTaskConfig.addCronTask("dynamicTask", "0 */5 * * * *", () -> {
            System.out.println("动态添加的任务执行:" + LocalDateTime.now());
        });
    }
    
    // 取消任务
    public void cancelTask(String taskId) {
        dynamicTaskConfig.cancelTask(taskId);
    }
}

框架特色

1. 多种Cron表达式支持

框架支持以下几种Cron表达式格式:

标准Cron表达式

使用标准的Spring Cron表达式格式,例如:0 0 12 * * ?(每天中午12点执行)

预定义注解

注解描述Cron表达式
@yearly 或 @annually每年执行一次0 0 0 1 1 *
@monthly每月执行一次0 0 0 1 * *
@weekly每周执行一次0 0 0 * * 0
@daily 或 @midnight每天执行一次0 0 0 * * *
@hourly每小时执行一次0 0 * * * *

配置文件引用

使用${property.name}格式从配置文件中读取Cron表达式。

禁用任务

使用-符号可以禁用定时任务。

2. 自动任务ID生成

如果未指定taskId,框架会自动生成任务ID,格式为:类名#方法名,例如:TaskDemo#standardCronTask。

3. 优雅关闭

框架在应用停止时会自动清理所有定时任务,避免资源泄漏。

实现细节与优化

1. 任务注册流程

  1. 应用启动完成后,DynamicTaskProcessor扫描所有带有@DynamicTask注解的方法
  2. 解析注解中的Cron表达式,支持标准表达式、预定义注解和配置文件引用
  3. 生成任务ID(用户自定义或自动生成)
  4. 创建任务执行器,通过反射调用目标方法
  5. 调用DynamicTaskConfig.addCronTask方法注册任务

2. 任务执行机制

框架使用Spring的ThreadPoolTaskScheduler进行任务调度,支持并发执行多个任务。每个任务的执行结果会被包装在ScheduledFuture中,并存储在scheduledTasks集合中,方便后续管理。

3. 异常处理

框架对任务执行过程中的异常进行了处理,确保一个任务的异常不会影响其他任务的执行。同时,对无效的Cron表达式和配置项进行了验证,提供了友好的错误提示。

应用场景

  1. 定时数据同步:定期从外部系统拉取数据或推送数据
  2. 定时报表生成:每天、每周或每月自动生成业务报表
  3. 缓存定时刷新:定期刷新系统缓存数据
  4. 定时任务监控:监控系统运行状态,发送告警信息
  5. 批量处理任务:定时执行批量数据处理任务

总结与展望

本文介绍了一个基于Spring Boot的动态定时任务调度框架,该框架通过自定义注解和配置类,实现了定时任务的动态管理和灵活配置。相比于Spring Boot内置的@Scheduled注解,该框架具有更强的灵活性和可管理性,适用于各种复杂的定时任务场景。

参考资料