这也是开发 owner 的宗旨。

owner是个功能丰富的 API,但在增加更多的功能之前,我们尽可能的保持它现有基本用法的简单化。

owner API 支持一系列功能,如下:

  • 加载策略
  • 引用属性
  • 参数化属性
  • 类型转换
  • 变量扩展
  • 加载和热加载
  • 可访问性和可变性
  • 程序调试
  • 禁用功能
  • 配置工厂
  • XML支持
  • 事件支持
  • 单例

新特性的开发不希望将已有特性变得复杂,版本的向后兼容也在我们的目标当中。

加载策略 properties 文件和映射接口的关联是通过 owner API 匹配类名和文件名(.properties)来实现的,当然这个逻辑是可以根通过一些额外的注解来实现用户个性化的需求。

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

在上面的例子中, owner 会尝试从不同的 @Sources 中加载属性:

  1. 首先,它会尝试从用户home目录的 ~/.myapp.config 加载属性文件,假如找到,这个文件将会被使用;
  2. 假如上一个失败,它就会尝试从 /etc/myapp.config 加载属性文件,假如找到,这个文件将会被使用;
  3. 作为最后的手段,它将尝试从类路径下以 foo/bar/baz.properties 标志的文件中加载属性;
  4. 假如以上 URL 资源都不存在,java 类将不会与任何文件相关联,只有 @DefaultValue 会被使用。假如没有默认值,将会返回null(就像 java.util.Properties)

在上面的例子中,属性值只会从第一个被找到的文件中加载。一旦有匹配的文件被加载,其他的都会被忽略。这是使用了 @Sources 注解的默认加载方式,你也可以显式的在接口定义时使用 @LoadPolicy(LoadType.FIRST) 注解来指定。

那么假如你需要在一些属性间做一些重载该怎么办?没问题,这完全是可能的,你可以通过 @LoadPolicy(LoadType.MERGE) 注解来实现:

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

这种情况下,所有 URL 中指定的文件都将被查询到,并且第一个定义某重载属性的文件中的值将会被采纳。具体过程如下:

  1. 首先,它会从 ~/.myapp.config 中加载指定的属性,如果找到就将关联的属性值返回;
  2. 然后,从 /etc/myapp.config 中加载指定的属性,如果找到就将关联的属性值返回;
  3. 最后它会从 classpath 路径下的 foo/bar/baz.properties 中加载指定的属性,如果找到就将关联的属性值返回;
  4. 假如指定的属性未在上述任何文件中找到的话,它就会返回用 @DefaultValue 标注的值,否则返回null。

因此基本上我们在多个 properties 文件中执行合并,且第一个properties 文件会重写后面的文件的相同属性值。

@Sources注解通过语法 file:${user.home}/.myapp.config (通过’user.home’ 系统属性得到解决)或者 file:${HOME}/.myapp.config (通过$HOME 环境变量得到解决)考虑系统属性或环境变量。前面例子中使用的“~”是另一个变量扩展的例子,它等同于 ${user.home}。

引用属性 你可以使用另外的机制来加载属性到映射接口中,那就是在调用 ConfigFactory.create() 时人工指定一个属性对象:

  1. public interface ImportConfig extends Config {
  2.   @DefaultValue("apple")
  3.   String foo();
  4.   @DefaultValue("pear")
  5.   String bar();
  6.   @DefaultValue("orange")
  7.   String baz();
  8. }
  9. // 然后...
  10. Properties props = new Properties();
  11. props.setProperty("foo", "pineapple");
  12. props.setProperty("bar", "lime");
  13. ImportConfig cfg = ConfigFactory.create(ImportConfig.class, props); // 属性引用!
  14. assertEquals("pineapple", cfg.foo());
  15. assertEquals("lime", cfg.bar());
  16. assertEquals("orange", cfg.baz());

当然你可以同时指定多个需要引用的属性:

