“配置万物” 或 “使用 Spring 实现 12-Factor App 风格的配置”

工程 | Josh Long | January 13, 2015 | ...

在开始之前,让我们确定一些术语。当我们在 Spring 中谈论 配置 时,我们通常指的是 Spring 框架各种 ApplicationContext 实现的输入,这些输入帮助容器理解你想要完成的事情。这可能是一个要提供给 ClassPathXmlApplicationContext 的 XML 文件,或者是一些以特定方式注解的 Java 类,要提供给 AnnotationConfigApplicationContext

另一种类型的 配置,如 12-Factor 应用宣言中所描述的,是应用的任何在不同部署环境(staging, production, developer environments 等)之间可能发生变化的部分,例如服务凭证和主机名。

自从引入 PropertyPlaceholderConfigurer 类以来,Spring 就很好地支持了第二种类型的配置,即应该存在于已部署应用外部的配置。从那时起,Spring 对这类配置的支持已经有了很大的进步,在这篇博客中我们将回顾这一演变。

PropertyPlaceholderConfigurer

Spring 自 2003 年起就提供了 PropertyPlaceholderConfigurer。Spring 2.5 引入了 XML 命名空间支持,并随之带来了属性占位符解析的 XML 命名空间支持。例如,<context:property-placeholder location = "simple.properties"/> 允许我们用(外部)属性文件中键对应的值来替换 XML 配置中 bean 定义的字面值(在本例中是 simple.properties,它可以位于 classpath 中或应用外部)。这个属性文件可能看起来像这样

# Database Credentials
configuration.projectName = Spring Framework

Environment 抽象

这个解决方案早于 Java 配置在 Spring Framework 3.0 中的引入。Spring 3 使使用 @Value 注解轻松地将配置值注入到 Java 组件配置中,像这样

@Value("${configuration.projectName}") 
private String projectName; 

Spring 3.1 引入了 Environment 抽象。它在运行中的应用程序与其运行环境之间提供了一点运行时间接性。Environment 充当键值对的映射。你可以通过贡献一个对象来配置从何处读取这些值。在任何你想要的地方注入一个类型为 Environment 的对象,并向它提问。默认情况下,Spring 会加载系统环境键值对,例如 line.separator。你可以使用 @PropertySource 注解告诉 Spring 加载特定文件中的配置键。

package env;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.context.support.*;
import org.springframework.core.env.Environment;

@Configuration
@ComponentScan
@PropertySource("file:/path/to/simple.properties")
public class Application {

	@Bean
	static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
		return new PropertySourcesPlaceholderConfigurer();
	}

	@Value("${configuration.projectName}")
	void setProjectName(String projectName) {
		System.out.println("setting project name: " + projectName);
	}

	@Autowired
	void setEnvironment(Environment env) {
		System.out.println("setting environment: " + 
                      env.getProperty("configuration.projectName"));
	}

	public static void main(String args[]) throws Throwable {
		new AnnotationConfigApplicationContext(Application.class);
	}
}

此示例加载 simple.properties 文件中的值,然后使用 @Value 注解注入一个值 configuration.projectName,接着从 Spring 的 Environment 抽象中再次读取该值。为了能够使用 @Value 注解注入值,我们需要注册一个 PropertySourcesPlaceholderConfigurer。在本例中,输出为 Spring Framework

Environment 还带来了profiles 的概念。它允许你为 bean 分组赋予标签(profiles)。使用 profiles 来描述在不同环境之间变化的 bean 和 bean 图。你可以一次激活一个或多个 profile。没有分配 profile 的 bean 总是会被激活。带有 default profile 的 bean 仅在没有其他 profile 处于活动状态时激活。

Profiles 允许你描述需要在不同环境中以不同方式创建的 bean 集。例如,你可以在本地 dev profile 中使用嵌入式 H2 javax.sql.DataSource,而在 Cloud Foundry 中激活 prod profile 时,切换到通过 JNDI 查找或读取环境变量属性解析的 PostgreSQL 的 javax.sql.DataSource。在两种情况下,你的代码都能正常工作:你获得一个 javax.sql.DataSource,但决定使用哪个特定实例的决策由活动的 profile 或 profile 集合决定。

