当前位置: 代码网 > it编程>编程语言>Java > SpringBoot动态定时任务怎么实现?四种方案详解

SpringBoot动态定时任务怎么实现?四种方案详解

2026年09月01日 Java 我要评论
一、定时任务的基础认知与演进思路在 springboot 中,定时任务的发展大致经历了三个阶段:静态阶段:使用@scheduled注解硬编码 cron 表达式,改动需重启。动态阶段:通过schedul

一、定时任务的基础认知与演进思路

在 springboot 中,定时任务的发展大致经历了三个阶段:

  1. 静态阶段:使用 @scheduled 注解硬编码 cron 表达式,改动需重启。

  2. 动态阶段:通过 schedulingconfigurer 接口从数据库读取 cron,实现表达式动态生效 。

  3. 可控制阶段:使用 threadpooltaskscheduler 或 quartz,不仅能改时间,还能手动启停任务 。

本文将覆盖上述所有阶段,其中 方案二 和 方案三 是目前中小团队最常用的“动态任务”解决方案。

二、方案一:基于注解的静态定时任务(快速入门)

作为基础铺垫,我们先快速搭建一个最简版本。

2.1 启用定时任务

在启动类上添加 @enablescheduling 注解。

@springbootapplication
@enablescheduling
public class scheduledapplication {
    public static void main(string[] args) {
        springapplication.run(scheduledapplication.class, args);
    }
}

2.2 创建静态任务

@component
public class statictask {
    // 每5秒执行一次
    @scheduled(cron = "0/5 * * * * ?")
    public void execute() {
        system.out.println("静态任务执行:" + localdatetime.now());
    }
}

2.3 优缺点

  •  优点:代码简单,适合固定不变的周期性任务。

  • 缺点:cron 表达式硬编码在代码中,修改需重启服务。

三、方案二:实现schedulingconfigurer接口(动态读取 cron)

这是实现“动态定时任务”最经典的方案,无需引入第三方依赖,直接从数据库读取 cron 表达式 。

3.1 核心原理

schedulingconfigurer 接口允许我们自定义 scheduledtaskregistrar,通过 addtriggertask 方法动态添加任务。每次执行任务前,都会重新读取数据库中的 cron 表达式,从而实现“动态生效”。

3.2 数据库表设计

create table `cron_config` (
  `id` int not null auto_increment,
  `task_key` varchar(50) not null comment '任务唯一标识',
  `cron_expression` varchar(50) not null comment 'cron表达式',
  `task_desc` varchar(100) default null comment '任务描述',
  `status` tinyint default '1' comment '1:启用 0:停用',
  `updated_time` timestamp null default current_timestamp on update current_timestamp,
  primary key (`id`),
  unique key `uniq_task_key` (`task_key`)
) engine=innodb default charset=utf8mb4;
insert into `cron_config` values (1, 'demotask', '0/10 * * * * ?', '演示任务', 1, now());

3.3 完整实现代码

3.3.1 实体类

@data
@tablename("cron_config")
public class cronconfig {
    private integer id;
    private string taskkey;
    private string cronexpression;
    private string taskdesc;
    private integer status;
    private localdatetime updatedtime;
}

3.3.2 mapper 接口

@mapper
public interface cronconfigmapper extends basemapper<cronconfig> {
    // 根据任务key查询有效的cron
    default string getcronbykey(string taskkey) {
        lambdaquerywrapper<cronconfig> wrapper = new lambdaquerywrapper<>();
        wrapper.eq(cronconfig::gettaskkey, taskkey)
               .eq(cronconfig::getstatus, 1);
        cronconfig config = selectone(wrapper);
        return config != null ? config.getcronexpression() : null;
    }
}

3.3.3 核心调度类

