配置一次,到处运行:将配置与运行时解耦

核心要点

\\
  • 配置是一个横切性的关注点,跨所有的应用类型,但是并没有Java标准来管理配置;\\t
  • Apache Tamaya是一个孵化项目,旨在提供一个社区协作的配置标准;\\t
  • 如果属性没有定义的话,将会使用默认属性;\\t
  • 如果出现冲突的话,会有默认的合理值,但是默认行为可以通过自定义的映射器进行重写;\\t
  • 支持各种运行时环境的编程API,比如独立应用、CDI和Spring等。\

Credit Suisse和Oracle曾试图为Java EE的配置创建一个宏伟的JSR标准,现在距离这个计划的破产已经过去了两年的时间。导致这个计划破产的原因很多,我们在这里的关注点也不是讨论它的细节。需要说明的是,尽管官方的JSR从未被JCP执行委员会所批准,但是标准化Java配置的努力却从未停止过。在本文中,我将会关注后续的工作以及这个初始项目的当前状态。

\\

配置标准为何如此重要?

\\

配置是一个通用的横切性关注点,跨所有的应用类型。属性通常会以key = value的形式进行指定,这些属性会以文件的形式来提供并且会加载到一个Java Properties对象中。令人遗憾的是,OSGi、Spring、Java EE、SE以及其他在Java中运行的框架和解决方案都提供了自己的配置API和格式。其中有很多会使用专有的XML格式,另外一些则可能使用更为现代化的格式,比如Yaml。Java EE甚至不支持大多数场景下的动态和远程配置。在应用中,组合使用不同的框架通常都是非常繁琐的,这要归因于不同的配置格式、存放位置以及冗余性。这都会增加不必要的复杂性并且易于出现错误。它会影响到某个应用内部的代码编写,同时还会影响它与周边系统的集成。在过去的二十年间,Java在很多领域都做出了巨大的贡献,为各种类型的应用开发构建了无与伦比的生态系统。这不免令人觉得有些怪异,在配置管理这样一个通用关注点上居然缺乏一个标准API,如果能有一个这样的标准的话,应用程序就不用构建自己的配置方案了,同时也可以简化与不同利益相关者所提供的模块进行集成。

\\

动因与背景

\\

在如何进行配置以及配置到底该是什么样子方面,所涉及到的意见差别很大。因此,配置标准不应该关注于配置什么内容或何时进行配置。以此作为驱动力,我们将已有的知识和实验性代码转移到了一个新的孵化项目中,这个项目的名称叫做Apache Tamaya。我们早期的讨论集中在已有的想法和需求上,但最终,我们后退了一步,从头开始重新定义使用场景,打造了一个崭新的实现。鉴于配置管理是使用最广泛的横切性关注点之一,我们希望和期待这项工作能够成为某种形式的标准,让整个Java生态系统都能从中受益。

\\

Tamaya的一些特性包括:

\\
  • 定义了一组配置注解(tamaya-inject-api),它们可以添加到客户端代码中,从而注入配置的值。注解会按照一种统一的方式来运行,不管你的代码是作为简单老式的Java SE方式运行,还是运行在CDI容器或Spring环境之中。它们甚至还支持OSGi服务。\\t
  • Tamaya所支持的并不局限于String值,可以是任意的Java类型,只要我们所注册的PropertyConverters能够从原始的配置值(String类型)衍生出类型化的值就可以,例如将其作为Date或URL。\\t
  • 此外,Apache Tamaya还提供了无数的扩展和功能集成,这样的话就能根据用户的需求自定义运行时的配置(这样的话,允许用户为他们的系统选择最合适功能,从而解决了配置复杂性所面临的挑战)。这里很棒的一点在于所有的扩展都不会依赖于核心模块,除非运行在测试作用域(test scope)中,这个作用域提供了一个功能性的实现,用来执行我们的测试用例。\

简而言之,借助Apache Tamaya当前发布的0.2-incubating版本,我们有了一个功能完整的配置解决方案,它提供了稳定且经过验证的API/SPI。众多的扩展业已成功证明,它能够以一种可重用和可扩展的方式对配置进行建模。

