通过 Elastic Job 实现定时任务,每写一个定时任务都需要配置不少东西,故此想要使用 模块装配 的形式封装 Elastic Job,什么是模块装配?
想要理解模块装配,先理解一下 Spring 的自动装配,Spring 的装配大致分为三种:
- 全手动配置的XML文件阶段,用户需要的Bean全部需要在XML文件中声明,用户手动管理全部的Bean
- 半手动配置的注解阶段,用户可以安装需求 @EnableXXX 对应的功能模块,如添加 @EnableWebMvc 可以启用 MVC 功能
- 全自动配置阶段,使用 SpringBoot,用户只需要引入对应的 starter 包,Spring会通过 Spring SPI 的机制自动装配需要的模块
全手动配置示意图:
半手动配置示意图:
全自动配置:
此时又引入一个新概念 Spring SPI,如何理解 SPI ?JDK 原生的 SPI 与 Spring 的 SPI 有什么区别?
SPI 全称叫 Service Provider Interface(服务提供接口),它可以通过一个指定的接口或者抽象类,寻找到预先配置好的实现类,并创建实现类对象。
JDK 原生 SPI和 Spring SPI的区别:
- JDK SPI 扫描的路径是:META-INF/services/ 下的文件,文件名为 接口或抽象类 的全路径;
Spring SPI 的扫描路径是:META-INF 下名为 spring.factories 的文件 - JDK SPI 文件内容为 实现类全路径、支持多个换行、文件名和文件内容需是继承或实现关系;
Spring SPI 的文件内容为 key/value 对,key 支持注解、接口、类;value 支持为全路径名,key 和 value 不需是继承或实现关系 - JDK SPI 实现类为 ServiceLoader,支持根据接口/抽象类获取 Iterator,然后遍历获取所有实现类实例 load;
Spring SPI 的实现类为 SpringFactoriesLoader,支持根据注解、接口、类获取全路径名列表 loadFactoryNames 或实例列表loadFactories
回到上面的问题,什么是模块装配?
像 @EnableXXX 这样注解一键开启 XXX 功能的支持,甚至连配置都不需要就可以使用。这种操作方式就可以看作为模块装配。
现在捋一下思路:
- 首先需要了解 Elastic Job 哪些配置消息可以写在配置文件中读取,比如 Zookeeper 注册中心的配置就可以写在配置文件,这是因为 Zookeeper 注册中心可以作为一个单例 Bean 实例存在,当用户每次创建一个定时作业时,无需再重新地创建它;而其它的诸如 JobCoreConfiguration 核心作业配置就需要在用户每次创建定时作业时自定义一下属性值读取。
- 在了解到上述的需求,我们就要考虑到创建一个自定义的 @EnableSeieiElasticJob 注解,这个注解大概要做些什么工作?我们可以把 Zookeeper 注册中心的实例化过程丢在这里执行,此时就可以使用 @Import 注解,这个注解可以引用某个 @Configuration 组件,而 Zookeeper 注册中心的实例化实际就是写在这个被注入的 @Configuration 组件里头某个声明了 @Bean 的 Bean 中,这里我把这个 @Configuration 组件 命名为 SeieiElasticJobAutoConfiguration
- 接下来我们就可以着重在 SeieiElasticJobAutoConfiguration 这个类的创建,前面也说到我们希望把 Zookeeper 的配置信息写在配置文件上读取,所以 SeieiElasticJobAutoConfiguration 这个类就需要在检测到配置文件包含 Zookeeper 的配置信息才让正式注入,此时可以使用 @ConditionalOnProperty 注解检测配置文件中是否含有 zookeeper 配置中最重要的两个配置信息 namspaces 和 serverlists
- 然后就是创建 Zookeeper 注册中心 Bean 注入到容器中,我们可以在 SeieiElasticJobAutoConfiguration 添加 @Configuration 注解,然后在里面声明 Zookeeper 注册中心并使用 @Bean 注解注入到容器中既可以,而 Zookeeper 配置信息的读取就可以使用 @ConfigurationProperties 注解声明并注入进来读取,至此只要用户使用了 @EnableSeieiElasticJob 注解并且在配置文件中配置了相关的信息就可以在容器注入 Zookeeper 注册中心了
- 接下来就要考虑如何实现实际作业的配置,照葫芦画瓢,先考虑我们想要达到一个什么效果,我希望在声明如下 @EnableSeieiElasticJob 注解之后,在具体的定时任务逻辑上,添加如下的注解即可完成所有配置
@SeieiElasticJobConfig(jobName = "mySimpleJobDemo",cron = "0/5 * * * * ? *",shardingTotalCount = 3,overwrite = true,listener = "top.seiei.simpleJob.MySimpleJonListener",eventTraceRdbDataSource = "elasticJobDataSource")
- 所以现在问题就去到怎么实现这样的效果,首先我们还是要声明 @SeieiElasticJobConfig 这个注解,这个注解里头需要定义所有关于 Elastic Job 的配置以供用户后面配置。然后就要考虑如何读取这个注解的信息,并且实例化对应一系列 Elastic Job 配置注入到 Spring 容器中
- 上面的问题即是创建一个注解解析器解析 @SeieiElasticJobConfig,我们要考虑到,这个注解解析器需要在 Spring 把所有的 Bean 都创建成功才开始运行,此时就可以让这个解析器实现 ApplicationListener 这个接口, 它的 onApplicationEvent 方法就是 Spring 容器所有 bean 组件加载初始化完成之后的生命周期接口,通过其中的 ApplicationReadyEvent 我们既可以完成当前需求实现,这里注意的是这个注解解析器也需要让 Spring 扫描到,可以在上面的 SeieiElasticJobAutoConfiguration 使用 @ComponentScan 注解自动扫描
至此中间件就编写完成,如果不希望使用 @EnableXXX 的形式,也可以使用 Spring SPI 的形式直接注入 SeieiElasticJobAutoConfiguration 同样也能达到同样的效果
3 知识点 3.1 声明注释使用 @interface 修饰词声明注释时,需要确定这个注解的 生命周期 和 该注释 需要用到哪些地方,即:
- @interface 修饰词:用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数,方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过 default 来声明参数的默认值。
- @Target 注释:指定注解使用的目标范围
- @Target(ElementType.TYPE):接口、类、枚举、注解
- @Target(ElementType.FIELD):字段、枚举的常量
- @Target(ElementType.METHOD):方法
- @Target(ElementType.PARAMETER):方法参数
- @Target(ElementType.CONSTRUCTOR):构造函数
- @Target(ElementType.LOCAL_VARIABLE):局部变量
- @Target(ElementType.ANNOTATION_TYPE):注解
- @Target(ElementType.PACKAGE):包
- @Retention 注释:用来确定这个注解的生命周期
- RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成 class 文件的时候,注解被遗弃;被编译器忽略
- RetentionPolicy.CLASS:注解被保留到class文件,但 JVM 加载 class 文件时候被遗弃,这是默认的生命周期,即在class文件中存在,但 JVM 将会忽略,运行时无法获得。
- RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,JVM 加载class文件之后,将被JVM保留,所以他们能在运行时被 JVM 或其他使用反射机制的代码所读取和使用。
- @Documented:@Documented 注解标记的元素,Javadoc 工具会将此注解标记元素的注解信息包含在 Javadoc 中
- @Inherited:@Inherited 注解修饰的注解,如果作用于某个类上,其子类是可以继承的该注解的
- @ConditionalOnProperty:使用该注解可以让声明了该注解的组件根据配置文件是否还有对应的属性值才进入创建注入,这里默认读取的是 classpath 路径下的 application.properties 文件和 application.yml 文件
@ConditionalOnProperty(prefix = "elastic.job.zk", name = {"namespace", "serverLists"}, matchIfMissing = false)
3.3 注解解析器
3.3.1 SpringBoot 监听器
ApplicationContext 事件机制是观察者设计模式的实现,通过 ApplicationEvent 类和 ApplicationListener 接口,可以实现ApplicationContext 事件处理。
如果容器中有一个 ApplicationListener Bean,每当 ApplicationContext 发布 ApplicationEvent 时,ApplicationListener Bean将自动被触发。
SpringBoot 中支持的事件类型如下:
- ApplicationFailedEvent:该事件为SpringBoot启动失败时的操作
- ApplicationPreparedEvent:上下文 context 准备时触发
- ApplicationReadyEvent:上下文已经准备完毕的时候触发
- ApplicationStartedEvent:SpringBoot 启动监听类
- ApplicationEnvironmentPreparedEvent:环境事先准备
- SpringApplicationEvent:获取 SpringApplication
在构建注解解析器时,就可以创建一个实现了 ApplicationListener Bean,通过实现方法的参数 ApplicationReadyEvent 即可完成接下来一系列对于 Spring 上下文的读取和写入操作了。
3.3.2 读取注解信息- ApplicationReadyEvent 参数调用 getApplicationContext 方法即可以获取到 ApplicationContext Spring 上下文;
- ApplicationContext Spring 上下文通过 getBeansWithAnnotation(SeieiElasticJobConfig.class) 方法即可以获取全文声明了 - SeieiElasticJobConfig 注释的 Bean 集合;
- 循环 Bean 集合通过调用 getClass 方法获取对应的 Class,得到 Class 之后调用它的 getInterfaces 方法便获取对应的声明接口列表;
- 循环接口列表,查看该 Bean 是否实现了 Elastic job 的 SimpleJob 接口或者 DataflowJob 接口或者 ScriptJob 接口,从而决定要创建简单任务、流任务还是脚本任务
参考文章:
- 《【走近Spring】BeanDefinition深入分析(RootBeanDefinition、ChildBeanDefinition…)》
- 《2021-07-20:Spring IOC 之 模块装配&条件装配实战》
- BeanDefinition:顾名思义就是一个关于 Bean 的定义描述,通过定义它,最后 DefaultListableBeanFactory 的 registerBeanDefinition(registerBeanName, beanDefinition) 方法就可以实现自定义 Bean 的注入
- DefaultListableBeanFactory:可以由ApplicationContext 上下文通过 getAutowireCapableBeanFactory 强转获取到
BeanDefinition 有一个构造器创建方法就是 BeanDefinitionBuilder,它常用有以下几个方法:
- addConstructorArgValue:填充 Bean 的构造函数参数,类型可以为对应参数的实例,也可以为对应参数的 BeanDefinition
- addConstructorArgReference:填充 Bean 的构造函数参数,类型为字符串,是对应参数 Bean 定义的 beanName
- addPropertyValue:设置 Bean 的属性值,参考上面
- addPropertyReference:设置 Bean 的属性值,参考上面
- setScope:设置 Bean 模式为单例还是多例
- getBeanDefinition:获取 BeanDefinition 对象