@component
public class dynamicschedulingtask implements schedulingconfigurer {
    @autowired
    private cronconfigmapper cronconfigmapper;
    @override
    public void configuretasks(scheduledtaskregistrar taskregistrar) {
        // 注册一个trigger任务
        taskregistrar.addtriggertask(
            // 1. 任务内容 (runnable)
            () -> executetask(),
            // 2. 触发条件 (trigger)
            triggercontext -> {
                // 每次执行前都去数据库获取最新的cron
                string cron = cronconfigmapper.getcronbykey("demotask");
                if (stringutils.isempty(cron)) {
                    return null; // 如果没有配置或停用,则不执行
                }
                return new crontrigger(cron).nextexecutiontime(triggercontext);
            }
        );
    }
    private void executetask() {
        system.out.println("动态任务执行: " + localdatetime.now());
        // 这里可以写具体的业务逻辑
    }
}

3.4 测试动态修改

修改数据库中的 cron_expression 字段,例如从 0/10 * * * * ? 改为 0/20 * * * * ?,无需重启应用,下次任务执行时将自动采用新表达式 。

3.5 局限性分析

  • 优点:无需引入额外依赖,实现简单,cron 修改即时生效。

  • 缺点:无法手动停止任务(除非设置 cron 为 null),也无法在运行时新增任务。

四、方案三:使用threadpooltaskscheduler(手动控制启停)

如果需要通过 api 动态地启动、停止任务,或者支持一次性任务(如指定某时间执行一次),方案二就不够用了。此时需要使用 threadpooltaskscheduler 。

4.1 核心原理

threadpooltaskscheduler 是 spring 提供的任务调度线程池,它的 schedule(runnable task, trigger trigger) 方法可以动态提交任务,并返回一个 scheduledfuture 对象。

通过 scheduledfuture.cancel() 我们可以随时停止任务。

4.2 配置线程池

@configuration
public class schedulerconfig {
    @bean
    public threadpooltaskscheduler threadpooltaskscheduler() {
        threadpooltaskscheduler scheduler = new threadpooltaskscheduler();
        scheduler.setpoolsize(10);                 // 线程池大小
        scheduler.setthreadnameprefix("dynamic-task-");
        scheduler.setawaitterminationseconds(60);  // 优雅停机等待时间
        scheduler.setwaitfortaskstocompleteonshutdown(true); // 等待任务完成再停机
        return scheduler;
    }
}

4.3 任务管理服务

这是一个核心服务类,支持任务的增、删、改、查

@service
public class dynamictaskservice {
    // 存储正在运行的任务
    private final map<string, scheduledfuture<?>> taskmap = new concurrenthashmap<>();
    @autowired
    private threadpooltaskscheduler taskscheduler;
    /**
     * 启动一个cron任务
     * @param taskid 任务id
     * @param task 要执行的任务
     * @param cronexpression cron表达式
     */
    public boolean startcrontask(string taskid, runnable task, string cronexpression) {
        // 如果任务已存在,先停止
        if (taskmap.containskey(taskid)) {
            stoptask(taskid);
        }
        scheduledfuture<?> future = taskscheduler.schedule(task, new crontrigger(cronexpression));
        taskmap.put(taskid, future);
        return true;
    }
    /**
     * 启动一个一次性任务(指定时间执行)
     * @param taskid 任务id
     * @param task 任务
     * @param starttime 执行时间
     */
    public boolean startonetimetask(string taskid, runnable task, date starttime) {
        if (taskmap.containskey(taskid)) {
            stoptask(taskid);
        }
        scheduledfuture<?> future = taskscheduler.schedule(task, starttime);
        taskmap.put(taskid, future);
        return true;
    }
    /**
     * 停止任务
     */
    public boolean stoptask(string taskid) {
        scheduledfuture<?> future = taskmap.get(taskid);
        if (future != null) {
            future.cancel(true);  // 如果正在执行,是否立即中断
            taskmap.remove(taskid);
            return true;
        }
        return false;
    }
    /**
     * 修改cron表达式(先停后启)
     */
    public boolean updatecrontask(string taskid, runnable task, string newcron) {
        stoptask(taskid);
        return startcrontask(taskid, task, newcron);
    }
    public set<string> getrunningtasks() {
        return taskmap.keyset();
    }
}

4.4 通过 controller 动态管理