ImportConfig cfg = ConfigFactory.create(ImportConfig.class, props1, props2, ...);

假如 props1 和 props2 同时指定了同一个属性的值,那么首先指定的值将会被采用。

  1. Properties p1 = new Properties();
  2. p1.setProperty("foo", "pineapple");
  3. p1.setProperty("bar", "lime");
  4. Properties p2 = new Properties();
  5. p2.setProperty("bar", "grapefruit");
  6. p2.setProperty("baz", "blackberry");
  7. ImportConfig cfg = ConfigFactory.create(ImportConfig.class, p1, p2); // 属性引用!
  8. assertEquals("pineapple", cfg.foo());
  9. // p1先指定,所以这是 lime 而不是 grapefruit
  10. assertEquals("lime", cfg.bar());
  11. assertEquals("blackberry", cfg.baz());
  12. 此外,你可以非常方便的引用系统属性或环境变量:
  13. interface SystemEnvProperties extends Config {
  14.   @Key("file.separator")
  15.   String fileSeparator();
  16.   @Key("java.home")
  17.   String javaHome();
  18.   @Key("HOME")
  19.   String home();
  20.   @Key("USER")
  21.   String user();
  22.   void list(PrintStream out);
  23. }
  24. SystemEnvProperties cfg = ConfigFactory.create(SystemEnvProperties.class, System.getProperties(), System.getenv());
  25. assertEquals(File.separator, cfg.fileSeparator());
  26. assertEquals(System.getProperty("java.home"), cfg.javaHome());
  27. assertEquals(System.getenv().get("HOME"), cfg.home());
  28. assertEquals(System.getenv().get("USER"), cfg.user());
与加载策略的交互 上面讲到的“引用属性”是属性加载机制中的附加功能。通过代码引入的属性在优先级上要高于通过 @Sources 注释的方式。假设有这样一个场景,你已通过 @Sources 定义你的配置文件,但是你又想让用户在代码中自己指定配置文件,这个时候怎么办呢?
  1. @Sources(...)
  2. interface MyConfig extends Config {
  3.   ...
  4. }
  5. public static void main(String[] args) {
  6.   MyConfig cfg;
  7.   if (args.lenght() > 0) {
  8.     Properties props = new Properties();
  9.     props.load(new FileInputStream(new File(args[0])));
  10.     cfg = ConfigFactory.create(MyConfig.class, userProps);
  11.   } else {
  12.     cfg = ConfigFactory.create(MyConfig.class);
  13.   }
  14. }

在上例中,用户指定的 properties 文件将会重写通过 @Sources 注解加载的 properties 文件中的同名属性。很多命令行工具会使用这种方式,它允许用户在命令行中重写默认配置。(这仅针对1.0.3.1及更高级的版本,在1.0.3.1版本之前引用属性优先级比从 properties 文件中加载的要低。这在1.0.3.1作出改变,并将在以后的版本中都采用这种方式)

参数化属性 owner 另外一个杰出的特性,就是它允许在方法接口上提供参数。属性值应当遵循 java.util.Formatter 类指定的位置计数规则。

  1. public interface Sample extends Config {
  2.   @DefaultValue("Hello Mr. %s!")
  3.   String helloMr(String name);
  4. }
  5. Sample cfg = ConfigFactory.create(Sample.class);
  6. print(cfg.helloMr("Luigi")); // will println 'Hello Mr. Luigi!'
禁用参数扩展 owner 支持用户关闭参数扩展,这可以通过使用 @DisableFeature 注解来实现。
  1. public interface Sample extends Config {
  2.   @DisableFeature(PARAMETER_FORMATTING)
  3.   @DefaultValue("Hello %s.")
  4.   public String hello(String name);
  5.   // 将会忽略参数并返回"Hello %s."
  6. }

