使用Quartz框架集成Spring,动态配置定时任务(个人思考)

Posted 张子行的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Quartz框架集成Spring,动态配置定时任务(个人思考)相关的知识,希望对你有一定的参考价值。

前言

最近遇到这样一个需求,需要开发一个类似若依的任务管理控制面板。核心内容是让用户可以动态的去进行对任务的增、删、改、查操作。实现方式有很多种下面先来简单列举一下可实现这个需求的技术。

  1. 使用Spring为我们提供的@Scheduled注解可以生成定时任务,只要我反射学的足够NB,动态的修改定时任务的参数也不是什么难事(自己造轮子、不推荐)
  2. 使用XXL-JOB这个技术框架,XXL-JOB是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。(以后抽空学习这个框架)
  3. 使用Quartz框架,利用其中为我们提供的API轻松解决,前提是对Spring有一定的理解才行,好了不多BB,下文基于这个框架来铺述

正文

先来简单描述一下Quartz中的三大基本组件吧。

  1. 调度器工厂(SchedulerFactory):顾名思义用来生产调度器的,调度器负责控制所有旗下任务的运行
  2. 触发器(Trigger):用来设置任务的触发条件
  3. 任务类型(JobDetail):自定义任务的类型,只需传入对应类的Class即可

接下来一个小例子生动形象的诠释上面的三大组件:我帮老师创建一个改作业的任务(确定JobDetail的类型),当有作业上交到办公室的时候(确定任务触发时机),帮老师改作业(由调度器调度任务的状态),这样老师就轻松一点了。

需求分析

我们需要提供一套对任务的CRUD接口出来,那么SchedulerFactory必然要交给Spring Ioc容器管理撒,其次我们要对任务的信息做一个存储,这样我们才能对具体的某个任务进行CRUD,就这么简单,接下来敲代码,嘿嘿

编码阶段

我坦白了,不装了这里我提供一个我写的对任务的工具类,这种工具类有俩种写法,下面的是写法一,由于我知道Spring-Ioc底层的源码实现中有这么一步操作:会对即将生成的Bean会进行属性填充,也就是会通过Setter方法对Bean中的属性填充赋值,如果我们在Setter方法上加上了@Autowired注解,那么方法参数的来源是从Ioc容器中获取,如果Ioc容器中都没有,那么会进行CreateBean()的操作(具体的源码运行我有点扯多了),总之方法参数schedulerFactory是从Ioc容器中获取的,如下的代码就实现了SchedulerFactory 的自动注入,以及对Scheduler 的初始化。通过如下的工具类,我们可以很方便的对任务进行CRUD。

@Component
public class JobUtil implements ApplicationContextAware, DisposableBean {
    private static ApplicationContext applicationContext;
    private static Scheduler scheduler;

    @Autowired
    public void setScheduler(SchedulerFactory schedulerFactory) throws SchedulerException {
        JobUtil.scheduler = schedulerFactory.getScheduler();
    }

    @SneakyThrows
    public static void addJob(Class orderJob, Trigger trigger, JobDataMap jobDataMap) {
        JobDetail jobDetail = JobBuilder.newJob().ofType(orderJob).setJobData(jobDataMap).build();
        scheduler.scheduleJob(jobDetail, trigger);
    }

    @SneakyThrows
    public static void pauseJob(String jobName, String jobGroup) {
        JobKey jobKey = new JobKey(jobName, jobGroup);
        scheduler.pauseJob(jobKey);
    }

    @SneakyThrows
    public static void resumeJob(String jobName, String jobGroup) {
        JobKey jobKey = new JobKey(jobName, jobGroup);
        scheduler.resumeJob(jobKey);

    }

    @SneakyThrows
    public static void deleteJob(String jobName, String jobGroup) {
        JobKey jobKey = new JobKey(jobName, jobGroup);
        scheduler.deleteJob(jobKey);
    }

    @SneakyThrows
    public static JobDetail getJob(String jobName, String jobGroup) {
        JobKey jobKey = new JobKey(jobName, jobGroup);
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);
        return jobDetail;
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void destroy() {
        JobUtil.applicationContext = null;
    }
}

工具类采用Setter方式赋值原因

可能很多小伙伴,不理解我为什么要通过Setter方式进行Bean注入的,JobUtil工具类不是已经实现了ApplicationContextAware接口吗,你Spring容器的上下文对象都已经有了,为什么不通过如下这种方式对Scheduler进行初始化?笔者有幸学过一点JVM的皮毛,这一切还要从static关键字说起了,被static修饰的变量,在类加载的时候就会被进行赋值,请问这个时候applicationContext从哪里来,到这里又不得不说一下这些Aware接口调用时机了(看下图),Spring的Ioc容器不是万能的!!!!Spring Ioc容器能帮我们进行管理类,但是你最起码对应的类要生成吧。所以 如下这种方式注入,一定会爆空指针异常

private static Scheduler scheduler = applicationContext.getBean(SchedulerFactory.class).getScheduler();

注入方式改进方案

每个工具方法中写上这么一句代码,可是可以,但是代码冗余太高了!

Scheduler scheduler = applicationContext.getBean(SchedulerFactory.class).getScheduler();

    @SneakyThrows
    public static void addJob(Class orderJob, Trigger trigger, JobDataMap jobDataMap) {
        Scheduler scheduler = applicationContext.getBean(SchedulerFactory.class).getScheduler();
        JobDetail jobDetail = JobBuilder.newJob().ofType(orderJob).setJobData(jobDataMap).build();
        scheduler.scheduleJob(jobDetail, trigger);
    }