你应该谨慎使用此功能。理想情况下,不同环境之间的对象图应该保持相当固定。

Bootiful 配置

Spring Boot 显著地改进了配置方式。Spring Boot 默认会读取 src/main/resources/application.properties 中的属性。如果某个 profile 处于活动状态,它还会根据 profile 名称自动读取配置文件,例如 src/main/resources/application-foo.properties,其中 foo 是当前 profile。如果 classpath 中存在 Snake YML 库,它还会自动加载 YML 文件。是的,再读一遍这部分。YML 太棒了,值得一试!这是一个 YML 文件示例

configuration:
	projectName : Spring Boot
	someOtherKey : Some Other Value

Spring Boot 还极大地简化了常见情况下的正确结果。它将 java 进程的 -D 参数和环境变量作为属性提供。它甚至会规范化它们,因此环境变量 $CONFIGURATION_PROJECTNAME-D 形式的参数 -Dconfiguration.projectname 都可以通过键 configuration.projectName 进行访问。

配置值是字符串,如果你的配置值很多,确保这些键本身不会成为代码中的“魔术字符串”可能会很麻烦。Spring Boot 引入了 @ConfigurationProperties 组件类型。使用 @ConfigurationProperties 注解一个 POJO 并指定一个前缀,Spring 将尝试将所有以前缀开头的属性映射到 POJO 的属性。在下面的示例中,configuration.projectName 的值将被映射到该 POJO 的一个实例,所有代码随后都可以注入并解引用该实例来读取(类型安全的)值。通过这种方式,你只需在一个地方进行键值映射。

在下面的示例中,属性将自动从 src/main/resources/application.yml 中解析。

package boot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

// reads a value from src/main/resources/application.properties first
// but would also read:
//  java -Dconfiguration.projectName=..
//  export CONFIGURATION_PROJECTNAME=..

@SpringBootApplication
public class Application {

	@Autowired
	void setConfigurationProjectProperties(ConfigurationProjectProperties cp) {
		System.out.println("configurationProjectProperties.projectName = " + cp.getProjectName());
	}

	public static void main(String[] args) {
		SpringApplication.run(Application.class);
	}
}

@Component
@ConfigurationProperties("configuration")
class ConfigurationProjectProperties {

	private String projectName;

	public String getProjectName() {
		return projectName;
	}

	public void setProjectName(String projectName) {
		this.projectName = projectName;
	}
}

Spring Boot 大量使用了 @ConfigurationProps 机制,允许用户覆盖系统的一些部分。你可以通过将 org.springframework.boot:spring-boot-starter-actuator 依赖项添加到基于 Spring Boot 的 web 应用程序中,然后访问 http://127.0.0.1:8080/configprops 来查看哪些属性键可以用于更改事物。这将根据运行时 classpath 中存在的类型为你提供支持的配置属性列表。随着你添加更多 Spring Boot 类型,你将看到更多属性。

使用 Spring Cloud 配置支持实现集中式、日志记录的配置

到目前为止一切顺利,但目前的方法存在一些不足之处

  • 应用配置的更改需要重启
  • 没有可追溯性:我们如何确定哪些更改已引入生产环境,并在必要时进行回滚?
  • 配置是分散的,不清楚应该到哪里去更改什么。
  • 有时出于安全考虑,配置值应该被加密和解密。对此没有开箱即用的支持。

Spring Cloud,它基于 Spring Boot 构建,并集成了各种用于微服务工作的工具和库,包括 Netflix OSS 栈,提供了配置服务器和该配置服务器的客户端。这些支持结合起来解决了最后三个问题。

让我们来看一个简单的例子。首先,我们将设置一个配置服务器。配置服务器是在一组基于 Spring Cloud 的应用或微服务之间共享的。你需要将它运行起来,部署在某个地方,一次就好。然后,所有其他服务只需要知道在哪里可以找到配置服务。配置服务充当配置键值对的代理,它从在线或本地磁盘的 Git 仓库中读取这些键值对。

package cloud.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