@DisableFeature 注解可以用在方法层面,也可以用在接口层面,当使用在接口层面时,将会适用于该接口下的所有方法。

  1. DisableFeature(PARAMETER_FORMATTING)
  2. public interface Sample extends Config {
  3.   @DefaultValue("Hello %s.")
  4.   public String hello(String name);
  5.   // 将会忽略参数并返回"Hello %s."
  6. }

类型转换 owner API 支持原始类型和枚举类型的属性转换。当你定义映射接口时,你可以使用广泛的返回类型,并且他们会自动从 String 类型转换成原始类型或者枚举类型。

  1. // conversion happens from the value specified in the properties files (if available).
  2. int maxThreads();
  3. // conversion happens also from @DefaultValue
  4. @DefaultValue("3.1415")
  5. double pi();
  6. // enum values are case sensitive! java.util.concurrent.TimeUnit is an enum
  7. @DefaultValue("NANOSECONDS");
  8. TimeUnit timeUnit();

你可以在配置接口中将业务对象定义为返回类型,甚至是你自定义的对象,最简单的方式就是使用一个带 String 参数的 public 构造参数来定义你的业务对象:

  1. public class CustomType {
  2.   private final String text;
  3.   public CustomType(String text) {
  4.     this.text = text;
  5.   }
  6.   public String getText() {
  7.     return text;
  8.   }
  9. }
  10. public interface SpecialTypes extends Config {
  11.   @DefaultValue("foobar.txt")
  12.   File sampleFile();
  13.   @DefaultValue("http://owner.aeonbits.org")
  14.   URL sampleURL();
  15.   @DefaultValue("example")
  16.   CustomType customType();
  17.   @DefaultValue("Hello %s!")
  18.   CustomType salutation(String name);
  19. }

owner API 会将”example”传递给 CustomType 的构造函数并返回。

数组和集合

owner 具有对 java 数组和集合的第一级支持,因此你可以这样定义属性:

  1. public class MyConfig extends Config {
  2.   @DefaultValue("apple, pear, orange")
  3.   public String[] fruit();
  4.   @Separator(";")
  5.   @DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
  6.   public int[] fibonacci();
  7.   @DefaultValue("1, 2, 3, 4")
  8.   List<Integer> ints();
  9.   @DefaultValue("http://aeonbits.org, http://github.com, http://google.com")
  10.   MyOwnCollection<URL> myBookmarks();
  11.   // Concrete class are allowed (in this case java.util.Stack)
  12.   // when type is not specified <String> is assumed as default
  13.   @DefaultValue("The Lord of the Rings,The Little Prince,The Da Vinci Code")
  14.   Stack books();
  15. }

你可以通过对接口明确指定 Collection、 List、 Set、 SortedSet 或者 Vector、 Stack、 LinkedList 等具体实现(甚至你自己实现的 java 集合框架接口,只要在实现类中定义一个默认无参的构造函数)来使用数组对象、原始类型或者 java 集合(但是要注意,并不支持 Map 接口及其子接口)。

默认情况下 woner 使用逗号(”,”)来分割数组和集合中的元素,但是你可以通过 @Separator 注解来指定不同的字符。假如你的属性有更复杂的拆分逻辑,你可以通过 @TokenizerClass 注解加上 Tokenizer 接口来自定义断词器类。例如:

  1. public class MyConfig extends Config {
  2.   @Separator(";")
  3.   @DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
  4.   public int[] fibonacci();
  5.   @TokenizerClass(CustomDashTokenizer.class)
  6.   @DefaultValue("foo-bar-baz")
  7.   public String[] withSeparatorClass();
  8. }
  9. public class CustomDashTokenizer implements Tokenizer {
  10.   // this logic can be as much complex as you need
  11.   @Override
  12.   public String[] tokens(String values) {
  13.     return values.split("-", -1);
  14.   }
  15. }

