SpringBoot实现用户定制的定时任务(动态定时任务)

简介: SpringBoot实现用户定制的定时任务(动态定时任务)的实现

SpringBoot实现用户定制的定时任务(动态定时任务)

文章目录

  • SpringBoot实现用户定制的定时任务(动态定时任务)
  • 情景
  • 实现
  • 实体类Cron
  • Service层:接口CronService以及其实现类CronServiceImpl
  • 定时任务管理器CronManageTask

情景

  我们知道SpringBoot能使用@Scheduled注解来进行定时任务的控制,该注解需要配合Cron表达式以及在启动类上添加@EnableScheduling注解才能使用。

  不过我们现在的假定情景并不是程序员设定的定时任务,而是用户可以在我们的网页上定制定时任务,前端将该任务的信息发送到后端后,后端可以将此任务存入数据库并在规定的时间内执行。例如用户可以设定定时任务的执行时间段,执行时刻等,并可以随时新增、删除和改变定时任务。

  接下来我们来使用SpringBoot实现这个假定情景

实现

实体类Cron

  我们需要创建实体类Cron代表定时任务,这里假设Cron有如下属性:执行时刻、任务标题、任务开始的日期、任务截止日期,以及存入数据库所需要的几个基本属性:id(作为主键)、创建时间、更新时间、状态status

  我们用一个BaseEntity来保存基本属性,Cron将继承BaseEntity,使用MyBatisPlus作为ORM框架,Cron的代码如下:

@Data
@EqualsAndHashCode(callSuper = true)
public class Cron extends BaseEntity {
  private static final long serialVersionUID = 1L;
  @NotNull(message = "执行时刻不能为空")
  private LocalTime executeTime;
  @NotBlank(message = "标题不能为空")
  private String title;
  @NotNull(message = "截止日期不能为空")
  private LocalDate deadTime;
  @NotNull(message = "开始日期不能为空")
  private LocalDate startTime;
}

  这里需要注意的是lombok的@Data注解相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。

  其中,@EqualsAndHashCode注解会生成equals(Object other) 和 hashCode()方法。我们重写了equals(Object other) 和 hashCode()方法,就是为了在两个对象的属性相同时equals能返回true,认为它们两个相同。但@EqualsAndHashCode默认仅使用该类中定义的属性且不会调用父类的equals(Object other) 和 hashCode()方法。这是什么意思呢?仅使用该类中的属性,也就是如果该类的两个对象属性相同,即使这两个对象对应父类的属性不同,equals也会认为它们两个对象相同,从而返回true。默认的实现中不使用父类的属性,将会导致问题,比如,有多个类有相同的部分属性,恰好id(数据库主键)在父类中,那么就会存在部分对象在比较时,它们并不相等,却因为lombok自动生成的equals(Object other) 和 hashCode()方法判定为相等,从而导致出错。所以我们在使用@Data时同时需要加上@EqualsAndHashCode(callSuper=true)注解来解决这一问题,加上(callSuper=true),其生成的equals(Object other) 和 hashCode()方法将调用父类的方法,也就是会考虑父类的属性。

  加上@EqualsAndHashCode(callSuper=true)就符合我们的要求了,这样即使两个Cron对象,它们的属性相同,但它们在父类中对应的主键不同,equals将认为它们是不同的对象,返回false。

  对于前端传参,我们需要进行非空验证,我们在实体类中还加入了@NotNull和@NotBlank注解,并且使用message配置提示语句。这两个注解都来自于javax.validation.constraints包,该包内还有另一个常用注解@NotEmpty,@NotEmpty 用在集合上面,一般用来校验List类型(不能注释枚举类型),而且长度必须大于0。@NotBlank 用在String上面,一般用来校验String类型不能为空,而且调用trim()后,长度必须大于0。@NotNull 可用在所有类型上,校验是否为非null。这些注解都需要配合@Validated注解使用,从而检验Controller的入参是否符合规范,例如:

public Result save(@Validated @RequestBody Cron cron)

  Cron的父类BaseEntity的代码如下:

@Data
public class BaseEntity implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private LocalDateTime created;
    private LocalDateTime updated;
    private Integer status;
}

  由于实体类需要在网络中传输,所以BaseEntity需要实现Serializable接口,这里使用MyBatisPlus的@TableId注解进行属性与数据库主键的映射。

Service层:接口CronService以及其实现类CronServiceImpl

  我们需定义接口CronService来实现用户定制定时任务需求。用户能创建、删除、修改定时任务,创建任务即判断当前日期是否为既定的执行日期,若是则启动定时任务。删除任务即判断该任务是否已被启动,若是,则将其停止。修改任务即先停止该任务,再重新启动该任务。我们让CronService继承MyBatisPlus的IService接口,对应的数据库操作直接在Controller层中调用相应方法即可,我们就不需要再在CronService中定义了。于是,我们需要在CronService中定义startCron(Cron cron)stopCron(Cron cron)changeCron(Cron cron)三个方法,分别对应用户的创建、删除、修改定时任务操作。