@restcontroller
@requestmapping("/task")
public class taskcontroller {
    @autowired
    private dynamictaskservice dynamictaskservice;
    // 启动一个cron任务
    @postmapping("/start/cron")
    public string startcron(@requestparam string taskid, @requestparam string cron) {
        runnable task = () -> {
            system.out.println(taskid + " 执行: " + localdatetime.now());
            // 业务逻辑
        };
        dynamictaskservice.startcrontask(taskid, task, cron);
        return "任务 " + taskid + " 已启动,cron: " + cron;
    }
    // 启动一次性任务
    @postmapping("/start/once")
    public string startonce(@requestparam string taskid,
                            @requestparam string time) throws parseexception {
        simpledateformat sdf = new simpledateformat("yyyy-mm-dd hh:mm:ss");
        date starttime = sdf.parse(time);
        runnable task = () -> system.out.println(taskid + " 一次性任务执行: " + localdatetime.now());
        dynamictaskservice.startonetimetask(taskid, task, starttime);
        return "任务 " + taskid + " 将在 " + time + " 执行";
    }
    // 停止任务
    @deletemapping("/stop/{taskid}")
    public string stop(@pathvariable string taskid) {
        boolean b = dynamictaskservice.stoptask(taskid);
        return b ? "任务已停止" : "任务不存在";
    }
    // 查看正在运行的任务
    @getmapping("/list")
    public set<string> list() {
        return dynamictaskservice.getrunningtasks();
    }
}

4.5 与数据库结合

在实际生产中,我们不会通过 controller 直接硬编码任务内容,而是将任务信息存储在数据库(如任务名、执行类路径、cron、状态等)。启动时从数据库加载所有状态为“启用”的任务,调用 startcrontask 提交到调度器 。

4.6 优缺点

  • 优点:灵活可控,支持运行时动态启停,支持一次性任务。

  • 缺点:如果应用重启,内存中的任务会丢失,需要结合数据库做启动时的任务恢复。

五、方案四:整合 quartz 集群模式(企业级分布式方案)

当应用部署在多台服务器上时,需要避免任务被多次执行。此时需要引入分布式调度框架,如 quartz 集群、elastic-job 或 xxl-job 。

5.1 为什么需要 quartz?

  • 持久化:任务和触发器信息存储在数据库表中,重启不丢失。

  • 集群支持:通过数据库行锁实现任务分配的负载均衡和故障转移,确保同一个任务在同一时间只被一台机器执行 。

5.2 引入依赖

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-quartz</artifactid>
</dependency>
<dependency>
    <groupid>mysql</groupid>
    <artifactid>mysql-connector-java</artifactid>
</dependency>

5.3 配置文件

spring:
  quartz:
    job-store-type: jdbc   # 使用数据库存储
    jdbc:
      initialize-schema: never  # 生产环境建议手动初始化,不要自动建表
    properties:
      org:
        quartz:
          scheduler:
            instancename: clusteredscheduler
            instanceid: auto   # 集群中自动生成唯一id
          jobstore:
            class: org.quartz.impl.jdbcjobstore.jobstoretx
            driverdelegateclass: org.quartz.impl.jdbcjobstore.stdjdbcdelegate
            tableprefix: qrtz_
            isclustered: true    # 启用集群
            clustercheckininterval: 10000
            useproperties: false
          threadpool:
            class: org.quartz.simpl.simplethreadpool
            threadcount: 10
            threadpriority: 5

5.4 自定义业务表与 quartz 联动

在实际业务中,我们通常有一个自己的任务配置表(如 business_job)。应用启动时,需要从业务表读取任务,并注册到 quartz 中 。

5.4.1 业务表实体

@entity
@data
public class businessjob {
    @id
    private string jobname;
    private string jobgroup;
    private string cronexpression;
    private string jobclass; // 全限定类名
    private string jobdata;  // json格式的参数
    private integer status;  // 1启用 0停用
}

5.4.2 启动时加载任务

