使用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将其优化一波呢?本文篇幅有限,以后俺再来研究一波吧。

小咸鱼的技术窝

关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页
在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小咸鱼的技术窝

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值