public interface CronService extends IService<Cron> {
  void startCron(Cron cron);
  void stopCron(Cron cron);
  void changeCron(Cron cron);
}

  我们创建CronService的实现类CronServiceImpl来实现上述3个方法。

  对于每个定时任务,我们肯定是让线程池提供一个线程去执行它,springboot提供了ThreadPoolTaskScheduler,可以很方便地对重复执行的任务进行调度管理;相比于通过java自带的周期性任务线程池ScheduleThreadPoolExecutorThreadPoolTaskScheduler支持根据cron表达式创建周期性任务,这正是我们所需要的。其实ThreadPoolTaskScheduler底层也是通过线程池ScheduleThreadPoolExecutor实现的,不过多加了一些支持Cron表达式的代码。ThreadPoolTaskScheduler的核心成员变量是ScheduledExecutorService scheduledExecutor,一个 ExecutorService 可以安排任务在给定的延迟后运行,或者定期执行。ScheduledFuture表示可以取消的延迟结果动作。 通常,ScheduledFuture是使用 ScheduledExecutorService 执行任务的返回结果。

  因此,我们使用ThreadPoolTaskScheduler来启动线程,执行定时任务。但这还不够,我们有很多定时任务,我们必须保存它们的信息,以便查找,因为我们还有停止任务和更新任务操作。于是我们可以创建一个HashMap来保存定时任务的信息,key肯定是cron的id,value为ScheduledExecutorService 执行任务的返回结果ScheduledFuture。我们可以调用ScheduledFuturecancel方法来终止任务的执行。

  接下来我们来考虑CronService接口3个方法的具体实现。对于startCron方法,我们需要避免它重复启动已经启动的任务,因此我们要先判断该任务是否已经在HashMap中,若不在,我们再去判断当前日期是否在执行日期范围内,若在,我们通过Cron的执行时刻属性构造cron表达式,创建实现了Runnable接口的内部类来实现任务要做的事,调用ThreadPoolTaskSchedulerschedule方法启动该任务,并将该任务存入HashMap中。

  对于stopCron方法,我们通过Cron的id从HashMap中查找其对应的ScheduledFuture,若不为空,则调用其cancel(true)方法停止任务,并将其从HashMap中删除。cancel方法的参数传入true会中断线程停止任务,而传入false则会让线程正常执行至完成。

  changeCron方法的实现很简单,先调用stopCron,再调用startCron即可

  CronServiceImpl的完整代码如下:

@Service
public class CronServiceImpl extends ServiceImpl<CronMapper, Cron> implements CronService {
  private Logger log = LoggerFactory.getLogger(getClass());
  @Autowired
  private ThreadPoolTaskScheduler threadPoolTaskScheduler;
  private Map<Long, ScheduledFuture<?>> futureMap = new HashMap<>();
  @Bean
  public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
    return new ThreadPoolTaskScheduler();
  }
  @Override
  public void startCron(Cron cron) {
    if (futureMap.containsKey(cron.getId())) {
      log.warn("已经存在重复任务,任务id:{},任务标题:{},任务提醒时刻:{},任务开始时间:{},任务截止时间:{}",
          cron.getId(), cron.getTitle(), cron.getExecuteTime(), cron.getStartTime(), cron.getDeadTime());
      return;
    }
    if (LocalDate.now().isEqual(cron.getStartTime()) || LocalDate.now().isEqual(cron.getDeadTime()) ||
        (LocalDate.now().isAfter(cron.getStartTime()) && LocalDate.now().isBefore(cron.getDeadTime()))) {
      LocalTime executeTime = cron.getExecuteTime();
      String cronExp = StringUtils.join(Integer.valueOf(executeTime.getSecond()).toString(), " ", Integer.valueOf(executeTime.getMinute()).toString()
          , " ", Integer.valueOf(executeTime.getHour()).toString(), " * * ?");
      ScheduledFuture<?> future = threadPoolTaskScheduler.schedule(new MyRunnable(cron), new CronTrigger(cronExp));
      futureMap.put(cron.getId(), future);
      log.info("启动定时任务成功,任务id:{},任务标题:{},任务提醒时刻:{},任务开始时间:{},任务截止时间:{}",
          cron.getId(), cron.getTitle(), cron.getExecuteTime(), cron.getStartTime(), cron.getDeadTime());
    }
  }
  @Override
  public void stopCron(Cron cron) {
    ScheduledFuture<?> future = futureMap.get(cron.getId());
    if (future != null) {
      future.cancel(true);
      futureMap.remove(cron.getId());
      log.info("关闭定时任务成功,任务id:{},任务标题:{},任务提醒时刻:{},任务开始时间:{},任务截止时间:{}",
          cron.getId(), cron.getTitle(), cron.getExecuteTime(), cron.getStartTime(), cron.getDeadTime());
    }
  }
  @Override
  public void changeCron(Cron cron) {
    stopCron(cron);// 先停止,在开启.
    startCron(cron);
  }
  private class MyRunnable implements Runnable {
    private Cron cron;
    public MyRunnable(Cron cron) {
      this.cron = cron;
    }
    @Override
    public void run() {
      // 定义任务要做的事,完成任务逻辑
    }
  }
}

  其实我们这样做还没有完成需求,因为在startCron中,只有当前时间在执行时间段内,才会创建线程去执行定时任务,这样是肯定不行的。我们还需要创建一个定时任务管理器,让它每天定时去启动数据库中尚未启动的定时任务,并删除已经过期的定时任务,防止数据积压。

