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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Configurable(Constants.CP_PREFIX + "configurable_options") public class ExampleOptions extends AbstractOptions { private static final long serialVersionUID = -8145624960779711094L ; @Attribute(name = "myFiles") private File[] files; @Attribute(defaultValue = "15") private int number; @Attribute(name = "property.message") private String propMessage; @Attribute(defaultValue = "1780000") public long longValue; @Attribute(name = "another.long.value", defaultValue = "1000000") public long anotherLongValue; private Double floatingPointNumber; @Attribute private String fieldMessage; @Attribute private Boolean trueFalse; @Attribute(name = "list", converter = ListConverter.class) public List<String> list; @Attribute(converter = SetConverter.class) public Set<String> set; @Override public void update () { } @Override public boolean validate () { } }
初始化配置管理器:
1 2 3 4 5 6 7 final ConfigManager configManager = ConfigManager.getInstance();configManager.initialize("org.zhenchao.theia.example" ); final ExampleOptions options = configManager.getOptions(ExampleOptions.class);System.out.println(options.getPropMessage());
好啦,就这么简单,接下去就可以愉快的使用配置项啦!
如果是 Spring 应用,则只需要在对应的 Options 类上添加 @Component
注解,并在 Spring 配置文件中添加如下配置:
1 <bean class ="org.zhenchao.theia.SpringInitializer" />
Spring 框架在启动期间会自动扫描所有被 @Component
注解的配置 Options 类,并完成加载和初始化过程。
使用指南
本小节针对快速接入中的各个步骤进行详细说明。首先来看 步骤 1 ,对于需要注入的 options,需要先实现 Options 接口,或继承 AbstractOptions 抽象类。Options 接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface Options extends Serializable { void update () ; boolean validate () ; }
其中 Options#update
方法会在成功完成注入时回调,可以用于对配置字段的二次解析。方法 Options#validate
需要由应用自己实现对于配置的合法性校验,该方法会在预注入时调用,如果返回 false 则会放弃后续的正式注入操作,并抛出异常。
然后( 步骤 2 ),需要使用 @Configurable
注解为 options 关联对应的数据源,该注解定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public @interface Configurable { String resource () default "" ; String value () default "" ; boolean autoConfigure () default true ; boolean autoload () default false ; }
配置项 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public @interface Attribute { String name () default "" ; String value () default "" ; boolean required () default true ; String defaultValue () default "" ; boolean raw () default false ; Class<? extends Converter> converter() default VoidConverter.class; }
各个配置项说明如下:
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 final ConfigManager configManager = ConfigManager.getInstance();final int count = configManager.initialize("org.zhenchao.theia.manager" );Assert.assertEquals(4 , count); Assert.assertNotNull(configManager.getOptions(Options1.class)); Assert.assertNotNull(configManager.getOptions(Options2.class)); Assert.assertNotNull(configManager.getOptions(Options3.class)); Assert.assertNotNull(configManager.getOptions(Options4.class)); Assert.assertNull(configManager.getOptions(Options5.class)); Assert.assertSame(configManager.getOptions(Options1.class), configManager.getOptions(Options1.class)); Assert.assertNotSame(configManager.getOptions(Options1.class), configManager.getOptions(Options2.class)); final Options5 options5 = new Options5();configManager.getInjector().configureBean(options5); Assert.assertNotNull(configManager.getOptions(Options5.class)); Assert.assertSame(options5, configManager.getOptions(Options5.class));
ConfigManager 在执行初始化(即调用 ConfigManager#initialize
方法)时允许指定扫描 Options 的根包名,如果没有设置则会扫描所有的包,推荐设置。
ConfigManager 提供了 ConfigManager#getOptions
方法用于依据类型获取对应的 options 实例。
工具类 Parser 定义了 Parser#toList
和 Parser#toSet
方法,抽象了字符串数组到 List 和 Set 类型的转换,可以依据场景考虑使用。
最后来聊聊监听机制,配置库定义了两种监听器:InjectEventListener 和 UpdateEventListener。其中,InjectEventListener 用于监听注入过程,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface InjectEventListener extends EventListener { void prevHandle (final Options options) ; void postHandle (final Options options) ; }
此类监听器会在执行注入过程前后被调用,可以调用 ConfigInjector#registerInjectListener
方法和 ConfigInjector#removeInjectListener
方法分别注册和注销监听器。
UpdateEventListener 则用于监听更新过程,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface UpdateEventListener extends EventListener { void prevHandle (Options options) ; void postHandle (Options options) ; }
此类监听器会在调用 Options#update
方法前后被调用,可以调用 ConfigInjector#registerUpdateListener
方法和 ConfigInjector#removeUpdateListener
方法分别注册和注销监听器。
如何扩展
除了内建对 ClassPath 路径下配置的加载,Theia 还允许用户对支持的配置数据源进行扩展。接入一个新的数据源只需要继承 AbstractSourceProvider 抽象类即可,然后在项目的 /META-INF/services
目录下新建一个名为 org.zhenchao.theia.source.provider.SourceProvider
的文件,添加以下内容:
1 2 org.zhenchao.theia.source.provider.ClasspathSourceProvider // your source provider class name here
配置库基于 jdk 内置的 SPI 机制加载所有的 SourceProvider。最后调用 ConfUtils#registerPrefix
静态方法注册对应的 prefix 标识即可。
下面以从 zookeeper 加载配置为例演示如何实现扩展,首先继承 AbstractSourceProvider 实现一个 ZkSourceProvider,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 public class ZkSourceProvider extends AbstractSourceProvider implements SourceProvider { private final CuratorFramework zkClient; private final Set<Source> sourceRegistry = new HashSet<>(); public ZkSourceProvider () { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000 , 3 ); this .zkClient = CuratorFrameworkFactory.newClient("127.0.0.1:2181" , retryPolicy); this .zkClient.start(); } @Override protected Properties doLoadProperties (final Source source, final PropertiesBuilder builder) throws ConfigException { final Class<?> optionsClass = source.getOptionsClass(); final String resourceName = this .resourceName(source); log.info("Load zk configuration, resource[{}], options[{}]." , resourceName, optionsClass); try { final String zkPath = this .toZkPath(resourceName); final byte [] bytes = zkClient.getData().forPath(zkPath); if (null == bytes || 0 == bytes.length) { log.warn("No zk property value resolved, path[{}]." , zkPath); return builder.build(); } final String data = Bytes.toString(bytes); if (StringUtils.isBlank(data)) { log.warn("No zk property value resolved, path[{}]." , zkPath); return builder.build(); } if (log.isDebugEnabled()) { log.debug("Get zk property, path[{}], value[{}]." , zkPath, data); } final Properties properties = this .toProperties(data); if (!properties.isEmpty()) { builder.addAll(ConfUtils.toMap(properties)); } return builder.build(); } catch (Throwable t) { log.error("Load zk configuration error, resource[{}], optionsClass[{}]" , resourceName, optionsClass, t); throw new ConfigException("load zk configuration error, " + "resource: " + resourceName + ", options: " + optionsClass, t); } } @Override public void postLoad (Source source) { if (!this .tryRegisterListener(source)) { throw new IllegalStateException("register zk listener error, " + "resource: " + source.getResourceName() + ", options: " + source.getOptionsClass()); } } @Override protected String resourceName (Source source) { String resourceName = source.getResourceName(); Validate.isTrue(ConfUtils.isZkResource(resourceName), "invalid zk resource name: " + resourceName); return resourceName; } @Override public boolean support (Source source) { return StringUtils.startsWithIgnoreCase(super .resourceName(source), Constants.ZK_PREFIX); } @Override public int priority () { return 0 ; } private String toZkPath (String resourceName) { return resourceName.substring(Constants.ZK_PREFIX.length()).trim(); } private boolean tryRegisterListener (final Source source) { if (sourceRegistry.contains(source)) { return true ; } final String zkPath = this .toZkPath(this .resourceName(source)); log.info("Register zk data change listener for path[{}]." , zkPath); try { zkClient.getData() .usingWatcher((CuratorWatcher) event -> { final Watcher.Event.EventType eventType = event.getType(); if (!Watcher.Event.EventType.NodeDataChanged.equals(eventType)) { log.info("Uninterested zk event type: {}, and ignore it." , eventType); return ; } final String eventPath = event.getPath(); try { if (zkPath.equals(eventPath)) { log.info("Refresh zk configuration, path[{}]." , eventPath); ConfigInjector.getInstance().reload(source); } else { log.debug("[{}] unexpected change, and ignore it, path[{}]." , zkPath, eventPath); } } catch (Throwable t) { throw new ConfigException("refresh zk configuration error, path: " + eventPath, t); } }) .forPath(zkPath); } catch (Throwable t) { log.error("Try register zk data change listener error, path: {}" , zkPath, t); return false ; } sourceRegistry.add(source); return true ; } }
然后编写 /META-INF/services/org.zhenchao.theia.source.provider.SourceProvider
文件:
1 2 org.zhenchao.theia.source.provider.ClasspathSourceProvider org.zhenchao.theia.source.provider.ZkSourceProvider
最后一步,注册 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 ,在此表示感谢。