@Separator 和 @TokenizerClass 能够作用在方法层面和接口层面,当只在方法层面指定时只对该方法有效,当在接口层面上指定时则对整个接口类生效。方法层面上指定的注解会覆盖类层面上的注解:

  1. @Separator(";")
  2. public interface ArrayExample extends Config {
  3.   // takes the class level @Separator
  4.   @DefaultValue("1; 2; 3; 4")
  5.   public int[] semicolonSeparated();
  6.   // overrides the class-level @Separator(";")
  7.   @Separator(",")
  8.   @DefaultValue("1, 2, 3, 4")
  9.   public int[] commaSeparated();
  10.   // overrides the class level @Separator(";")
  11.   @TokenizerClass(CustomDashTokenizer.class)
  12.   @DefaultValue("1-2-3-4")
  13.   public int[] dashSeparated();
  14. }

需要注意的是,不能在同一级别上同时使用 @Separator 和 @TokenizerClass,因为这两个注解实际上是在干同一件事!比如下面的例子将会抛出 UnsupportedOperationException 异常:

  1. // @Separator and @TokenizerClass cannot be used together
  2. // on class level.
  3. @TokenizerClass(CustomCommaTokenizer.class)
  4. @Separator(",")
  5. public interface Wrong extends Config {
  6.   // will throw UnsupportedOperationException!
  7.   @DefaultValue("1, 2, 3, 4")
  8.   public int[] commaSeparated();
  9. }
  10. public interface AlsoWrong extends Config {
  11.   // will throw UnsupportedOperationException!
  12.   // @Separator and @TokenizerClass cannot be
  13.   // used together on method level.
  14.   @Separator(";")
  15.   @TokenizerClass(CustomDashTokenizer.class)
  16.   @DefaultValue("0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55")
  17.   public int[] conflictingAnnotationsOnMethodLevel();
  18. }

然而,即使在类层面上有冲突,woner 却能够在方法层面上进行纠正:

  1. // @Separator and @TokenizerClass cannot be used together on class level.
  2. @Separator(";")
  3. @TokenizerClass(CustomDashTokenizer.class)
  4. public interface WrongButItWorks extends Config {
  5.   // but this overrides the class level annotations
  6.   // hence it will work!
  7.   @Separator(";")
  8.   @DefaultValue("1, 2, 3, 4")
  9.   public int[] commaSeparated();
  10. }

当然,我们是不建议这么做的,毕竟这是一种错误的设置方法(可以理解为 owner 自身的 bug 吧)……

@ConverterClass 注解

owner 通过提供 @ConverterClass 注解,使用户可以通过实现 Converter 接口指定自定义的转换逻辑。

  1. interface MyConfig extends Config {
  2.   @DefaultValue("foobar.com:8080")
  3.   @ConverterClass(ServerConverter.class)
  4.   Server server();
  5.   @DefaultValue("google.com, yahoo.com:8080, owner.aeonbits.org:4000")
  6.   @ConverterClass(ServerConverter.class)
  7.   Server[] servers();
  8. }
  9. class Server {
  10.   private final String name;
  11.   private final Integer port;
  12.   public Server(String name, Integer port) {
  13.     this.name = name;
  14.     this.port = port;
  15.   }
  16. }
  17. public class ServerConverter implements Converter<Server> {
  18.   public Server convert(Method targetMethod, String text) {
  19.     String[] split = text.split(":", -1);
  20.     String name = split[0];
  21.     Integer port = 80;
  22.     if (split.length >= 2)
  23.       port = Integer.valueOf(split[1]);
  24.     return new Server(name, port);
  25.   }
  26. }
  27. MyConfig cfg = ConfigFactory.create(MyConfig.class);
  28. Server s = cfg.server(); // will return a single server
  29. Server[] ss = cfg.servers(); // it works also with collections

在上面的例子中,我们调用 servers() 并返回一个 Server 对象数组,ServerConverter 会被调用多次以转换每一个元素,并且在任何情况下 ServerConverter 都将针对单个元素工作。