“全面配置”或“使用 Spring 的 12 要素应用风格配置”

工程 | Josh Long | 2015年1月13日 | ...

在我们开始之前,让我们先建立一些词汇。当我们在 Spring 中谈论配置时,我们通常指的是输入到 Spring 框架的各种ApplicationContext实现中的内容,这些内容帮助容器理解您想要完成的任务。这可能是一个要馈送到ClassPathXmlApplicationContext的 XML 文件,或者是以某种方式进行注释的 Java 类,要馈送到AnnotationConfigApplicationContext

正如12 要素应用宣言中所述,另一种类型的配置是应用程序的任何可能在不同部署(暂存、生产、开发环境等)之间变化的内容,例如服务凭据和主机名。

自从引入PropertyPlaceholderConfigurer类以来,Spring 就一直很好地支持这种应该位于已部署应用程序外部的配置类型。从那时起,Spring 对这种配置类型的支持已经取得了长足的进步,在本篇博文中,我们将探讨这一发展历程。

PropertyPlaceholderConfigurer

Spring 自 2003 年以来就提供了PropertyPlaceholderConfigurer。Spring 2.5 引入了 XML 命名空间支持,以及对属性占位符解析的 XML 命名空间支持。例如,<context:property-placeholder location = "simple.properties"/> 允许我们将 XML 配置中的 bean 定义字面值替换为分配给(外部)属性文件中的键的值(在本例中为simple.properties,它可能位于类路径上或应用程序外部)。此属性文件可能如下所示:

# Database Credentials
configuration.projectName = Spring Framework

Environment 抽象

此解决方案早于在 Spring Framework 3.0 中引入 Java 配置。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还引入了profile的概念。它允许您将标签(profile)赋予 bean 的分组。使用 profile 来描述在不同环境中发生变化的 bean 和 bean 图。您可以一次激活一个或多个 profile。没有分配 profile 的 bean 始终处于激活状态。具有 profiledefault的 bean 只有在没有其他 profile 处于活动状态时才会激活。

Profile 允许您描述需要在一个环境中与另一个环境不同创建的 bean 集。例如,您可以在本地dev profile 中使用嵌入式 H2 javax.sql.DataSource,然后在prod profile 活动时切换到通过 JNDI 查找或从Cloud Foundry中的环境变量读取属性的 PostgreSQL 的javax.sql.DataSource。在这两种情况下,您的代码都能正常工作:您都获得了javax.sql.DataSource,但是关于使用哪个专用实例的决定是由活动的 profile 决定的。

您应该谨慎使用此功能。理想情况下,一个环境与另一个环境之间的对象图应该保持相对固定。

Bootiful 配置

Spring Boot 大大改进了这些情况。Spring Boot 默认情况下会读取src/main/resources/application.properties中的属性。如果激活了 profile,它还会根据 profile 名称自动读取配置文件,例如src/main/resources/application-foo.properties,其中foo是当前 profile。如果Snake YML 库位于类路径上,那么它也会自动加载 YML 文件。是的,请再次阅读这一部分。YML 非常好,非常值得一试!这是一个 YML 文件示例:

configuration:
	projectName : Spring Boot
	someOtherKey : Some Other Value

Spring Boot 还简化了在常见情况下获得正确结果的过程。它使java进程的-D参数和环境变量可用作属性。它甚至会规范化它们,因此环境变量$CONFIGURATION_PROJECTNAME或形式为-Dconfiguration.projectname-D参数都可以使用键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来查看哪些属性键可用于更改内容。这将为您提供基于运行时类路径上存在的类型支持的配置属性列表。随着您添加更多 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的环境变量。

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

spring:
	application:
		name: config-client
		cloud:
			config:
				uri: https://127.0.0.1:8888

当 Spring Cloud 微服务运行时,它会看到其spring.application.nameconfig-client。它将联系配置服务(我们已经告诉 Spring Cloud 它运行在http://localhosst:8080,但这也可以是环境变量),并请求任何配置。配置服务返回包含application.(properties,yml)文件中所有配置值以及config-client.(yml,properties)中任何特定于服务配置的 JSON。它还将加载给定服务和特定配置文件的任何配置,例如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 *作用域*,允许任何 bean 就地重新创建自身(并从配置服务重新读取配置值)。有多种方法可以触发刷新:发送POST请求到http://127.0.0.1:8080/refresh(例如: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 配置支持还包括对安全和加密的一流支持。我会让您自己探索最后一英里,但这相当简单,相当于配置一个有效的密钥。

下一步

我们在这里涵盖了很多内容!有了所有这些,应该很容易打包一个工件,然后将该工件从一个环境移动到另一个环境,而无需更改工件本身。如果您今天要启动一个应用程序,我建议您从 Spring Boot 和 Spring Cloud 开始,特别是现在我们已经了解了它默认为您带来的所有好处。不要忘记查看所有这些示例背后的代码

获取 Spring 新闻

通过 Spring 新闻保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部