如果你管理得当,那么你的任何服务中唯一的配置应该是告诉配置服务去哪里找到 Git 仓库的配置,以及告诉其他客户端服务去哪里找到配置服务的配置。

这是配置服务的配置,src/main/resources/application.yml

server:
	port: 8888

spring:
	cloud:
		config:
			server:
				git :
					uri: https://github.com/joshlong/microservices-lab-configuration

这告诉 Spring Cloud 配置服务在我的 GitHub 帐户上的 Git 仓库中查找单个客户端服务的配置文件。当然,URI 也可以很轻松地是我的本地文件系统上的一个 Git 仓库。用于 URI 的值也可以是一个属性引用,形式如 ${SOME_URI},它可能引用一个名为 SOME_URI 的环境变量。

运行应用程序,你将可以通过将浏览器指向 http://localhost:8888/SERVICE/master 来验证你的配置服务是否正常工作,其中 SERVICE 是从你的客户端服务 boostrap.yml 中获取的 ID。基于 Spring Cloud 的服务会查找一个名为 src/main/resources/bootstrap.(properties,yml) 的文件,它期望在该文件中找到用于——你猜对了!——引导服务的信息。它期望在 bootstrap.yml 文件中找到的一项内容是指定为属性 spring.application.name 的服务 ID。这是我们的配置客户端的 bootstrap.yml

spring:
	application:
		name: config-client
		cloud:
			config:
				uri: http://localhost:8888

当 Spring Cloud 微服务运行时,它会看到其 spring.application.nameconfig-client。它将联系配置服务(我们告诉 Spring Cloud 它运行在 http://localhosst:8080,尽管这也可能是一个环境变量)并向其请求任何配置。配置服务返回 JSON,其中包含 application.(properties,yml) 文件中的所有配置值以及 config-client.(yml,properties) 中的任何服务特定配置。它还会加载给定服务特定 profile 的任何配置,例如 config-client-dev.properties

这一切都是自动发生的。在下面的示例中,配置值是从配置服务中读取的。

package cloud.client;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	@Autowired
	void setEnvironment(Environment e) {
		System.out.println(e.getProperty("configuration.projectName"));
	}
}

@RestController
@RefreshScope
class ProjectNameRestController {

	@Value("${configuration.projectName}")
	private String projectName;

	@RequestMapping("/project-name")
	String projectName() {
		return this.projectName;
	}
}

ProjectNameRestController@RefreshScope 注解,这是一个自定义的 Spring Cloud scope,允许任何 bean 原地重新创建自己(并从配置服务重新读取配置值)。有几种方法可以触发刷新:向 http://127.0.0.1:8080/refresh 发送 POST 请求(例如:curl -d{} http://127.0.0.1:8080/refresh),使用自动暴露的 JMX 刷新端点,或使用 Spring Cloud Bus。

Spring Cloud Bus 通过基于 RabbitMQ 的总线连接所有服务。这尤其强大。你可以通过向消息总线发送一条消息来告诉一个(或数千个!)微服务刷新自身。这避免了停机,并且比系统地重启单个服务或节点要友善得多。

要看到这一切的实际效果,请运行配置客户端和配置服务器,确保将配置服务器指向你可以控制并进行更改的 Git 仓库。访问 REST 端点并确认你看到 Spring Cloud。然后更改 Git 中的配置文件,并至少 git commit 这些更改。然后触发配置客户端的刷新,并再次访问 REST 端点。你应该会看到更新的值反映出来!

Spring Cloud 配置支持包含了对安全性和加密的一流支持。我将留给你自己去探索这最后一部分,但这相当简单,只需要配置一个有效的密钥。

下一步

我们在这里讲了很多内容!有了这一切,应该可以轻松打包一个 artifact,然后将该 artifact 从一个环境移动到另一个环境,而无需更改 artifact 本身。如果你今天打算开始一个应用,我推荐从 Spring Boot 和 Spring Cloud 开始,尤其是我们已经了解了它默认带来的所有好处。别忘了查看所有这些示例背后的代码

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助你快速前进。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一个简单的订阅。

了解更多

即将举办的活动

查看 Spring 社区所有即将举办的活动。

查看全部