移动端仍可深度探索的领域有哪些?点击看业内代表性技术方案及案例>> 了解详情
写点什么

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

  • 2016-11-27
  • 本文字数:9175 字

    阅读完需:约 1 分钟

核心要点

  • 配置是一个横切性的关注点,跨所有的应用类型,但是并没有 Java 标准来管理配置;
  • Apache Tamaya 是一个孵化项目,旨在提供一个社区协作的配置标准;
  • 如果属性没有定义的话,将会使用默认属性;
  • 如果出现冲突的话,会有默认的合理值,但是默认行为可以通过自定义的映射器进行重写;
  • 支持各种运行时环境的编程 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 服务。
  • Tamaya 所支持的并不局限于String值,可以是任意的 Java 类型,只要我们所注册的PropertyConverters能够从原始的配置值(String类型)衍生出类型化的值就可以,例如将其作为 Date 或 URL。
  • 此外,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 环境;
  • 支持像 Spring 和 OSGi 这样的框架;
  • 支持动态增强(允许我们引入额外的属性源);
  • 过滤可访问的 key/value,这种过滤可以按照单个属性来进行,也可以按照完整的配置来进行。这样的话,就允许 key 和 value 进行变更(例如,密码要进行掩码处理)、省略或添加。或者,我们也可以基于访问控制列表来进行访问的限制;
  • 默认情况下,配置会组织成 PropertySource 的有序列表,每个 PropertySource 都会有一个顺序值。PropertySource 可以提供任意类型的属性,比如系统属性、环境属性、基于文件的属性,以及任意种类的来源和格式。具有较高顺序值的 PropertySource 将会覆盖(默认情况下)具有较低顺序值的 PropertySource 所提供的属性条目(每个 PropertySource 会实现针对某种资源的映射);
  • 正如前面所介绍的,针对相同的 key,重要性较高的值(由具有较高顺序值的 PropertySource 所提供的值)会覆盖掉重要性较低的值。但在有些场景下,由用户来自定义行为会更加合适,为了支持这些场景,我们允许用户自定义联合策略,从而实现更为灵活的覆盖机制。例如,在配置List值的时候,我们可以定义一种行为来联合两个值“a, b”“c, d, e”,从而形成 “a, b, c, d, e”
  • 在配置文件中,支持占位符,例如${sys:a.sys.property}、${url:config.server:8090/v1/a.b.c}、${conf:cross.ref}
  • 支持多种配置格式,比如 Yaml、Json、属性文件等;
  • 能够与各种新的、特定的配置后端集成,比如 e tcd C onsul
  • 支持动态和灵活的资源定位,例如在数据库中、在 Consul 或 etcd 中、在文件中等;
  • 支持动态的配置处理、事件以及可变配置。

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

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

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

  • 组件的实现;
  • 决定可配置的方面;
  • 定义 key 以及合理的默认值,从而能够支持“约定优于配置”;
  • 将提供集成功能的 Tamaya 库添加到你的目标运行时环境中,这种环境可能是 Java EE servlet 容器或 Spring Boot 应用;
  • 通过硬编码的默认值来使用组件,随后可以通过正常的实现类来运行,不需要任何额外的配置。借助 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中。这是默认的位置,该功能是开箱即用的(如果不需要的话,可以将其关闭);
  • 编写自己的 PropertySource ,并通过 JDK 的ServiceLoader机制进行注册,这样的话就能加载任意的配置了(包括动态值);
  • 在 Tamaya 配置中添加对 meta-model 的依赖,它会提供和注册 PropertySource 与 PropertySourceProvider 实例,这些实例是已经实现和配置好的。这个元模型定义了配置的映射、位置和格式等信息。这个文件可以由专门的平台工程团队来创建和管理,并将其全局性地发布到组织中所有的开发人员手中,从而确保应用或服务能够按照统一的方式来进行配置;
  • 在接下来要发布的 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 中,主要的抽象就是 v erticle 。为了让事情尽可能地简单,我们让自己的 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
  • 或者,将配置添加到META-INF/javaconfiguration.properties文件中,并确保该资源对于类路径是可见的。

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

添加测试配置

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

复制代码
public TestConfig extends BasePropertySource{
private static ThreadLocal> 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;
  • 使用 Tamaya 的 etcd 扩展模块。

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

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

总结与展望

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

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

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

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

关于作者

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

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

2016-11-27 15:525437

评论

发布
暂无评论
发现更多内容

第四周-作业一

ray-arch

极客大学架构师训练营

一篇文章搞懂 @weakify 和 @strongify

疯清扬

objective-c weak weakify strongify 循环引用

架构师训练营 - 第八周总结

一个节点

极客大学架构师训练营

架构师训练营第八周总结

xs-geek

极客大学架构师训练营

架构一期第八周作业

Airs

架构师训练营第 1 期 - 第八周作业

Todd-Lee

极客大学架构师训练营

架构训练营第四周课后作业

Sandman

极客大学架构师训练营

典型互联网应用系统使用的技术方案和手段

jorden wang

还有人不知道JVM调优参数?一次性打包发给你

田维常

JVM jvm调优

架构师训练营第二期 Week 4 总结

bigxiang

极客大学架构师训练营

网络模型及性能优化

天天向上

极客大学架构师训练营

第八周作业

Geek_ce484f

极客大学架构师训练营

第八周作业

Geek_ce484f

极客大学架构师训练营

并发压力&响应时间&系统吞吐量

Yangjing

极客大学架构师训练营

性能优化学习笔记

Yangjing

极客大学架构师训练营

第四周作业

Griffenliu

第四周学习总结

Griffenliu

架构师训练营第八周作业

xs-geek

极客大学架构师训练营

《JavaScript高级程序设计》.pdf

田维常

Java 电子书

国产大数据系统通过验收,”核高基”基础软件再下一城

陈泽云

人工智能 大数据 知识产权 操作系统

架构 2 期 - 第四周作业(1)

浮生一梦

极客大学架构师训练营 第四周作业 2组

架构师训练营 - 第八周作业

一个节点

极客大学架构师训练营

第八周作业

orchid9

第八周学习总结

饭桶

架构师训练营第二期 Week 4 作业

bigxiang

极客大学架构师训练营

第八周总结

orchid9

匠心、携手、深耕:5G Capital展现出的无线产业新范式

脑极体

第八周总结

睁眼看世界

极客大学架构师训练营

第八周课后练习

饭桶

第八周作业总结

Geek_ce484f

极客大学架构师训练营

架构师训练营第四周系统架构总结

Sandman

极客大学架构师训练营

配置一次,到处运行:将配置与运行时解耦_Java_Anatole Tresch_InfoQ精选文章