spring schedule配置多任务动态cron(增删启停)

一、背景

之前公司经常会遇到配置定时任务,简单的任务可以直接依赖spring。
简单任务直接使用 @scheduled 注解配合@EnableScheduling。
但是如何实现简单的动态cron呢?

开发原则:
尽可能在项目本身去实现,少依赖第三方框架,避免项目过于臃肿和复杂。

俩种任务调度方式:

二、本篇说明

springBoot 基础模块 spring-boot-starter-web 已经内置 schedule ,无需引入额外依赖。
先思考几个问题:

1、动态 cron 实现的原理

任务的 【 停止】是基于 future接口 的cancel() 方法。
任务的 【增加、删除、启动】是基于 注册到 类ScheduledTaskRegistrar 的 ScheduledFuture的数量。
涉及核心类:

  • ScheduledFuture
  • SchedulingConfigurer
  • ScheduledTaskRegistrar

2、多任务并行执行配置
spring默认机制对schedule是单线程,需要配置多线程并行执行。

3、如何配置多个任务
好多博文,都是配置一个cron,这让初学者很难受。

4、如何配置任务分组
根据自己业务背景,可根据步骤三,进行改造。

5、如何配置服务启动自启任务。
想要程序启动时首次去加我们设置的task,只需实现 CommandLineRunner 即可。

6、如何从数据库读取配置
这个其实很简单,在实现 ScheduledTaskRegistrar 时,先直接查询我们需要的数据即可。

7、如何优雅的实现我们的代码
这里为了我们多个task实现时,去除臃肿的if else ,使用策略模式去实现我们的task,这里代码里面会具体介绍。

参考类图:

8、如何去触发我们的schedule 【增删启停】
配置好 task任务类,注入到 controller ,通过接口直接调用即可。

三、代码实现

先贴出我的github 代码,下面代码描述不全。

1. 普通多任务动态cron 实现

1.1 对应数据库的实体类 TaskEntity

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TaskEntity {
  /**
   * 任务id
   */
  private int taskId;
  /**
   * 任务说明
   */
  private String desc;
  /**
   * cron 表达式
   */
  private String expression;
}

1.2 配置每个任务实现

配置任务接口 TaskService

public interface TaskService {

  void HandlerJob();

  Integer jobId();

}

配置任务接口实现 TaskServiceJob1Impl、TaskServiceJob2Impl …

@Service
public class TaskServiceJob1Impl implements TaskService {
  @Override
  public void HandlerJob() {
    System.out.println("------job1 开始执行---------:"+new Date());

    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "  " + Thread.currentThread().getName() + "  任务一启动");
    try {
      Thread.sleep(10000);//任务耗时10秒
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "  " + Thread.currentThread().getName() + "  结束");

  }

  @Override
  public Integer jobId() {
    return 1;
  }
}

1.3 配置任务解析器 TaskSolverChooser

注:
这里引入策略模式
为啥要配置 任务解析器选择器:
因为我们实现多个任务时,一个任务对应一个 CronTask,需要在 MyScheduledTask 里面去实现我们每一个方法。
譬如,我们有100个任务就要自定义100个任务实现方法,代码会很臃肿,明显不符合,【开闭原则】,于是这里采用策略模式,解耦我们多个任务业务实现逻辑。

@Slf4j
@Component
public class TaskSolverChooser implements ApplicationContextAware {

  private ApplicationContext applicationContext;

  private Map<Integer, TaskService> chooseMap = new HashMap<>(16);

  /**
   * 拿到spring context 上下文
   */
  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }

  @PostConstruct
  private void registerToTaskSolver(){
    Map<String, TaskService> taskServiceMap = applicationContext.getBeansOfType(TaskService.class);
    for (TaskService value : taskServiceMap.values()) {
      chooseMap.put(value.jobId(), value);
      log.info("task {} 处理器: {} 注册成功",new Object[]{value.jobId(),value});
    }
  }

  /**
   * 获取需要的job
   */
  public TaskService getTask(Integer jobId){
    return chooseMap.get(jobId);
  }
}

1.4 配置MyScheduledTask (动态cron核心配置)

说明:
1、配置多线程执行任务
2、配置 刷新 task
3、配置 停止 task
4、配置 执行task 业务逻辑

@Component
public class MyScheduledTask implements SchedulingConfigurer {

  private volatile ScheduledTaskRegistrar registrar;

  private final ConcurrentHashMap<Integer, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();
  private final ConcurrentHashMap<Integer, CronTask> cronTasks = new ConcurrentHashMap<>();

  @Autowired
  private TaskSolverChooser taskSolverChooser;

  @Override
  public void configureTasks(ScheduledTaskRegistrar registrar) {

    //设置20个线程,默认单线程,如果不设置的话,不能同时并发执行任务
    registrar.setScheduler(Executors.newScheduledThreadPool(10));
    this.registrar = registrar;
  }

