SpringBoot3优雅停止/重启定时任务

开发 前端
在Spring Boot中,使用@Scheduled注解可以方便地创建定时任务。然而,随着应用程序的复杂性和运维需求的增加,动态管理这些定时任务成为了一个重要的问题。针对这种动态管理定时任务Spring Boot中并没有提供相应的实现,所以就需要我们自己动手来实现定时任务的管理。

环境: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个方法可以对具体的任务进行停止及重启。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2024-09-03 10:44:32

2024-09-20 05:49:04

SpringBoot后端

2024-02-28 09:54:07

线程池配置

2017-08-16 16:41:04

JavaSpringBoot定时任务

2019-02-20 15:52:50

技术开发代码

2012-02-07 13:31:14

SpringJava

2009-10-28 10:05:29

Ubuntucrontab定时任务

2023-08-07 14:28:07

SpringBoot工具

2023-01-04 09:23:58

2024-09-09 08:11:12

2010-03-10 15:47:58

crontab定时任务

2021-06-30 07:19:34

SpringBoot定时任务

2023-10-31 12:42:00

Spring动态增删启停

2024-11-04 16:01:01

2023-08-09 08:29:51

SpringWeb编程

2024-01-30 12:08:31

Go框架停止服务

2020-12-21 07:31:23

实现单机JDK

2010-01-07 13:38:41

Linux定时任务

2024-05-13 09:49:30

.NETQuartz库Cron表达式

2021-12-16 14:25:03

Linux定时任务
点赞
收藏

51CTO技术栈公众号