OWNER 初探

  1. 基本用法
  2. 加载策略
  3. 参数化
  4. 自定义类型
  5. 变量表达式

OWNER是一个Java库,目标是最大限度的减少应用程序中处理Java properties的代码。

主要功能

  • 加载策略:OWNER通过匹配接口类名和properties文件名自动解析并映射;也可以通过注解定制properties文件名。
  • 导入properties:另外一种加载properties文件到映射接口的方法。
  • 参数化properties:另外一个实用功能,给接口方法提供参数,通过参数配置。
  • 类型转换:支持从String类型到基本类型和枚举类型的转换。
  • 变量扩展:引用properties中的其他属性。
  • 热加载:支持热加载。
  • 可访问和可变:可以继承Config的子接口Accessible或者Mutable实现属性的可访问和可变。
  • 调试:支持调试功能。
  • 禁用功能:可禁用引起问题的功能。
  • 配置ConfigFactory:ConfigFactory也是可配置的。
  • XML支持:支持XML配置。
  • 事件支持:OWNER实现了功能丰富的事件系统,使你知道热加载的发生和属性变化。
  • 单例模式:配置信息在一个应用中是单例的。

OWNER同样是开源的, 我们可以使用maven来引用它

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.aeonbits.owner</groupId>
<artifactId>owner</artifactId>
<version>1.0.8</version>
</dependency>
</dependencies>

或者使用java8版本

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.aeonbits.owner</groupId>
<artifactId>owner-java8</artifactId>
<version>1.0.6</version>
</dependency>
</dependencies>

基本用法

我们现在在MAVEN项目中测试一下
首先我们在test\src\main\java\ownerTest目录下创建一个配置类

1
2
3
4
5
6
7
8
9
10
package ownerTest;

import org.aeonbits.owner.Config;

public interface ServerConfig extends Config {
int port();
String hostname();
@DefaultValue("42")
int maxThreads();
}

然后在test\src\main\resources\ownerTest目录下创建配置文件ServerConfig.properties

1
2
3
port=80
hostname=foobar.com
maxThreads=100

然后我们书写一个测试类

1
2
3
4
5
6
7
8
9
10
11
package ownerTest;

import org.aeonbits.owner.ConfigFactory;

public class Main {

public static void main(String[] args) {
ServerConfig cfg = ConfigFactory.create(ServerConfig.class);
System.out.println("Server " + cfg.hostname() + ":" + cfg.port() + " will run " + cfg.maxThreads());
}
}

这时OWNER会自动地将ServerConfig.properties配置文件匹配到ServerConfig上, 输出结果为

1
Server foobar.com:80 will run 100

我们看到在ServerConfig里我们使用了一个@DefaultValue注解, 当在配置文件里找不到这个值的时候, 这个注解值会为我们设置上注解里的默认值.

如果在配置文件里找不到这个设置也没有添加@DefaultValue会产生一个空指针异常

有时候我们在配置文件里会使用server.http.port=80的配置, 那么这种情况下我们就在使用@Key注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package ownerTest;

import org.aeonbits.owner.Config;

public interface ServerConfig extends Config {
@Key("server.http.port")
int port();

@Key("server.host.name")
String hostname();

@Key("server.max.threads")
@DefaultValue("42")
int maxThreads();
}

我们的配置文件如下

1
2
3
server.http.port=80
server.host.name=foobar.com
server.max.threads=100

加载策略

正如上文所说, OWNER是按照classpath去自动匹配配置类和配置文件的, 但是其实我们可以自定义配置文件的加载策略, 如下例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Sources({ "file:~/.myapp.config",
"file:/etc/myapp.config",
"classpath:foo/bar/baz.properties" })
public interface ServerConfig extends Config {
@Key("server.http.port")
int port();

@Key("server.host.name")
String hostname();

@Key("server.max.threads");
@DefaultValue("42")
int maxThreads();
}

我们使用了@Sources指定配置文件的路径, 按照上面的代码, 它依次按照下面的流程进行文件匹配,一旦匹配成功就进行加载忽略后面的文件

  1. file:~/.myapp.config 从home目录开始查找
  2. file:/etc/myapp.config 从绝对目录进行查找
  3. classpath:foo/bar/baz.properties classpath从classpath中查找

其实上面的加载策略称为LoadType.FIRST(完整注解@LoadPolicy(LoadType.FIRST)). 我们还有其他选择LoadType.MERGE, 示例如下:

1
2
3
4
5
6
7
@LoadPolicy(LoadType.MERGE)
@Sources({ "file:~/.myapp.config",
"file:/etc/myapp.config",
"classpath:foo/bar/baz.properties" })
public interface ServerConfig extends Config {
...
}

上面的加载策略是, 不管当前路径下是否找到了都会进行路径下查找, 但与LoadType.FIRST不同的是, 当找到之后它会替换之前知道的配置文件.

TODO 是局部替换还是整个文件一起替换呢?

参数化

我们还可以向配置里传递参数

1
2
3
4
5
6
7
public interface Sample extends Config {
@DefaultValue("Hello Mr. %s!")
String helloMr(String name);
}