到这里绝大部分读者可能都已经壁坑了,就怕有人钻牛角尖啊,如果不清楚Spring的源码是怎么运行的恐怕就出不来了,请看下文,牛角尖人员的编码

极端人员编写代码如下

好家伙我也使用Setter方法注入,二而且我还利用到了 ApplicationContext 这个咧,快夸我(__) 嘻嘻,但是这里一定会爆ApplicationContext空指针异常出现,
原因就是Bean的生命周期是这样的,先是进行populateBean(进行Setter方式注入参数Bean)操作然后再是进行initializeBean(调用Aware接口)操作,当前类都还没调用Aware增强接口,ApplicationContext 自然没填充进来的。具体关于Spring源码部分的研究,读者可以看一下我写的这篇文章 spring 源码解析(配图文讲解)顺带搞懂了循环依赖、aop底层实现

@Autowired
public void setScheduler(SchedulerFactory schedulerFactory) throws SchedulerException {
    JobUtil.scheduler = applicationContext.getBean(SchedulerFactory.class).getScheduler();
}

好了好了相信大家对这个JobUtil已经是了如指掌了吧,下文开始正式开发 使用Quartz框架集成Spring,动态配置定时任务的这个需求。

接口实现

拿着工具类来写一套CRUD哈哈哈哈哈,但是也需要注意一些细节哦,我们可以利用Quartz为我们提供的JobDataMap这个Map来实现为任务进行传参,具体的实现查看如下的代码: jdk开启定时任务接口。

@Slf4j
@Api(tags = "任务中心")
@RestController
@RequestMapping("job")
public class jobController {
    @Autowired
    private SchedulerFactory schedulerFactory;

    @SneakyThrows
    @PostConstruct
    public void initJobs() {
        Scheduler scheduler = schedulerFactory.getScheduler();
        scheduler.start();
    }

    @ApiOperation(value = "jdk开启定时任务", notes = "b")
    @GetMapping("/testJob")
    public R getNacosConfig() {
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("orderId", "1");
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
                .startNow()//立即生效
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)//每隔1s执行一次
                        .withRepeatCount(10000)).build(); //重复1w次
        JobUtil.addJob(OrderJob.class, trigger, jobDataMap);
        JobKey jobKey = trigger.getJobKey();
        return R.ok("success" + jobKey.getName() + "--" + jobKey.getGroup());
    }

    @ApiOperation(value = "jdk删除定时任务", notes = "b")
    @GetMapping("/deleteJob/{jobName}/{jobGroup}")
    public R Jobdelete(@PathVariable("jobName") String jobName,
                       @PathVariable("jobGroup") String jobGroup) {
        JobUtil.deleteJob(jobName, jobGroup);
        return R.ok("success");
    }

    @ApiOperation(value = "jdk暂停定时任务", notes = "b")
    @GetMapping("/pauseJob/{jobName}/{jobGroup}")
    public R JobStop(@PathVariable("jobName") String jobName,
                     @PathVariable("jobGroup") String jobGroup) {
        JobUtil.pauseJob(jobName, jobGroup);
        return R.ok("success");
    }

    @ApiOperation(value = "jdk重启定时任务", notes = "b")
    @GetMapping("/resumeJob/{jobName}/{jobGroup}")
    public R resumeJob(@PathVariable("jobName") String jobName,
                       @PathVariable("jobGroup") String jobGroup) {
        JobUtil.resumeJob(jobName, jobGroup);
        return R.ok("success");
    }
}

我们的自定义任务

从开发角度上看任务必然是需要依赖Service层的撒,但是任务就是一个普通的类啊,Service注入不进来啊慌得一批,没事利用上文JobUtil中的ApplicationContext对象做文章获取一下指定的Service就好了,接着进行任务的参数传递到Service层。具体实现看如下代码

public class OrderJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        OrderServiceImpl orderService = JobUtil.getBean(OrderServiceImpl.class);
        orderService.payOrder(jobDataMap);
    }
}

最终效果

一旦开启定时任务,就会调用业务层的payOrder方法,在这里我们可以实现自己的任务逻辑,十分方便,同时可以随时终止、暂定、删除该任务

本文思考

JobDataMap:我们可以通过JobDataMap将一些业务数据传递到定时任务中,继而定时任务就可以帮我们处理对应的业务了。

个人在写这篇文章的时候一直在思考,这种参数传递一层套一层,让人看着实在不优雅,盲猜一波JobDataMap其实就是通过ThreadLocal来进行传递的,那么我们上文中的代码是不是也能利用Threadlocal将其优化一波呢?本文篇幅有限,以后俺再来研究一波吧。

我是互联网中苟且偷生的一名大四狗,欢迎大家和我交流技术呀哈哈哈哈哈

以上是关于使用Quartz框架集成Spring,动态配置定时任务(个人思考)的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot集成持久化Quartz定时任务管理和界面展示

Spring Boot集成Quartz注入Spring管理的类

SpringBoot系列:Spring Boot集成定时任务Quartz

SpringBoot系列:Spring Boot集成定时任务Quartz

Spring MVC—— quartz实现定时任务

Spring Boot集成quartz实现定时任务并支持切换任务数据源