1 模块装配的概念
通过 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
功能的支持,甚至连配置都不需要就可以使用。这种操作方式就可以看作为模块装配。
2 构建思路
现在捋一下思路:
- 首先需要了解 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
注解修饰的注解,如果作用于某个类上,其子类是可以继承的该注解的
3.2 关于配置文件
@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
接口,从而决定要创建简单任务、流任务还是脚本任务
3.3.3 动态注册 Bean
参考文章:
- 《【走近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
对象