\\

除此之外,我们还基于注入API定义了一个完整的注解,并提供了对Java EE/CDI的支持(以及多个其他的运行时平台)。所以,现在也许应该重新讨论一下是否要将我们的想法转化为一个JSR。这样的话,所有的Java爱好者和专家(包括本文的读者)都可以了解一下Tamaya并提供反馈。毫无疑问,我们目前并没有上百万美元的营销预算,所以对于您的帮助,我们将会感激之至。

\\

现在,我们更加深入一些,看一下所谓的“一次配置,到处运行”对于开发人员的日常工作到底意味着什么。

\\

使用场景

\\

Apache Tamaya涵盖了各种各样的使用场景,借助我们的模块化结构,你可以很容易地添加任何目前尚不存在的功能。大致看来,Tamaya最为重要的特性包括:

\\
  • 一套完整统一的API,可用于基于编程或注解方式的配置访问,适用于Java SE和EE环境;\\t
  • 支持像Spring和OSGi这样的框架;\\t
  • 支持动态增强(允许我们引入额外的属性源);\\t
  • 过滤可访问的key/value,这种过滤可以按照单个属性来进行,也可以按照完整的配置来进行。这样的话,就允许key和value进行变更(例如,密码要进行掩码处理)、省略或添加。或者,我们也可以基于访问控制列表来进行访问的限制;\\t
  • 默认情况下,配置会组织成PropertySource的有序列表,每个PropertySource都会有一个顺序值。PropertySource可以提供任意类型的属性,比如系统属性、环境属性、基于文件的属性,以及任意种类的来源和格式。具有较高顺序值的PropertySource将会覆盖(默认情况下)具有较低顺序值的PropertySource所提供的属性条目(每个PropertySource会实现针对某种资源的映射);\\t
  • 正如前面所介绍的,针对相同的key,重要性较高的值(由具有较高顺序值的PropertySource所提供的值)会覆盖掉重要性较低的值。但在有些场景下,由用户来自定义行为会更加合适,为了支持这些场景,我们允许用户自定义联合策略,从而实现更为灵活的覆盖机制。例如,在配置List值的时候,我们可以定义一种行为来联合两个值“a, b”“c, d, e”,从而形成 “a, b, c, d, e”;\\t
  • 在配置文件中,支持占位符,例如${sys:a.sys.property}、${url:config.server:8090/v1/a.b.c}、${conf:cross.ref};\\t
  • 支持多种配置格式,比如Yaml、Json、属性文件等;\\t
  • 能够与各种新的、特定的配置后端集成,比如etcdConsul;\\t
  • 支持动态和灵活的资源定位,例如在数据库中、在Consul或etcd中、在文件中等;\\t
  • 支持动态的配置处理、事件以及可变配置。\

尚未发布的特性包括(计划在0.3-incubating版本会涵盖):

\\
  • 支持配置使用度量;\\t
  • 支持配置校验和文档化,例如为CLI -help命令行生成输出;\\t
  • 便于使用的基于YAML的DSL;\\t
  • 用于可视化和管理配置的Web组件.\

接下来,让我们看一下“配置一次,到处运行”的一些样例。使用Tamaya来实现可配置的组件通常会包含如下步骤:

\\
  • 组件的实现;\\t
  • 决定可配置的方面;\\t
  • 定义key以及合理的默认值,从而能够支持“约定优于配置”;\\t
  • 将提供集成功能的Tamaya库添加到你的目标运行时环境中,这种环境可能是Java EE servlet容器或Spring Boot应用;\\t
  • 通过硬编码的默认值来使用组件,随后可以通过正常的实现类来运行,不需要任何额外的配置。借助Tamaya,可以文档化和观察我们的配置,通过添加某项简单的依赖,我们就可以指定生效的文件格式或配置后端。\

实现SupportContact类

\\