  /**
   * 修改 cron 需要 调用该方法
   */
  public void refresh(List<TaskEntity> tasks){
    //取消已经删除的策略任务
    Set<Integer> sids = scheduledFutures.keySet();
    for (Integer sid : sids) {
      if(!exists(tasks, sid)){
        scheduledFutures.get(sid).cancel(false);
      }
    }
    for (TaskEntity TaskEntity : tasks) {
      String expression = TaskEntity.getExpression();
      //计划任务表达式为空则跳过
      if(!StringUtils.hasLength(expression)){
        continue;
      }
      //计划任务已存在并且表达式未发生变化则跳过
      if (scheduledFutures.containsKey(TaskEntity.getTaskId())
          && cronTasks.get(TaskEntity.getTaskId()).getExpression().equals(expression)) {
        continue;
      }
      //如果策略执行时间发生了变化,则取消当前策略的任务
      if(scheduledFutures.containsKey(TaskEntity.getTaskId())){
        scheduledFutures.get(TaskEntity.getTaskId()).cancel(false);
        scheduledFutures.remove(TaskEntity.getTaskId());
        cronTasks.remove(TaskEntity.getTaskId());
      }
      //业务逻辑处理
      CronTask task = cronTask(TaskEntity, expression);

      //执行业务
      ScheduledFuture<?> future = registrar.getScheduler().schedule(task.getRunnable(), task.getTrigger());
      cronTasks.put(TaskEntity.getTaskId(), task);
      scheduledFutures.put(TaskEntity.getTaskId(), future);
    }
  }

  /**
   * 停止 cron 运行
   */
  public void stop(List<TaskEntity> tasks){
    tasks.forEach(item->{
      if (scheduledFutures.containsKey(item.getTaskId())) {
        // mayInterruptIfRunning设成false话,不允许在线程运行时中断,设成true的话就允许。
        scheduledFutures.get(item.getTaskId()).cancel(false);
        scheduledFutures.remove(item.getTaskId());
      }
    });
  }

  /**
   * 业务逻辑处理
   */
  public CronTask cronTask(TaskEntity TaskEntity, String expression) {
    return new CronTask(() -> {
          //每个计划任务实际需要执行的具体业务逻辑
          //采用策略,模式 ,执行我们的job
          taskSolverChooser.getTask(TaskEntity.getTaskId()).HandlerJob();
        }, expression);
  }

  private boolean exists(List<TaskEntity> tasks, Integer tid){
    for(TaskEntity TaskEntity:tasks){
      if(TaskEntity.getTaskId() == tid){
        return true;
      }
    }
    return false;
  }

  @PreDestroy
  public void destroy() {
    this.registrar.destroy();
  }

}

1.5 配置程序启动时首次去加我们设置的task

@Component
public class StartInitTask implements CommandLineRunner {

  @Autowired
  private MyScheduledTask myScheduledTask;

  @Override
  public void run(String... args) throws Exception {
    List<TaskEntity> list = Arrays.asList(
        new TaskEntity(1, "测试1", "0/1 * * * * ?"),
        new TaskEntity(2, "测试2", "0/1 * * * * ?")
    );
    myScheduledTask.refresh(list);
  }
}

1.6 配置web接口去触发,增删启停

@RestController
public class StartController {

  @Autowired
  private MyScheduledTask scheduledTask;

  @PostMapping(value = "/startOrChangeCron")
  public String changeCron(@RequestBody List<TaskEntity> list){
    if (CollectionUtils.isEmpty(list)) {
      // 这里模拟存在数据库的数据
      list = Arrays.asList(
          new TaskEntity(1, "测试1","0/1 * * * * ?") ,
          new TaskEntity(2, "测试2","0/1 * * * * ?")
      );
    }
    scheduledTask.refresh(list);
    return "task任务:" + list.toString() + "已经开始运行";
  }

  @PostMapping(value = "/stopCron")
  public String stopCron(@RequestBody List<TaskEntity> list){
    if (CollectionUtils.isEmpty(list)) {
      // 这里模拟将要停止的cron可通过前端传来
      list = Arrays.asList(
          new TaskEntity(1, "测试1","0/1 * * * * ?") ,
          new TaskEntity(2, "测试2","0/1 * * * * ?")
      );
    }
    scheduledTask.stop(list);
    List<Integer> collect = list.stream().map(TaskEntity::getTaskId).collect(Collectors.toList());
    return "task任务:" + collect.toString() + "已经停止启动";
  }

}

2. 分组多任务动态cron 实现

