记quartz框架Job运行时动态入参思考过程

420 阅读8分钟

前言

​ 本文重点非框架如何使用,而是回顾当我遇上问题的解决思路,描述了跟踪问题的心路历程,为帮助自己不断总结,完善自己的思考问题方式,如果还能帮助到大家解决类似问题,就很开心了。如果想直接看如何处理,请看文章的“需求背景”和“验证猜想”部分。由于本人对Spring Scheduler和quartz都不熟,仅仅是会用阶段,存在硬伤希望大佬指正。

需求背景

​ 项目要求从多个系统方获取指标信息,通过看板形式做统一展示。目前大概有60~70个指标,从至少5个系统方获取。指标数据的获取频度(要求的时效性不同),有的需要每5s获取一次,有的需要每10min获取一次...由于项目作为敏捷开发,渐进的迭代方式做完善,初版实现思路,采用spring自带的调度工具Spring Scheduler,考虑后续可能由于调度模块压力过大,要做多实例分布式部署。Spring Scheduler没有完备的控制同时刻下,一个调度任务只在唯一实例上执行(可能有但是我没找到资料),可以通过分布式缓存加锁解决,但是要做的话是初尝试难免会遇到一些bug或者问题。因而考虑切换为quartz框架实现调度,quartz提供了完备的分布式调度解决方案,方便后续扩展。由于使用quartz框架每个任务必须对应一个实现了Job接口的实体类,有n个指标,就需要有n个Job实现类,这个做法明显拉胯。所以考虑通过动态入参的方式,Job实现类咱们只定义一个,甚至不定义。

需求总结:

  1. 项目需要针对多系统不同频度获取多个指标展示。
  2. 考虑到指标后续会增多,调度模块压力后续变大要采用多实例部署缓解服务压力。
  3. Spring Scheduler 实现分布式需要自己写逻辑,不稳定。
  4. quartz提供了完备的分布式调度解决方案。
  5. 每有一个指标数据的查询,就需要有一个Job类,后端代码会随着指标的增加变得繁多。所以考虑探索通过动态入参的方式,Job实现类咱们只定义一个,甚至不定义。

开始操作

​ 目前时间紧任务重,分布式先不搞。目前要解决的首要问题是,如何把这70个指标异步的跑起来。处理问题是先保证有,在考虑扩展优化。

第一步:引入springboot-start,这个包里面已经包含了quartz的相关依赖非常方便。

<!--SpringBoot集成QuartZ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>2.3.5.RELEASE</version>
</dependency>

第二步:定义Job

​ 这里遇上一个头疼的问题,由于有70个指标,每个指标获取的逻辑都不同,如果按照普通操作,每一个指标就应对应一个Job的实现类,然后每个Job类和不同的Trigger绑定,Job类会随着指标的增加而增长,且绑定Trigger的过程也会随之变多变复杂,我的设想理想情况有两种:

​ ①在java运行时动态的创建一个job对象;

​ ②定义一个Job类可以通过不同的入参来保持通用;

方案①:我优先考虑了这个方式,因为我发现Job接口是一个函数式接口,我完全可以通过lambda表达式的方式创建一个Job对象(实际上也就是实现了这个接口)。由于创建并注册调度,需要三步如下图:

​ 在第一步创建jobDetail时,需要将具体调度的逻辑通过Job实现类的class对象的方式传入。lambda表达式并没有具体的实现类啊。所以方案①被我否掉了。

bd162fd8f847b9ba4a55d13aa4035557.png ​ 方案②:定义一个通用Job简单,但是如何把传入不同的参数是一个难受的问题。首先在创建JobDetail的时候,需要我给它传一个Job的实现类的类对象,这样框架肯定是要通过反射实现这个Job类,否则调度逻辑没法执行,如果我找到了它在哪反射实现了我传入的Job,我就可以判断它到能不能通过构造子传参的方式实例化Job对象。目前来看应该默认是空参构造子才行,因为我一开始写的Job实现类并没有给有参构造子。

思路总结:

  1. 调度器必须要实例化我们定义的Job对象才能执行调度逻辑
  2. 而我们只传入了Job的class对象,说明调度器肯定是通过反射实例化咱们Job对象的。
  3. 找到实例化Job对象的代码位置,兴许可以看到是否能通过构造子传参的方式实例化对象的逻辑(如果有法子给构造子入参,这个框架肯定会有设置入参的接口,不然它不白瞎了嘛),这样就能做到动态入参了。

顺着这个思路,我首先找scheduler的实现类里有没有方法:

6fa99cb166f4f907604b878cd28e4d49.png 遗憾,希望破产,这个方法里最像执行调度逻辑的scheduleJob方法,也只是将调度器和job任务放入内存而已(因为我没有使用数据库保存调度任务信息数据,quartz默认存入内存),如下图:

12700528488d69f5893ee30a2e0857be.png

另辟蹊径,从job任务入手,既然框架要反射实例化我的Job类,我在Job类里打断点,当调度器执行我Job内的逻辑,之前一定会对这个Job类做实例化,我看调用栈找总可以找到在哪里实例化的吧。

所以有如下:

3aaaba04e905b193b2edb2e537a7cda5.png dd3e6affd26172d0ee3673c035d5d078.png

如上两张图,说明我只要咬死这个job,继续找job在哪被赋值的,不就完了嘛!

d93dd017ca3155cb0ad9433077037057.png 同一个类里在开启线程跑run之前,会调用jec初始化方法:org.quartz.core.JobRunShell#initialize,在该方法里找到了对Job的实例化: d62162954b9b37fa26cc394e750cdb22.png

1474b4681d58923c68b50f0305b80ff6.png

ece6ce1db70d5517d5ed5eb0b9835611.png

​ 此时我懵了,这里采用的是空参构造子实例化对象(或者从spring容器中拿,逻辑没有细追因为是spring相关ioc的知识,最终也是反射创建对象),通过构造子传入参数的希望大概是破灭了,我有想过一种思路:继承上图的SpringBeanJobFactory,然后自己写Job实例化逻辑,仅仅停留在想想,主要是对整体逻辑不太熟悉,怕改出一些问题。那么,我考虑是否还有其他设置入口,上图我注意到一个很有趣的方法调用bundle.getJobDetail().getJobDataMap(),在创建jobDeatil时可以设置,如下图:

d777a2d7efe24dc2811e00b075b7d39e.png 所以,继续追createJobInstance方法中,下方的if(isEligibleForPropertyPopulation(job))中的逻辑,如下图:

5f5219d1ea8d59db59e2a917da28af30.png

注意,bw.setPropertyValues有很多实现类,具体调用了哪个实现类的方法,我是通过打断点判断的,上面有很多步骤由来也是通过断点方式判断:

51dd11f1b71cf95ebe7504542ca2283d.png

ad69313a27b6e7837483da752adf4418.png

b9618bf3f20f5cdb5785ffeb27534298.png 因为,我的目的是找到如何把JobDataMap设置上的,所以对着重看逻辑内有没有类似setXxx的方法,找到一个setValue继续进入:

4deff7749d387ca48fcc08dfb001fccd.png

36bca97047bf52fbbb661b96d2171492.png

cad71481742a4e7e3cc633f364cb7b9f.png

62d6ffca41c1a8efaa89cd79d2fd2287.png

​ 继续分析代码,一定可以找到我想要的答案,但是看到这里我已经等不及了,我打算使用讨巧一点的方法——黑盒测试!首先我做了如下思考:

  1. 我们实现的Job类拥有的方法无非是Object对象共有的hashCode、equal、toString...这些方法,以及实现Job本身需要实现的方法execute

  2. 在上面我追代码的时候就有,在org.quartz.core.JobRunShell#run这个方法里面,是通过job.execute(...)的方式直接调用,如果再通过设置参数的方式调用(execute方法就调用了两次)显然很怪异,逻辑说不通。 40ce4363540a015a354b5b129a380003.png

  3. Object自带的方法,通过反射调用有点脱裤子放屁的感觉,因为任何对象都有的方法,直接obj.xxx()调用不就好了,反射调用一定是针对运行时才能确认调用的方法才有意义!(个人愚见)

  4. 通过以上追代码我确认,反射实现job时用的是空参构造子,既然不能通过构造子方式注入参数,那么只能通过set方式设置参数。

    综上,大胆猜想反射调用的是setXXX方法,我朝Job实现类里定义一个参数,提供get/set方法,然后在定义JobDetail设置JobDataMap时,将动态参数塞入即可。

验证猜想

  1. job实现类里定义一个形参,并提供get/set方法

43855b8a995ab895d49f8a157d3d3d4c.png 2. 定义jobDetail时,加入JobDataMap,将需要设置的参数传入,这里传入的是idxDef,同Job实现类的形参名一致,既然是叫jobDataMap而非methodMap,我就先传入形参名试试先,不对咱再改成setIdxDef嘛。

bd52b6752231e93897c2cceeca68430d.png 3. 跑起来看效果,符合预期

97418cdc7f5a110a1e3112af449aa3b5.png

总结

​ 由于之前只是掌握了quraz的一些基本使用,文档肯定对jobDataMap有相关描述,奈何英语不咋地被迫追了源码才发现设置参数入口,熟悉框架使用最好的方式还是阅读文档,其次是读源码才对框架使用有底气。寻找问题最快的解决方式是baidu(面向浏览器编程),但是那只是能快速解决问题而已,思考才能进步,勤之勉之。