我们来考虑一个简单的样例:假设我们正在构建一个组件,这个组件能够提供某个应用的售后支持通讯录信息。SupportContact组件的定义如下所示:

\\
\package com.mycompany;\\public class SupportContact{\  private String supportOrganization;\  private String phoneNumber;\  private String email;\  private boolean supports24x7;\  private Collection supportContacts;\\  public String getSupportOrganization(){\    return supportOrganization;\  }\\  public String getPhoneNumber(){\    return phoneNumber;\  }\\  [...]\}
\\

为了配置这个类,我们可以实现一个构造器来执行配置逻辑:

\\
\ public SupportContact(){\        this.supportOrganization =\                  ConfigurationProvider.getConfiguration()\                   .getOrDefault(“support.organization”, “N/A”);\    this.phoneNumer = \                  ConfigurationProvider.getConfiguration()\                   .getOrDefault(“support.phone”, “N/A”);\    [...]\} 
\\

这种声明式的访问方式确实可以运行,但是大多数开发人员都用过像Spring这样的依赖注入框架,这样的话,他们可能会想要使用tamaya-injection模块来配置该实例:

\\
\ public SupportContact(){\        ConfigurationInjection.getConfigurationInjector()\           .configure(this);\} 
\\

配置代码也可以位于一个外部的配置类中,这样的话,原始的类就不会受到什么影响了。所有内置的Java类型,比如String、booleanint以及java.math.BigDecimalBigInteger类型,默认都是支持的。如果以依赖的方式将tamaya-collections 模块添加进来的话,也能够支持集合类型。Tamaya的ConfigurationInjector是一个将配置注入到POJO中的接口,如果没有配置注解的话,它会按照一种最优的猜测方案来配置所有能够找到属性。它会将包名、类名以及域名组合为候选key的有序列表,并试图查找对应的配置值。所遇到的第一个非null值将会被注入。未定义的属性(所有的候选key均没有匹配到值)将会以警告的形式进行日志记录,但是不会抛出异常。这样的话,就允许我们在代码中,通过标准Java的形式提供默认值,如果需要的话,这些默认值会被属性重写:

\\
\private String supportOrganization = “N/A”;\private String phoneNumber = “N/A”;\private String email = “N/A”;\private boolean supports24x7 = true;\private Collection supportContacts = new ArrayList(); 
\\

更深入地介绍一下,Tamaya的属性映射机制会将这些条目映射为如下的候选key列表:

\\
\com.mycompany.SupportContact.supportOrganization\SupportContact.supportOrganization\supportOrganization\com.mycompany.SupportContact.phoneNumber\SupportContact.phoneNumber\phoneNumber\com.mycompany.SupportContact.email\SupportContact.email\email\com.mycompany.SupportContact.supports24x7\SupportContact.supports24x7\supports24x7\com.mycompany.SupportContact.supportContacts\SupportContact.supportContacts\supportContacts 
\\

我们还可以借助tamaya-injection-api扩展模块所提供的注解,为代码配置更为精确的key。如果我们采用这种方式的话,那么可以为类和属性添加如下所示的注解:

\\
\@Config(“organization”, defaultValue=“N/A“)\private String supportOrganization;\\@Config(value=“phone“, defaultValue=“N/A“)\private String phoneNumber;\\@Config(defaultValue=“N/A“)\private String email;\\@Config(defaultValue=“true“)\private boolean supports24x7;\\@Config(“contacts”, defaultValue=“Admin:admin“)\private Collection supportContacts;\} 
\\

这样的话,我们就可以完全控制Tamaya的属性映射机制如何映射这些条目,也就是:

\\
\support.organization\support.phone\support.email\support.supports24x7\support.contact 
\\

所以,我们可以将这些属性定义到一个简单的 .properties文件中:

\\
\support.organization=MyCompany\support.phone=+41(1)23 553 234\support.email=chief-admin@mycompany.com\support.supports24x7=true\support.contact=Chief Admin:Peter Cole;Advisory Admin:John Doe 
\\

