📴简单的代码实现
完全可以!不依赖 Quartz 或 SnailJob 等第三方框架,仅使用 Spring Boot 自带的 TaskScheduler 接口,就能非常轻量且优雅地实现定时任务的动态增、删、启、停。
核心思路是:利用 TaskScheduler 动态调度任务,它会返回一个 ScheduledFuture<?> 对象。我们将这个对象保存在内存(如 ConcurrentHashMap)中,通过它就可以随时控制任务的暂停(cancel)和恢复(重新 schedule)。
以下是结合你的 iot_task 表结构设计的完整代码实现:
1. 数据库实体类 (Entity)
首先,对应你的 iot_task 表建立实体类。为了实现"启动/停止"功能,建议在表中增加一个 status 字段(例如:1-运行中,0-已停止)。
java
public class IotTask {
private Long id;
private String taskName; // 任务名称(作为任务的唯一标识)
private String cron; // Cron 表达式
private String executeAction; // 执行动作(具体的业务逻辑标识)
private Integer status; // 状态:1-启动,0-停止
// 省略 Getter 和 Setter
}
2. 核心任务管理服务 (Service)
这是实现动态控制的核心。我们通过 TaskScheduler 配合 CronTrigger 来注册任务,并用 Map 缓存任务的执行句柄。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
@Service
public class DynamicTaskService {
@Autowired
private TaskScheduler taskScheduler;
// 假设你有对应的 Mapper 来操作数据库
// @Autowired private IotTaskMapper taskMapper;
// 内存中缓存正在运行的任务,key为taskName,value为任务的Future句柄
private final Map<String, ScheduledFuture<?>> taskCache = new ConcurrentHashMap<>();
/**
* 动态添加或启动定时任务
*/
public void startTask(IotTask task) {
String taskName = task.getTaskName();
// 如果任务已在运行,先停止旧任务,防止重复注册
if (taskCache.containsKey(taskName)) {
stopTask(taskName);
}
try {
// 定义任务具体要执行的业务逻辑
Runnable taskLogic = () -> {
System.out.println("正在执行 IoT 任务: " + taskName + ", 动作: " + task.getExecuteAction());
// 这里可以根据 task.getExecuteAction() 调用具体的业务 Service
};
// 使用 CronTrigger 按照 cron 表达式调度任务
ScheduledFuture<?> future = taskScheduler.schedule(taskLogic, new CronTrigger(task.getCron()));
// 将任务句柄存入缓存,方便后续控制
taskCache.put(taskName, future);
// 同步更新数据库状态为"启动"
// task.setStatus(1);
// taskMapper.updateById(task);
System.out.println("任务 [" + taskName + "] 启动成功!");
} catch (Exception e) {
System.err.println("任务 [" + taskName + "] 启动失败,请检查 Cron 表达式格式!");
e.printStackTrace();
}
}
/**
* 动态停止(暂停)定时任务
*/
public void stopTask(String taskName) {
ScheduledFuture<?> future = taskCache.get(taskName);
if (future != null) {
// cancel(true) 表示尝试中断正在执行的任务
future.cancel(true);
taskCache.remove(taskName);
// 同步更新数据库状态为"停止"
// taskMapper.updateStatusByName(taskName, 0);
System.out.println("任务 [" + taskName + "] 已停止!");
}
}
/**
* 动态删除定时任务
*/
public void deleteTask(String taskName) {
// 先停止任务
stopTask(taskName);
// 然后再从数据库删除对应的记录
// taskMapper.deleteById(taskName);
System.out.println("任务 [" + taskName + "] 已删除!");
}
/**
* 动态修改任务的 Cron 表达式
*/
public void updateTaskCron(IotTask task) {
// 修改 Cron 的本质是:先停止旧任务,再用新的 Cron 启动新任务
stopTask(task.getTaskName());
startTask(task);
// 同步更新数据库中的 cron 字段
// taskMapper.updateById(task);
}
}
3. 配置线程池 (Configuration)
为了让动态任务有更好的并发处理能力,建议配置一个专用的线程池。
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 根据实际任务量调整线程池大小
scheduler.setThreadNamePrefix("iot-dynamic-task-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
scheduler.initialize();
return scheduler;
}
}
4. 系统启动时的任务恢复 (Runner)
为了保证系统重启后,数据库中原本处于"启动"状态的任务能自动恢复运行,需要在 Spring Boot 启动完成后加载这些任务。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class TaskInitializationRunner implements CommandLineRunner {
@Autowired
private DynamicTaskService dynamicTaskService;
// @Autowired private IotTaskMapper taskMapper;
@Override
public void run(String... args) throws Exception {
// 1. 从数据库查询所有状态为"启动"(status=1) 的任务
// List<IotTask> activeTasks = taskMapper.selectByStatus(1);
// List<IotTask> activeTasks = ...; // 假设这里获取到了数据
// 2. 遍历并重新注册到调度器中
// for (IotTask task : activeTasks) {
// dynamicTaskService.startTask(task);
// }
System.out.println("系统启动完毕,正在恢复数据库中的动态定时任务...");
}
}
💡 方案总结
- 纯原生实现 :完全基于 Spring Boot 自带的
TaskScheduler,无需引入任何沉重的第三方调度框架,代码非常轻量。 - 动态控制 :通过
ConcurrentHashMap存储ScheduledFuture,完美实现了任务的动态添加、删除、启动和停止。 - 状态持久化 :结合
iot_task表和CommandLineRunner,确保了任务配置和运行状态在系统重启后依然能够保持一致。
📴TaskScheduler使用详解
org.springframework.scheduling.TaskScheduler 是 Spring 框架提供的一个用于任务调度的核心接口。相比于大家熟知的 @Scheduled 静态注解,TaskScheduler 最大的优势在于它支持编程式调度,允许你在程序运行时动态地创建、启动、暂停、停止和修改定时任务,非常适合需要高灵活性和可运维性的企业级应用场景。
下面为你详细解析 TaskScheduler 的核心用法与最佳实践。
🛠️ 核心实现与配置
TaskScheduler 最常用的实现类是 ThreadPoolTaskScheduler。它本质上是对 JDK ScheduledThreadPoolExecutor 的封装,内部维护了一个线程池,具备高并发、低延迟的生产级特性。
在 Spring Boot 中,通常将其配置为一个 Bean 交给 Spring 容器管理:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class TaskSchedulerConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 设置线程池大小(核心线程数)
scheduler.setPoolSize(10);
// 设置线程名称前缀,方便日志排查
scheduler.setThreadNamePrefix("my-task-scheduler-");
// 设置是否为守护线程
scheduler.setDaemon(false);
// 应用关闭时,等待任务执行完毕(最多等待60秒)
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
// 必须调用初始化方法
scheduler.initialize();
return scheduler;
}
}
⚙️ 多种任务调度策略
通过 TaskScheduler,你可以使用以下几种方式来调度 Runnable 任务:
-
在指定时间点执行一次
java@Autowired private TaskScheduler taskScheduler; public void scheduleAtSpecificTime() { // 5秒后执行 Date executionTime = new Date(System.currentTimeMillis() + 5000); taskScheduler.schedule(() -> { System.out.println("指定时间任务执行: " + LocalDateTime.now()); }, executionTime); } -
固定速率执行 (Fixed Rate)
不管上一个任务是否执行完毕,每隔固定的时间就会触发下一次任务。适合周期性检查、心跳检测等场景。
javapublic void startFixedRateTask() { // 每2000毫秒(2秒)执行一次 taskScheduler.scheduleAtFixedRate(() -> { System.out.println("固定速率任务执行: " + LocalDateTime.now()); }, 2000); } -
固定延迟执行 (Fixed Delay)
在上一个任务执行完毕后,等待固定的时间再触发下一次任务。适合前后任务有依赖、或者需要避免任务堆积的场景。
javapublic void startFixedDelayTask() { // 任务执行完后,延迟1000毫秒(1秒)再执行下一次 taskScheduler.scheduleWithFixedDelay(() -> { System.out.println("固定延迟任务执行: " + LocalDateTime.now()); }, 1000); } -
Cron 表达式触发
使用标准的 Cron 表达式(如
"0 0/5 * * * ?"表示每5分钟执行一次),可以实现极其复杂的定时规则javapublic void startCronTask() { // 每10秒执行一次 CronTrigger trigger = new CronTrigger("0/10 * * * * ?"); taskScheduler.schedule(() -> { System.out.println("Cron任务执行: " + LocalDateTime.now()); }, trigger); }
🎯 动态启停任务(核心亮点)
TaskScheduler 的 schedule 系列方法都会返回一个 ScheduledFuture<?> 对象。这个对象是任务的生命周期句柄,通过它可以动态地取消(停止)任务或查询任务状态。
以下是一个完整的动态管理任务的示例:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import java.util.concurrent.ScheduledFuture;
@Service
public class DynamicTaskService {
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
// 使用 AtomicReference 保证多线程下的安全性
private final AtomicReference<ScheduledFuture<?>> scheduledFutureRef = new AtomicReference<>();
// 启动或重启动态任务
public void startDynamicTask(String cronExpression) {
stopDynamicTask(); // 先停止之前的任务
ScheduledFuture<?> future = taskScheduler.schedule(
() -> System.out.println("动态任务正在执行..."),
new CronTrigger(cronExpression)
);
scheduledFutureRef.set(future);
}
// 停止任务
public void stopDynamicTask() {
ScheduledFuture<?> future = scheduledFutureRef.getAndSet(null);
if (future != null && !future.isDone()) {
future.cancel(true); // true 表示如果任务正在运行,尝试中断它
System.out.println("动态任务已停止");
}
}
}
通过这种机制,你可以轻松结合 REST API 或数据库配置,实现一个无需重启服务即可动态调整定时任务的管控台。
⚠️ 注意事项与最佳实践
- 异常处理 :定时任务内部如果抛出未捕获的异常,默认情况下会导致该任务终止且不会自动恢复。建议在
Runnable任务逻辑中手动添加try-catch块来捕获并记录异常。 - 分布式环境 :
TaskScheduler是单机级别的调度。如果在集群环境下部署,会导致每个节点都执行一遍任务。解决分布式重复执行问题,需要引入分布式锁(如 ShedLock + Redis)或专业的分布式调度中间件(如 XXL-JOB、ElasticJob)。 - 线程池隔离 :默认情况下 Spring Task 可能使用同一个线程池。对于耗时较长或极其重要的任务,建议配置独立的
TaskScheduler线程池,避免某个慢任务阻塞其他所有定时任务的执行。