实现原理:
基于反射实现,根据方法全类名,去动态执行方法。多任务分组配置,根据任务类型进行分组。
eg:
定时任务人员的相关操作,有检测人员离职状态,人员业绩达标,人员考勤…等,
作用:
对人员定时任务做一个分类,在同一个类里面去实现不同的task,
比较
《1. 普通多任务动态cron 实现》,是一个类可以实现一个task
《2. 分组多任务动态cron 实现》,是一个类可以实现多个task
详细可参考: 分组多任务动态cron

3 测试记录

测试1 项目启动自启
TaskServiceJob1Impl和TaskServiceJob1Impl … 设置 阻塞10s
观察日志时间可发现,已经同时并发执行俩个任务。

测试2 触发 刷新【增、删、启】我们的task,。
其实这里没这么智能,如果需要触发刷新接口,实际上是重新加载我们的task,就是对应触发我们,增加任务任务,删除任务,启动任务。
使用idea插件测试接口

观察日志

测试3 触发 停止接口,停止一个接口。
这里测试略过…

四、总结

其实实现简单的动态配置,以上代码可用,比较简单。

到此这篇关于spring schedule配置多任务动态cron(增删启停)的文章就介绍到这了,更多相关spring schedule 多任务动态cron内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring Boot支持Crontab任务改造的方法

    在以往的 Tomcat 项目中,一直习惯用 Ant 打包,使用 build.xml 配置,通过 ant -buildfile 的方式在机器上执行定时任务.虽然 Spring 本身支持定时任务,但都是服务一直运行时支持.其实在项目中,大多数定时任务,还是借助 Linux Crontab 来支持,需要时运行即可,不需要一直占用机器资源.但 Spring Boot 项目或者普通的 jar 项目,就没这么方便了. Spring Boot 提供了类似 CommandLineRunner 的方式,很好的执行

  • springboot实现多实例crontab抢占定时任务(实例代码)

    github: https://github.com/jiasion/eslog wechat:minghui-666 利用redisson实现多实例抢占定时任务 pom.xml <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.12.0</version> </dependency>

  • springtask 的使用方法和 cron 表达式解析

    spring 容器依赖 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.5.RELEASE</version> </dependency> 开启任务注解驱动.即扫描的时候扫描 springtask 相关的注解. <?xml version=&qu

  • springboot Quartz动态修改cron表达式的方法

    1.概述: 在开发中有的时候需要去手动禁止和启用定时任务,修改定时任务的cron表达式然后再让其动态生效,之前有过SSM的类似的业务的开发但是忘记写下来了...只好重新温习了一次,加上最近比较流行springBoot所以升级了一下用springBoot来完成. 2.关联技术 SpringBoot.Quartz.H2.thymeleaf (好像就这么多) 3.具体流程 1)首先去手动创建一个调度器工厂对象-SchedulerFactoryBean;其实应该不用手动创建的但是为了顾及到业务的复杂性所

  • Spring @Scheduler使用cron表达式时的执行问题详解

    前言 Spring Scheduler里有两个概念:任务(Task)和运行任务的框架(TaskExecutor/TaskScheduler).TaskExecutor顾名思义,是任务的执行器,允许我们异步执行多个任务.TaskScheduler是任务调度器,来运行未来的定时任务.触发器Trigger可以决定定时任务是否该运行了,最常用的触发器是CronTrigger.Spring内置了多种类型的TaskExecutor和TaskScheduler,方便用户根据不同业务场景选择. 本文主要介绍了关

  • SpringMVC Cron定时器Demo常见问题解决方案

    该技术的不适用的场景 如果在集群环境下,多台服务器中只希望有一台执行,那 Spring 自带的这种定时器方式可能不太符合你的需要. 但是,如果每台服务器都需要独立执行该定时器任务,且相互之间不存在同步,那么还是可以考虑的 SpringMVC 定时器 本文着重介绍的是 SpringMVC 配置定时器的方式,而不是 SpringBoot 配置定时器的方式. 注解方式 首先,在 Clock 类上添加 @Component,然后,在需要定时执行的方法上面加上 @Scheduled,最后指定 cron 表

  • spring schedule配置多任务动态cron(增删启停)

    一.背景 之前公司经常会遇到配置定时任务,简单的任务可以直接依赖spring. 简单任务直接使用 @scheduled 注解配合@EnableScheduling. 但是如何实现简单的动态cron呢? 开发原则: 尽可能在项目本身去实现,少依赖第三方框架,避免项目过于臃肿和复杂. 俩种任务调度方式: 二.本篇说明 springBoot 基础模块 spring-boot-starter-web 已经内置 schedule ,无需引入额外依赖. 先思考几个问题: 1.动态 cron 实现的原理 任务

  • Spring Boot如何实现定时任务的动态增删启停详解

    我以为动态停启定时任务一般用quartz,没想到还可以通过ScheduledTaskRegistrar来拓展.但是分布式场景,建议还是用quartz吧! 在 spring boot 项目中,可以通过 @EnableScheduling 注解和 @Scheduled 注解实现定时任务,也可以通过 SchedulingConfigurer 接口来实现定时任务.但是这两种方式不能动态添加.删除.启动.停止任务.要实现动态增删启停定时任务功能,比较广泛的做法是集成 Quartz 框架. 但是本人的开发原

  • spring @schedule注解如何动态配置时间间隔

    目录 @schedule注解动态配置时间间隔 spring 注解式Schedule配置定时任务 @schedule注解动态配置时间间隔 动态配置时间间隔是通过自己实现的任务注册到任务调度实现的,并在每次调度的时候更改下次调度时间间隔,如果任务阻塞或者挂掉了就不会再被调度了,如果设置时间过长,到下次调度就需要等待很长时间. import org.springframework.beans.factory.annotation.Autowired; import org.springframewor

  • spring schedule实现动态配置执行时间

    目录 spring schedule 动态配置执行时间 @schedule注解动态配置时间间隔 spring schedule 动态配置执行时间 之前saas平台实现动态修改定时任务的时间,都是通过xx-job这样的框架来实现,这样我们可以单独一个服务来管理我们整个saas平台的定时任务,但是最近给银行做的一个小项目,需要本地化部署,所以我不想弄很多的服务,并且他们并没有要求修改以后即时生效,所以我直接采用了 spring schedule结合mysql动态配置执行时间. 之前我们用的sched

  • Spring Schedule Task动态改写Cron配置方式

    目录 Spring Schedule Task动态改写Cron配置 Scheduling Tasks的常规使用 动态改写Cron @Scheduled定时任务动态修改cron参数 先来看下Spring常规定时任务的配置 下面来看看可以在不停服务的情况下动态修改任务周期的实现 Spring Schedule Task动态改写Cron配置 使用Spring @Scheduled标签可以很简单地定义Scheduled Task,但是有时我们需要在程序里动态地改写Cron的配置. 什么时候呢? 额,比如

  • 通过Spring Boot配置动态数据源访问多个数据库的实现代码

    之前写过一篇博客<Spring+Mybatis+Mysql搭建分布式数据库访问框架>描述如何通过Spring+Mybatis配置动态数据源访问多个数据库.但是之前的方案有一些限制(原博客中也描述了):只适用于数据库数量不多且固定的情况.针对数据库动态增加的情况无能为力. 下面讲的方案能支持数据库动态增删,数量不限. 数据库环境准备 下面一Mysql为例,先在本地建3个数据库用于测试.需要说明的是本方案不限数据库数量,支持不同的数据库部署在不同的服务器上.如图所示db_project_001.d

  • 一文搞懂如何实现Java,Spring动态启停定时任务

    目录 为什么需要定时任务 Java定时任务的原理 Timer+TimerTask ScheduledThreadPoolExecutor Timer VS ScheduledThreadPoolExecutor Spring定时任务 @Scheduled定时任务原理(源码) 为什么需要定时任务 定时任务的应用场景十分广泛,如定时清理文件.定时生成报表.定时数据同步备份等. Java定时任务的原理 jdk自带的库中,有两种技术可以实现定时任务,一种是Timer,另一种是ScheduledThrea

  • SpringBoot定时任务两种(Spring Schedule 与 Quartz 整合 )实现方法

    前言 最近在项目中使用到定时任务,之前一直都是使用Quartz 来实现,最近看Spring 基础发现其实Spring 提供 Spring Schedule 可以帮助我们实现简单的定时任务功能. 下面说一下两种方式在Spring Boot 项目中的使用. Spring Schedule 实现定时任务 Spring Schedule 实现定时任务有两种方式 1. 使用XML配置定时任务, 2. 使用 @Scheduled 注解. 因为是Spring Boot 项目 可能尽量避免使用XML配置的形式,

  • 利用Spring Cloud Zuul实现动态路由示例代码

    前言 本文主要给大家介绍了关于Spring Cloud Zuul实现动态路由的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. Zuul 是提供动态路由,监控,弹性,安全等的边缘服务.Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门. Zuul 可以适当的对多个 Amazon Auto Scaling Groups 进行路由请求. 首先新建maven项目,加入如下依赖 <dependencyManagement> <depend

  • Spring + Mybatis 项目实现动态切换数据源实例详解

    项目背景:项目开发中数据库使用了读写分离,所有查询语句走从库,除此之外走主库. 最简单的办法其实就是建两个包,把之前数据源那一套配置copy一份,指向另外的包,但是这样扩展很有限,所有采用下面的办法. 参考了两篇文章如下: http://www.jb51.net/article/111840.htm http://www.jb51.net/article/111842.htm 这两篇文章都对原理进行了分析,下面只写自己的实现过程其他不再叙述. 实现思路是: 第一步,实现动态切换数据源:配置两个D

随机推荐