Sample cfg = ConfigFactory.create(Sample.class);
print(cfg.helloMr("Luigi")); // will println 'Hello Mr. Luigi!'

OWNER还为我们提供了@DisableFeature注解, 让我们关闭参数化功能. 这个注解可以用在类或者方法的级别上

自定义类型

当我们使用OWNER的时候, 不仅仅可以使用原生类型, 还可以使用数组, 集合甚至是自定义类型

下来我们自定义一个类型

1

下面我们定义一个数组和集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyConfig extends Config {

@DefaultValue("apple, pear, orange")
public String[] fruit();

@Separator(";")
@DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
public int[] fibonacci();

@DefaultValue("1, 2, 3, 4")
List<Integer> ints();

@DefaultValue(
"http://aeonbits.org, http://github.com, http://google.com")
MyOwnCollection<URL> myBookmarks();

// Concrete class are allowed (in this case java.util.Stack)
// when type is not specified <String> is assumed as default
@DefaultValue(
"The Lord of the Rings,The Little Prince,The Da Vinci Code")
Stack books();

}

OWNER默认使用,分割元素. 但是我们可以通过@Separator(";")指定使用;进行切割, 需要注意的是OWNER只支持数组, 集合, Java原生类型, 并不支持Map.

支持的集合有Collection, List, Set, SortedSet

虽然我们可以使用@Separator(";")进行切割, 但是如果我们有更复杂的切割逻辑的话, 这可能就不再符合需求了, 我们可以使用@TokenizerClass来实现更复杂的切割逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyConfig extends Config {

@Separator(";")
@DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
public int[] fibonacci();

@TokenizerClass(CustomDashTokenizer.class)
@DefaultValue("foo-bar-baz")
public String[] withSeparatorClass();

}

public class CustomDashTokenizer implements Tokenizer {

// this logic can be as much complex as you need
@Override
public String[] tokens(String values) {
return values.split("-", -1);
}
}

虽然 @Separator(";")@TokenizerClass(CustomDashTokenizer.class)都可以在方法和类的级别上进行注解, 但是他们不允许同时出现在同一个级别上, 而且当分别出现在了方法和类的级别上后, 方法上的注解会替换类上的注解.

OWNER还提供了@ConverterClass注解来实现更加复杂的转换逻辑

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
interface MyConfig extends Config {
@DefaultValue("foobar.com:8080")
@ConverterClass(ServerConverter.class)
Server server();

@DefaultValue(
"google.com, yahoo.com:8080, owner.aeonbits.org:4000")
@ConverterClass(ServerConverter.class)
Server[] servers();
}

class Server {
private final String name;
private final Integer port;

public Server(String name, Integer port) {
this.name = name;
this.port = port;
}
}

public class ServerConverter implements Converter<Server> {
public Server convert(Method targetMethod, String text) {
String[] split = text.split(":", -1);
String name = split[0];
Integer port = 80;
if (split.length >= 2)
port = Integer.valueOf(split[1]);
return new Server(name, port);
}
}

MyConfig cfg = ConfigFactory.create(MyConfig.class);
Server s = cfg.server(); // will return a single server
Server[] ss = cfg.servers(); // it works also with collections

OWNER 支持的全部自动转换类型

  • 原生类型: boolean, byte, short, integer, long, float, double.
  • 枚举
  • java.lang.String
  • java.net.URL, java.net.URI.
  • java.io.File
  • java.lang.Class
  • 公有构造器只有一个java.lang.String的类
  • 公有构造器只有一个java.lang.Object的类
  • public static修饰, 签名为valueOf(java.lang.String)返回自身的方法
  • 带有上述元素的数组
  • 带有上述类的集合(Set, List, SortedSet or concrete implementations like LinkedHashSet). Map and sub-interfaces are not supported.

变量表达式

OWNER还提供了一个非常霸道的功能 —— 变量表达式, 参考如下配置文件

1
2
3
4
5
story=The ${animal} jumped over the ${target}
animal=quick ${color} fox
target=${target.attribute} dog
target.attribute=lazy
color=brown

然后定义一个配置类

1
2
3
public interface ConfigWithExpansion extends Config {
String story();
}

猜猜会输出什么, 对了

1
The quick brown fox jumped over the lazy dog

我们可以在配置文件里引用其他的配置

同样的我们还可以在配置类完成这样的功能

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 ConfigWithExpansion
extends Config {

@DefaultValue(
"The ${animal} jumped over the ${target}")
String story();

@DefaultValue("quick ${color} fox")
String animal();

@DefaultValue("${target.attribute} dog")
String target();

@Key("target.attribute")
@DefaultValue("lazy")
String targetAttribute();

@DefaultValue("brown")
String color();
}

ConfigWithExpansion conf = ConfigFactory
.create(ConfigWithExpansion.class);

String story = conf.story();

如果我们不需要开启变量表达式的话, 我们可以使用@DisableFeature(VARIABLE_EXPANSION)

1
2
3
4
5
6
7
8
9
10
public interface Sample extends Config {
@DefaultValue("Earth")
String world();

@DisableFeature(VARIABLE_EXPANSION)
@DefaultValue("Hello ${world}.")

// will return the string "Hello ${world}."
String sayHello();
}