领先一步
VMware 提供培训和认证,助你快速前进。
了解更多在开始之前,让我们确定一些术语。当我们在 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 集合决定。
你应该谨慎使用此功能。理想情况下,不同环境之间的对象图应该保持相当固定。
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 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.name
是 config-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 开始,尤其是我们已经了解了它默认带来的所有好处。别忘了查看所有这些示例背后的代码。