@component
public class jobinitlistener implements applicationlistener<contextrefreshedevent> {
    @autowired
    private scheduler scheduler;
    @autowired
    private businessjobrepository jobrepository;
    @override
    public void onapplicationevent(contextrefreshedevent event) {
        try {
            // 查询所有启用的任务
            list<businessjob> joblist = jobrepository.findbystatus(1);
            for (businessjob job : joblist) {
                schedulejob(job);
            }
        } catch (exception e) {
            e.printstacktrace();
        }
    }
    private void schedulejob(businessjob job) throws exception {
        // 构建jobdetail
        jobdetail jobdetail = jobbuilder.newjob((class<? extends job>) class.forname(job.getjobclass()))
                .withidentity(job.getjobname(), job.getjobgroup())
                .storedurably()
                .build();
        // 设置jobdatamap(传递参数)
        if (stringutils.isnotblank(job.getjobdata())) {
            jobdetail.getjobdatamap().put("data", job.getjobdata());
        }
        // 构建trigger
        crontrigger trigger = triggerbuilder.newtrigger()
                .withidentity(job.getjobname() + "trigger", job.getjobgroup())
                .withschedule(cronschedulebuilder.cronschedule(job.getcronexpression()))
                .build();
        // 调度
        scheduler.schedulejob(jobdetail, trigger);
    }
}

5.5 任务执行类

public class samplejob implements job {
    @override
    public void execute(jobexecutioncontext context) {
        string data = context.getmergedjobdatamap().getstring("data");
        system.out.println("samplejob 执行,参数: " + data + ",时间: " + localdatetime.now());
    }
}

5.6 优缺点

  • 优点:支持持久化、集群、故障转移,功能强大。

  • 缺点:配置相对复杂,需要维护 quartz 的 11 张系统表。

六、四种方案对比与选型建议

特性方案一 (@scheduled)方案二 (schedulingconfigurer)方案三 (threadpooltaskscheduler)方案四 (quartz集群)
动态cron不支持支持支持支持
动态启停不支持不支持支持支持
运行时新增任务不支持不支持支持支持
持久化内存内存内存数据库
分布式支持不支持不支持不支持支持
复杂性中高

选型建议

  • 个人项目 / 极简工具:方案一就够了。

  • 中小项目,只有cron需动态配置,无集群需求:方案二最合适,轻量无侵入。

  • 需要提供api给用户手动启停任务,或有一次性任务需求:方案三是首选。

  • 大型分布式项目,必须保证任务不重复执行:直接上 quartz 或更专业的分布式调度平台(如 xxl-job)。

七、生产环境中的避坑指南

7.1 关于线程池

  • 默认线程池大小为1:如果不自定义 taskscheduler,spring 默认使用单线程池执行所有定时任务。如果一个任务卡死,会导致所有任务阻塞。建议在配置类中设置合理的线程池大小 。

@bean
public taskscheduler taskscheduler() {
    threadpooltaskscheduler scheduler = new threadpooltaskscheduler();
    scheduler.setpoolsize(10);
    return scheduler;
}

7.2 关于分布式锁

如果暂时不想引入 quartz,但又有两台机器需要避免任务重复,可以使用 redis 分布式锁 + 方案二/方案三:

// 任务执行时尝试加锁
if (redistemplate.opsforvalue().setifabsent("task_lock", "1", 30, timeunit.seconds)) {
    try {
        // 执行业务逻辑
    } finally {
        redistemplate.delete("task_lock");
    }
}

7.3 关于任务幂等性

无论是否使用分布式调度,都应保证任务逻辑的幂等性,防止意外重复执行造成数据问题。

7.4 关于异常处理

定时任务中的异常如果不捕获,会导致任务后续不再执行(取决于调度器策略)。建议在任务最外层使用 try-catch 记录日志 。

八、总结与展望

本文详细介绍了 springboot 中实现动态定时任务的四种方式,从最简单的注解方式,到基于接口的cron动态刷新,再到灵活可控的 threadpooltaskscheduler,最后是企业级的 quartz 集群方案。

动态定时任务的核心逻辑始终是:将变化的配置(cron、状态)外移到数据库,在任务调度时实时读取。 掌握这些思路,足以应对大部分业务场景。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com