定时任务管理器CronManageTask

  这时我们就需要用@Scheduled注解了,我们定义CronManageTask中的cronManage()方法,加上@Scheduled注解,让它每天定时去启动数据库中尚未启动的定时任务,并停止并删除已经过期的定时任务。

  使用@Scheduled注解需要注意几个点,一是CronManageTask需使用@Component注解,且此类中不能包含其他带任何注解的方法;二是cronManage()方法不能有参数、不能有返回值;三是需添加@EnableScheduling注解到启动类上面。

  违反上述任一点,@Scheduled注解就不会生效

  CronManageTask的代码如下:

@Component
public class CronManageTask {
  private Logger log = LoggerFactory.getLogger(getClass());
  @Autowired
  private CronService cronService;
  @Scheduled(cron = "0 0 3 * * ?")
  public void cronManage() {
    List<Cron> list = cronService.list();
    list.forEach(c -> {
      if (LocalDate.now().isAfter(c.getDeadTime())) {
        cronService.stopCron(c);
        cronService.removeById(c.getId());
        log.info("删除过期定时任务成功,任务id:{},任务标题:{},任务提醒时刻:{},任务开始时间:{},任务截止时间:{}",
            c.getId(), c.getTitle(), c.getExecuteTime(), c.getStartTime(), c.getDeadTime());
      } else {
        log.info("尝试启动尚未start的定时任务,任务id:{},任务标题:{},任务提醒时刻:{},任务开始时间:{},任务截止时间:{}",
            c.getId(), c.getTitle(), c.getExecuteTime(), c.getStartTime(), c.getDeadTime());
        cronService.startCron(c);
      }
    });
  }
}

  cron表达式"0 0 3 * * ?"表示每天凌晨3点执行。需要注意的是,@Scheduled注解的cron表达式一般都要定义在配置文件里,方便修改,使用cron = "${xiaolinbao.cron}",并在application.yml中配置xiaolinbao.cron=0 0 3 * * ?即可。上面的代码偷懒了。

  至此,使用SpringBoot实现动态定时任务的需求就完成了

目录
相关文章
|
1月前
|
druid Java 数据库
Spring Boot的定时任务与异步任务
Spring Boot的定时任务与异步任务
|
1月前
|
Java
Springboot 导出word,动态填充表格数据
Springboot 导出word,动态填充表格数据
|
1月前
|
Java 调度 Spring
SpringBoot实现多线程定时任务动态定时任务配置文件配置定时任务
SpringBoot实现多线程定时任务动态定时任务配置文件配置定时任务
319 0
|
1月前
|
Java 开发工具 git
spring boot 集成 ctrip apollo 实现动态配置更新
spring boot 集成 ctrip apollo 实现动态配置更新
73 1
|
1月前
|
安全 数据安全/隐私保护
Springboot+Spring security +jwt认证+动态授权
Springboot+Spring security +jwt认证+动态授权
120 0
|
2天前
|
Java 应用服务中间件 容器
手写SpringBoot(二)之动态切换Servlet容器
我们在切换serlvet容器的时候,会将SpringBoot默认的tomcat jar包给排除掉,换上我们需要的jar包,比如jetty。
7 0
|
3天前
|
关系型数据库 数据库 数据安全/隐私保护
springboot+dynamic-datasource多数据源配置动态切换
springboot+dynamic-datasource多数据源配置动态切换
9 0
|
1月前
|
SQL Java 调度
SpringBoot使用@Scheduled定时任务录入将要过期任务数据
SpringBoot使用@Scheduled定时任务录入将要过期任务数据
|
1月前
|
Java Spring 容器
SpringBoot 使用Quartz执行定时任务对象时无法注入Bean问题
SpringBoot 使用Quartz执行定时任务对象时无法注入Bean问题
50 1
|
1月前
|
Java 调度 Maven
Springboot实战篇--Springboot框架通过@Scheduled实现定时任务
Spring Boot的Scheduled定时任务无需额外Maven依赖,通过`@EnableScheduling`开启。任务调度有两种方式:fixedRate和fixedDelay,前者任务结束后立即按设定间隔执行,后者在任务完成后等待设定时间再执行。更灵活的是cron表达式,例如`0 0 3 * * ?`表示每天3点执行。实现定时任务时,需注意默认单线程执行可能导致的任务交错,可通过自定义线程池解决。