或者在定义一个yaml文件之中:

\\
\---\support:\        organization:      MyCompany\        phone:         +41(1)23 553 234\        email:         chief-admin@mycompany.com\        supports24x7:      true\        contacts:      \        - Chief Admin\          Peter Cole\          Advisory Admin\          John Doe 
\\

Tamaya将会处理一些细节,比如配置的格式、配置所在的位置以及配置项的重写等。如果要观察项目中配置所在的位置,我们可以使用tamaya-model扩展,它能够提供一个已定义配置属性的列表,并且能够评估它们的使用的情况。

\\

Tamaya非常灵活,它可以与你(或你的客户)所使用的任意后端相连接,所以开发人员可以只关注配置“什么”的问题。至于“如何”配置则成为一个集成点,可以进行单独处理。

\\
注意:
\\

在实践中,Tamaya提供了多种与配置后端集成的可选方案:

\\
  • 在(测试)类路径中,可以将一些测试配置添加到META-INF/javaconfiguration.properties中。这是默认的位置,该功能是开箱即用的(如果不需要的话,可以将其关闭);\\t
  • 编写自己的PropertySource,并通过JDK的ServiceLoader机制进行注册,这样的话就能加载任意的配置了(包括动态值);\\t
  • 在Tamaya配置中添加对meta-model的依赖,它会提供和注册PropertySource与PropertySourceProvider实例,这些实例是已经实现和配置好的。这个元模型定义了配置的映射、位置和格式等信息。这个文件可以由专门的平台工程团队来创建和管理,并将其全局性地发布到组织中所有的开发人员手中,从而确保应用或服务能够按照统一的方式来进行配置;\\t
  • 在接下来要发布的0.3-incubating中,会规划一个元模型DSL,通过它,我们能够像配置日志那样来描述运行时的系统配置。借助这种方式,能够定义一组profile和格式,并且能够对其进行排序,每个profile会分配一个PropertySource列表和一个基础的顺序值。定义为“defaults”的profile会始终纳入考虑的范围。如果没有设置profile的话,那么“default-active”会定义默认活跃的profile(会作为默认profile的补充),“evaluation”能够定义要采用什么方式来确定当前活跃的profile(使用Tamaya的占位符机制)。作为样例,以下的Yaml定义了一个完整的配置系统:\
\TAMAYA:\ PROFILES_DEF:\  - profiles:            DEFAULTS,DEV,TEST,PTA,PROD\  - supports-multi:      false\  - defaults:            DEFAULTS\  - default-active:      DEV\  - evaluation:          sys-property:ENV, env-property:ENV\\ FORMAT-DEF:\  - formats: yaml, properties\  - suffixes: yml,yaml,properties\\ PROFILES:\   :\     - sources:\       - named:env-properties    # provider name, or class name\       - named:main-args\       - classpath:META-INF/defaults/**/*.SUFFIX\       - file:${config.dir}/defaults**/*.SUFFIX             ?optional=true\       - classpath:META-INF/config/**/*.SUFFIX\       - named:sys-properties   \   DEFAULTS:\     - prio:       0        # optional\     - filters:\       - include:DEFAULTS\\.*?ignoreCase=true\       - exclude:_\\.*   # removes all meta-entries\   DEV:\    - prio:        100          # optional\    - filters:\        - include:DEV\\.*?ignoreCase=true\   [...]\   PROD:\    - prio:        1000         # optional\    - filters:\        - include:PROD\\.*?ignoreCase=true 
\\

类型化的配置模板

\\

除了为类添加注解以外,还有一个替代方案,那就是使用Tamaya的模板特性,它会使用类型化的接口来定义配置。例如,为了配置一个简单的Web服务器,我们可以编写如下的配置接口,并通过Tamaya注解对其进行增强:

\\
\@ConfigSection(“server”)\public interface ServerConfig{\    @Config(defaultValue=”8080”);\    int getPort();\    @Config(defaultValue=”/”);\    int getRootContext();\} 
\\

借助CDI,这个配置可以直接进行注入:

\\
\@Inject\ServerConfig serverConfig;
\\

Tamaya将会基于注解和当前的配置后端来实现这个bean。

\\

使用SupportContact组件

\\

简单的Java SE

\\

正如在前面所看到的,我们可以通过Tamaya的injection模块将属性注入到Java SE应用中:

\\
\SupportContact contact = new SupportContact();\ConfigurationInjection.getConfigurationInjector()\.configure(contact); 
\\

Java EE

\\

在Java EE中,CDI是可选的生命周期管理器,所以我们告诉CDI来“注入”我们的组件(CDI实际上会使用 @Dependent pseudo-scope来创建一个组件)。为了实现该功能,我们必须要添加tamaya-injection-cdi扩展模块,它会通过Tamaya的配置注入机制来使用CDI,所以最终我们只是让CDI来实现类的注入:

\\
\@Inject\private SupportContact contact; 
\\

Spring

\\

Spring同样会采用CDI的方案,不过它会按照Spring的方式。我们只需添加 tamaya-spring集成模块即可,这样所有的Spring bean就都可以进行配置了。所以,你可以将SupportContact bean添加到Spring上下文中,在注入之前,它已经在暗中配置完成了:

\\
\@AutoWire\private SupportContact contact;
\\

Vertx

\\

在最后一个样例中,我们希望为你展现将Tamaya的配置灵活地添加到你的项目中是多么地简单。因此,我们看一下vertx.io。在Vertx中,主要的抽象就是verticle。为了让事情尽可能地简单,我们让自己的verticle扩展一个可重用的基础类,将其称之为ConfigurableVerticle

\\
\public abstract class ConfigurableVerticle extends AbstractVerticle{  \    public ConfigurableVerticle(){\        ConfigurationInjection.getConfigurationInjector()\            .configure(this);\    } \} 
\\

现在,就可以通过Tamaya的注解来配置我们的verticle:

\\
\public class MyVerticle extends ConfigurableVerticle{\    @Config(value=”monitoring.count-limit”, defaultValue=”100”)\    private int countLimit;\    [...]\} 
\\

当然,这是一个非常简单化的例子,但是它展示了在配置组件时,能够与它的目标运行时环境进行解耦,而且不会给开发人员的日常工作带来明显的复杂性。

\\

连接配置后端

\\

使用默认的javaconfiguration.properties

\\

Tamaya默认会读取环境属性、系统属性,并从类路径下读取META-INF/javaconfiguration.properties。系统属性具有最高的优先级,会否决掉其他的配置方案。因此,我们可以按照如下的方式来配置组件:

\\
  • 设置对应的环境属性。比如,当运行在Docker中的时候,我们可以添加如下的环境属性:\
\ ENV support.supportOrganization foo\      ENV support.phoneNumber bar\      ENV support.email foo2\      ENV support.supportContacts bar2
\\
  • ​设置系统属性,比如:-Dsupport.supportOrganization=Tamaya\\t
  • 或者,将配置添加到META-INF/javaconfiguration.properties文件中,并确保该资源对于类路径是可见的。\

这依然非常简单,正如我在前文所述,我们正在为 0.3-incubating版本添加一个元配置DSL,它将会使得配置更加灵活。

\\

添加测试配置

\\

测试领域经常会滋生各种问题,在配置内容全局共享,测试需要在多线程间并行运行时更是如此。它本身并不是什么问题,不过在测试中,我们一般都希望测试各种各样的配置,以确保组件的行为符合预期,在这样的场景中共享配置可能会导致竞态条件的产生,从而使测试结果失效。解决这种问题的一种可行方式(可能会在下一个发布版本中引入该特性)就是实现并注册一个具有很高顺序值的PropertySource。这个顺序值会覆盖掉其他属性源所提供的所有属性,所以只需要确保我们的PropertySource内部使用ThreadLocal来实现隔离就可以了,这个PropertySource依然是跨线程共享的。再结合一些静态的访问器方法,我们的测试配置就可以使用了:

\\
\public TestConfig extends BasePropertySource{\    private static ThreadLocal\u0026gt; maps = \                                               new ThreadLocal(){\                            [...]\    };\\    public TestConfig(){\       super(100000); // default ordinal\    }\\    @Override\    public Map getProperties(){\        return maps.get();\    }\\    public static String put(String key, String value){\        return TestConfig.maps.put(key, value);\    }\\    public static String remove(String key){\        return TestConfig.maps.remove(key);\    }\} 
\\

我们可以通过ServiceLoader来注册PropertySource,只需将下面这行代码添加到META-INF/services/org.apache.tamaya.spi.PropertySource中即可:

\\

com.mycompany.TestConfig

\\

现在,我们可以在JUnit代码中直接使用它了:

\\
\public class TestSupportContact{\\  @Before\  public void setup(){\      TestConfig.put(“support.email”, “test@127.0.0.1”);\  }\\  @Test\  public void test(){\      [...]\  }\\  @After\  public void teardown(){\      TestConfig.remove(“support.email”);\  }\} 
\\

添加远程后端

\\

在我们的样例组件中,配置都是基于类路径资源或本地测试文件的,对于大多数场景而言,这都是可行的解决方案。但是,我们假设一下,在应用完成之后,客户了解到etcd是一种分布式的key/value存储,并要求我们支持将etcd作为后端。通过使用Tamaya,我们有多种可选方案:

\\
  • 实现和注册自定义的PropertySource;\\t
  • 使用Tamaya的etcd扩展模块。\

其中,第一种方式非常类似于我们在前面所看到的测试场景。

\\

第二种方式也并不复杂,我们只需要将所需的条目添加到Tamaya的扩展模块中即可(如果有冲突或特定的映射需求的话,我们可以设置一些系统属性来调整模块的行为,比如要查找的etcd服务器)。这里的关键点在于我们依然不需要修改任何代码。我们的Java代码对于它的配置以及这些配置间是如何覆盖的完全不知情。也就是说,我们成功地将配置和它的后端进行了解耦:“一次配置,到处运行”。

\\

总结与展望

\\

为Java生态系统提供标准的配置API会带来很多的好处,我们所看到的只是其中很少的几个。在缺少标准的情况下,每个人都会按照自己的方式来完成这方面的工作。本文的一个主要目标就是引发一些公开的讨论并收集读者的意见:在JavaOne会议上我们是否应该将其作为一个新的JSR。从内容来看,它应该是相当紧凑的:

\\
  • 我们所讨论的一组注解,用到了CDI;\\t
  • 在CDI不可用的场景下,提供了一个最小的Java API;\\t
  • 对于自定义的附加场景,有一个SPI来提供扩展点。Tamaya SPI必须要非常灵活,同时还要保持其简洁性,所以这是非常适合进行讨论的关注点。\

一般而言,它可能会成为一个Java EE JSR,基于Apache所做的工作形成“新的Java EE配置JSR”。鉴于它与CDI 1.1基本兼容,对于Java EE平台没有什么额外的需求,我认为它有可能会加入到当前Java EE发布版本的规划中……

\\

不管JSR的决策如何,Tamaya正在寻找新的提交者。所以,如果你对它感兴趣的话,请与我们联系!

\\

关于作者

\\

6916bbd48f80e0720bc0c44d86abf9f3.jpgAnatole Tresch——在苏黎世大学完成其信息科学和经济学的学业之后,Anatole在一家咨询公司担任过多年的执行合伙人(Managing Partner),不管是在大型企业还是小企业中,他对于Java生态系统的所有领域都有着广泛的经验。目前,Anatole担任Trivadis的首席咨询顾问,主要关注Java现金\u0026amp;通货(Java Money \u0026amp; Currency)以及配置领域。Anatole还是Oracle Star Spec Lead、PPMC成员以及Apache Tamaya项目的创立者。

\\\\

查看英文原文:Configure Once, Run Everywhere: Decoupling Configuration and Runtime

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值