Theia:可扩展的注解式配置注入组件
Theia 是一个 java 语言编写的,支持自定义扩展的注解式配置加载与注入组件,旨在以注解的方式加载任何可以被表示成 Properties 对象的配置,并注入给目标对象,同时支持当配置内容发生变更时回调更新。配置文件的来源可以是本地文件、网络,以及第三方配置系统。Theia 默认支持从 ClassPath 加载本地配置文件,并支持以 SPI 的方式扩展以支持更多的配置来源,例如从 ZK 加载配置等。
特性一览:
- 支持以注解的方式加载多种配置数据源,并注入给配置对象。
- 支持预注入,预注入会校验配置的合法性,如果不合法则会放弃注入,避免配置出错影响服务的正常运行。
- 支持配置变更时回调更新,默认关闭,并允许用户配置是否启用。
- 内置基本类型转换器,用于将 String 类型配置项转换成目标类型对象。
- 支持自定义类型转换器,以实现一些定制化的类型转换。
- 支持以原生字符串或 Properties 对象的形式注入。
- 支持监听注入过程(InjectEventListener)和更新过程(UpdateEventListener)。
- 支持加载系统环境变量,并注入给配置对象。
- 支持
${}
占位符替换,使用指定的配置项替换占位符。 - 支持以 SPI 的方式扩展以支持更多类型的配置数据源。
- 对于 Spring 应用,支持自动扫描、加载并初始化配置对象。
开源地址:https://github.com/plotor/theia
快速接入
这里以加载并注入 ClassPath 配置文件 configurable_options.properties
为例,接入过程分为 4 步:
- 定义一个实现了 Options 接口的配置类 ExampleOptions;
- 为 ExampleOptions 类添加
@Configurable
注解,用于指定配置数据源路径; - 调用
ConfigManager#initialize
方法初始化所有被管理的配置项; - 调用
ConfigManager#getOptions
方法拿到目标 options 实例,以获取对应的配置信息。
ExampleOptions 的部分实现如下,完整实现可以参考源码:
1 |
|
初始化配置管理器:
1 | final ConfigManager configManager = ConfigManager.getInstance(); |
好啦,就这么简单,接下去就可以愉快的使用配置项啦!
如果是 Spring 应用,则只需要在对应的 Options 类上添加 @Component
注解,并在 Spring 配置文件中添加如下配置:
1 | <bean class="org.zhenchao.theia.SpringInitializer"/> |
Spring 框架在启动期间会自动扫描所有被 @Component
注解的配置 Options 类,并完成加载和初始化过程。
使用指南
本小节针对快速接入中的各个步骤进行详细说明。首先来看 步骤 1 ,对于需要注入的 options,需要先实现 Options 接口,或继承 AbstractOptions 抽象类。Options 接口定义如下:
1 | public interface Options extends Serializable { |
其中 Options#update
方法会在成功完成注入时回调,可以用于对配置字段的二次解析。方法 Options#validate
需要由应用自己实现对于配置的合法性校验,该方法会在预注入时调用,如果返回 false 则会放弃后续的正式注入操作,并抛出异常。
然后( 步骤 2 ),需要使用 @Configurable
注解为 options 关联对应的数据源,该注解定义如下:
1 | public Configurable { |
配置项 Configurable#autoConfigure
默认为 true,表示允许 ConfigManager 在初始化时自动实例化并注入配置项值,否则需要由开发人员自己完成实例化,并主动调用 ConfigInjector#configureBean(Options)
方法完成配置项值的注入。
配置项 Configurable#autoload
默认为 false,当设置为 true 时则会在每次配置变更时回调执行 Options#update
方法,而忽略 __commons_config_autoload
配置。该配置项主要应用于加载 raw text 的场景,此时源配置不满足 Properties 文件格式,所以不能简单的添加 __commons_config_autoload=true
配置项以控制是否回调更新,这种场景下可以通过 Configurable#autoload
配置项来默认启用更新。
完成与数据源的关联之后,接下来( 步骤 3 )需要使用 @Attribute
注解为各个字段关联对应的配置项,注解定义如下:
1 | public Attribute { |
各个配置项说明如下:
name
和value
:用于将当前 field 与对应的配置项名称进行关联,如果未指定则以当前属性名称作为配置项名称,强烈建议配置。required
:表示当前配置项是必须的,默认为 true,如果未指定默认值,且对应的配置项缺失则会抛出 ConfigException 异常。defaultValue
:默认值,如果对应的配置项缺失,则采用默认值注入。raw
:是否以原生类型(String 或 Properties)进行注入,需要注意的是,一个 options 中只能定义一个raw=true
的配置项,且与一般的注入方式互斥。converter
:自定义类型转换器,会将 String 类型转换成目标类型后再进行注入。
注解 @Attribute
可以修饰 field,也可以修饰 getter 或 setter 方法,如果未明确指定 name
,则会基于注解的属性或方法(getter 或 setter)自动计算 name
值,但是强烈建议手动配置 name
值,避免出错。类型转换器不是必须的,配置库内置了对以下类型的自动转换:
类型 | 转换器 | 说明 |
---|---|---|
boolean | BooleanConverter | 用于将字符串转换成 boolean 类型 |
char | CharacterConverter | 用于将字符串转换成 char 类型,提取字符串的首字母 |
byte | NumberConverter | 用于将字符串转换成 byte 类型,可以使用 @NumberRadix 指定原始值的进制类型,默认为 10 进制 |
short | NumberConverter | 用于将字符串转换成 short 类型,可以使用 @NumberRadix 指定原始值的进制类型,默认为 10 进制 |
int | NumberConverter | 用于将字符串转换成 int 类型,可以使用 @NumberRadix 指定原始值的进制类型,默认为 10 进制 |
long | NumberConverter | 用于将字符串转换成 long 类型,可以使用 @NumberRadix 指定原始值的进制类型,默认为 10 进制 |
float | NumberConverter | 用于将字符串转换成 float 类型,可以使用 @NumberRadix 指定原始值的进制类型,默认为 10 进制 |
double | NumberConverter | 用于将字符串转换成 double 类型,可以使用 @NumberRadix 指定原始值的进制类型,默认为 10 进制 |
String | StringConverter | 以字符串类型进行注入,区别于 raw 类型的 String 注入,后者使用整个配置文件进行注入 |
Array | ArrayConverter | 用于将字符串按照英文逗号进行分割,并转换成目标数组类型,仅支持一维数组转换 |
Date | DateConverter | 用于将字符串转换成 Date 类型,需要指定 @DatePattern |
Calendar | CalendarConverter | 用于将字符串转换成 Calendar 类型,依赖 DateConverter |
Object | GenericConverter | 将字符串转换成目标类型,相应的类需要具备一个包含 String 类型参数的构造方法 |
以上转换器无需手动指定,配置库会依据目标类型自动检测,如果手动指定了类型转换器,则优先级更高。
最后( 步骤 4 ),需要调用 ConfigManager#initialize
方法初始化和注入所有的配置项,如下:
1 | final ConfigManager configManager = ConfigManager.getInstance(); |
ConfigManager 在执行初始化(即调用 ConfigManager#initialize
方法)时允许指定扫描 Options 的根包名,如果没有设置则会扫描所有的包,推荐设置。
ConfigManager 提供了 ConfigManager#getOptions
方法用于依据类型获取对应的 options 实例。
工具类 Parser 定义了 Parser#toList
和 Parser#toSet
方法,抽象了字符串数组到 List 和 Set 类型的转换,可以依据场景考虑使用。
最后来聊聊监听机制,配置库定义了两种监听器:InjectEventListener 和 UpdateEventListener。其中,InjectEventListener 用于监听注入过程,定义如下:
1 | public interface InjectEventListener extends EventListener { |
此类监听器会在执行注入过程前后被调用,可以调用 ConfigInjector#registerInjectListener
方法和 ConfigInjector#removeInjectListener
方法分别注册和注销监听器。
UpdateEventListener 则用于监听更新过程,定义如下:
1 | public interface UpdateEventListener extends EventListener { |
此类监听器会在调用 Options#update
方法前后被调用,可以调用 ConfigInjector#registerUpdateListener
方法和 ConfigInjector#removeUpdateListener
方法分别注册和注销监听器。
如何扩展
除了内建对 ClassPath 路径下配置的加载,Theia 还允许用户对支持的配置数据源进行扩展。接入一个新的数据源只需要继承 AbstractSourceProvider 抽象类即可,然后在项目的 /META-INF/services
目录下新建一个名为 org.zhenchao.theia.source.provider.SourceProvider
的文件,添加以下内容:
1 | org.zhenchao.theia.source.provider.ClasspathSourceProvider |
配置库基于 jdk 内置的 SPI 机制加载所有的 SourceProvider。最后调用 ConfUtils#registerPrefix
静态方法注册对应的 prefix 标识即可。
下面以从 zookeeper 加载配置为例演示如何实现扩展,首先继承 AbstractSourceProvider 实现一个 ZkSourceProvider,如下:
1 | public class ZkSourceProvider extends AbstractSourceProvider implements SourceProvider { |
然后编写 /META-INF/services/org.zhenchao.theia.source.provider.SourceProvider
文件:
1 | org.zhenchao.theia.source.provider.ClasspathSourceProvider |
最后一步,注册 prefix 标识(不区分大小写):
1 | ConfUtils.registerPrefix("ZK"); |
实现原理
Theia 在设计和实现上主要分为两大模块:
- 从数据源拉取配置数据,并封装成 Properties 对象;
- 基于反射机制从 Properties 对象中获取对应的配置项并注入给目标对象对应的属性上。
同时监听数据源,当数据源更新时以回调的方式更新本地配置。
整体设计图如下:
SourceProvider 用于从数据源加载配置数据并封装成 Properties 对象,同时注册到对应数据源的监听器以监听配置更新。ConfigInjector 会解析 options 配置,并从 Properties 中获取对应的配置项,调用类型转换器 Converter 转成目标类型,并最终注入到目标 options 中。
注意事项
- 对于同一类 options 而言,不允许注册多个实例,否则会抛出 ConfigException 异常。
- 如果希望在注入时支持系统环境变量,可以构造一个
new PropertiesBuilderFactory(true, true)
对象,并调用ConfigInjector#setBuilderFactory
方法予以设置。 - 方法
ConfigInjector#reset
会清空 ConfigInjector 管理的所有 options 实例,但是不会清空对应 options 实例已注入的属性值。 - 方法
ConfigManager#reset
会在ConfigInjector#reset
的基础上清空 ConfigManager 的初始化状态。 - raw 类型是唯一的,且与一般类型互斥。
- 不允许注入 static 类型的属性。
- 自定义类型转换器的优先级高于系统内建的类型转换器,在实现自定义转换器时请保证代码质量。
- 尽量认真实现
Options#validate
方法,对配置的正确性严格控制。 - 被 ConfigInjector 管理的 options 实例是可能被多线程共享的,最好只允许配置库对实例进行修改。
- 请勿在 Listener、
Options#update
和Options#validate
中实现阻塞逻辑。
鸣谢
设计灵感来自 zlib-config,在此表示感谢。