SpringBoot3優雅停止/重啟定時任務
環境:SpringBoot3.2.5
1. 簡介
在Spring Boot中,使用@Scheduled注解可以方便地創建定時任務。然而,隨著應用程序的復雜性和運維需求的增加,動態管理這些定時任務成為了一個重要的問題。針對這種動態管理定時任務Spring Boot中并沒有提供相應的實現,所以就需要我們自己動手來實現定時任務的管理。
2. 執行原理
首先,我們要搞清楚Spring Boot定時任務的執行原理,其核心先通過ScheduledAnnotationBeanPostProcessor處理器,找到所有的Bean中使用了@Scheduled注解的方法,然后將對應的方法包裝到Runnable中。
public class ScheduledAnnotationBeanPostProcessor {
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 找到符合條件的方法
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
});
// 處理方法,在processScheduled方法中會將任務包裝成ScheduledMethodRunnable對象
annotatedMethods.forEach((method, scheduledAnnotations) ->
scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
}
}
接下來,就是通過TaskScheduler來執行定時任務,該接口提供了一些列的方法:
public interface TaskScheduler {
// 這些調用任務都返回了Future
ScheduledFuture<?> schedule(Runnable task, Trigger trigger) ;
ScheduledFuture<?> schedule(Runnable task, Instant startTime);
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
// 還有其它方法。
}
在默認情況下,Spring Boot定時任務的執行線程池使用的是ThreadPoolTaskSchedulerBean。內部真正任務調用是通過ScheduledExecutorService執行定時任務。
所以,要實現動態管理任務,就需要記錄下每個任務信息。記錄任務信息是為了停止任務及再次啟動任務,在上面的調度方法都返回了Future對象,可以通過該Future對象來終止任務,可以通過再次調用schedule方法來再次啟動任務。所以,我們需要自定義TaskScheduler,在自定義的實現中我們就能很方便的記錄管理每個定時任務。
3. 實戰案例
要管理任務,我們就必須為每個任務提供一個有意義的名稱。@Scheduled注解并沒有提供此功能。所以這塊功能,需要自己實現。
3.1 自定義@Task注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Task {
/**任務名稱*/
String value() default "" ;
}
該注解用來對任務的說明。
3.2 任務信息TaskInfo
public class TaskInfo {
private Runnable task ;
private Instant startTime ;
private Trigger trigger ;
private Duration period ;
private Duration delay ;
private ScheduledFuture<?> future ;
}
該類用來在執行任務前記錄當前的信息,以便可以對任務進行停止和重啟。
3.3 自定義線程池
@Component
public class PackTaskScheduler extends ThreadPoolTaskScheduler {
private static final Map<String, TaskInfo> TASK = new ConcurrentHashMap<>() ;
@Override
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
ScheduledFuture<?> schedule = super.schedule(task, trigger) ;
if (task instanceof ScheduledMethodRunnable smr) {
String taskName = parseTask(smr);
TASK.put(taskName, new TaskInfo(task, null, trigger, null, null, schedule)) ;
}
return schedule ;
}
// 還有其它重寫的方法,自行實現
private String parseTask(ScheduledMethodRunnable smr) {
Method method = smr.getMethod();
Task t = method.getAnnotation(Task.class) ;
String taskName = method.getName() ;
if (t != null) {
String value = t.value() ;
if (StringUtils.hasLength(value)) {
taskName = value ;
}
}
return taskName ;
}
public void stop(String taskName) {
TaskInfo task = TASK.get(taskName) ;
if (task != null) {
task.getFuture().cancel(true) ;
}
}
public void start(String taskName) {
TaskInfo task = TASK.get(taskName) ;
if (task != null) {
if (task.trigger != null) {
this.schedule(task.getTask(), task.getTrigger()) ;
}
if (task.period != null) {
this.scheduleAtFixedRate(task.getTask(), task.getPeriod()) ;
}
}
}
}
該類的核心作用就2個:1. 重寫任務調度方法,記錄任務信息2. 添加停止/重啟任務調度也可以考慮在該類中實現任務的持久化。
以上就完成了所有的核心操作。接下來寫2個方法進行測試。
3.4 測試
定時任務
@Scheduled(cron = "*/3 * * * * *")
@Task("測試定時任務-01")
public void scheduler() throws Exception {
System.err.printf("當前時間: %s, 當前線程: %s, 是否虛擬線程: %b%n", new SimpleDateFormat("HH:mm:ss").format(new Date()), Thread.currentThread().getName(), Thread.currentThread().isVirtual()) ;
}
停止/重啟接口
private final PackTaskScheduler packTaskScheduler ;
public SchedulerController(PackTaskScheduler packTaskScheduler) {
this.packTaskScheduler = packTaskScheduler ;
}
@GetMapping("stop")
public Object stop(String taskName) {
this.packTaskScheduler.stop(taskName) ;
return String.format("停止任務【%s】成功", taskName) ;
}
@GetMapping("/start")
public Object start(String taskName) {
this.packTaskScheduler.start(taskName) ;
return String.format("啟動任務【%s】成功", taskName) ;
}
分別調用上面2個方法可以對具體的任務進行停止及重啟。