领先一步
VMware提供培训和认证,以加速您的进步。
了解更多嗨,Spring 粉丝们!欢迎来到 Spring 提示的另一期!在本期中,我们将探讨一些相当基础的内容,一些我希望早点讨论的内容:配置。不,我不是指函数式配置或 Java 配置之类的,我说的是告知代码如何执行的字符串值。你放在 application.properties 中的内容。*那就是*配置。
Spring 中的所有配置都源自 Spring 的Environment
抽象。Environment
有点像字典——一个带有键值对的映射。Environment
只是一个接口,通过它我们可以询问关于Environment
的信息。这个抽象存在于 Spring Framework 中,并在十多年前的 Spring 3 中引入。在此之前,有一个集中的机制允许集成配置,称为属性占位符解析。这种环境机制以及围绕该接口的类集合已经完全取代了旧的支持。如果你发现某个博客仍在使用这些类型,我建议你转向更新、更绿色的领域 :)。
让我们开始吧。访问 Spring Initializr 并生成一个新项目,确保选择Spring Cloud Vault
、Lombok
和Spring Cloud Config Client
。我将我的项目命名为configuration
。点击Generate
生成应用程序。在您喜欢的 IDE 中打开项目。如果你想跟着一起做,请确保禁用 Spring Cloud Vault 和 Spring Cloud Config Client 依赖项。我们现在不需要它们。
对于大多数 Spring Boot 开发人员来说,第一步是使用 application.properties。当你生成一个新项目时,Spring Initializr 甚至会将一个空的 application.properties 放到src/main/resources/application.properties
文件夹中!非常方便。你确实是在 Spring Initializr 上创建项目的,对吧?你可以使用 application.properties 或 application.yml。我不太喜欢.yml
文件,但如果你更喜欢它,也可以使用。
Spring Boot 在启动时会自动加载application.properties
。你可以通过环境在 Java 代码中取消引用属性文件中的值。在application.properties
文件中添加一个属性,如下所示。
message-from-application-properties=Hello from application.properties
现在,让我们编辑代码以读取该值。
package com.example.configuration;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigurationApplication.class, args);
}
@Bean
ApplicationRunner applicationRunner(Environment environment) {
return args -> {
log.info("message from application.properties " + environment.getProperty("message-from-application-properties"));
};
}
}
运行此代码,你将在日志输出中看到来自配置文件的值。如果你想更改 Spring Boot 默认读取的文件,你也可以这样做。不过,这是一个先有鸡还是先有蛋的问题——你需要指定一个属性,Spring Boot 将使用该属性来确定在哪里加载所有属性。因此,你需要在 application.properties 文件之外指定此属性。你可以使用程序参数或环境变量来填充spring.config.name
属性。
export SPRING_CONFIG_NAME=foo
现在使用作用域中的环境变量重新运行应用程序,它将失败,因为它将尝试加载foo.properties
,而不是application.properties
。
顺便说一句,你也可以运行带有外部配置的应用程序,该配置位于 jar 文件旁边,如下所示。如果你这样运行应用程序,外部application.properties
中的值将覆盖.jar
内部的值。
.
├── application.properties
└── configuration-0.0.1-SNAPSHOT.jar
0 directories, 2 files
Spring Boot 也知道 Spring 配置文件。配置文件是一种机制,允许你标记对象和属性文件,以便可以在运行时有选择地激活或停用它们。如果你想拥有特定于环境的配置,这非常有用。你可以将 Spring bean 或配置文件标记为属于特定配置文件,当该配置文件被激活时,Spring 将自动为你加载它。
配置文件名称基本上是任意的。某些配置文件是魔术的——Spring 以某种特定方式使用它们。其中最有趣的是default
,当没有其他配置文件处于活动状态时,它会被激活。但通常情况下,名称由你决定。我发现将我的配置文件映射到不同的环境非常有用:dev
、qa
、staging
、prod
等。
假设有一个名为dev
的配置文件。Spring Boot 将自动加载application-dev.properties
。它将除了 application.properties 之外加载它。如果两个文件中的值之间存在冲突,则更具体的-带有配置文件的文件-将胜出。你可以有一个默认值,在没有特定配置文件的情况下应用,然后在配置文件的配置中提供具体信息。
你可以通过几种不同的方式激活给定的配置文件,但最简单的方法是在命令行中指定它。或者你可以在 IDE 的运行配置对话框中打开它。IntelliJ 和 Spring Tool Suite 都提供了一个地方来指定在运行应用程序时使用的配置文件。你也可以设置一个环境变量SPRING_PROFILES_ACTIVE
,或在命令行中指定一个参数--spring.profiles.active
。两者都可以接受以逗号分隔的配置文件列表——你可以一次激活多个配置文件。
让我们试试这个。创建一个名为application-dev.properties
的文件。将以下值放入其中。
message-from-application-properties=Hello from dev application.properties
此属性与application.properties
中的属性具有相同的键。这里的 Java 代码与我们之前的一样。只需确保在启动 Spring 应用程序之前指定配置文件即可。你可以使用环境变量、属性等。你甚至可以在main()
方法中构建SpringApplication
时以编程方式定义它。
package com.example.configuration.profiles;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
// this works
// export SPRING_PROFILES_ACTIVE=dev
// System.setProperty("spring.profiles.active", "dev"); // so does this
new SpringApplicationBuilder()
.profiles("dev") // and so does this
.sources(ConfigurationApplication.class)
.run(args);
}
@Bean
ApplicationRunner applicationRunner(Environment environment) {
return args -> {
log.info("message from application.properties " + environment.getProperty("message-from-application-properties"));
};
}
}
运行应用程序,你将看到输出中反映的专用消息。
到目前为止,我们一直在使用 Environment 来注入配置。你也可以使用@Value
注解将值作为参数注入。你可能已经知道这一点。但是你知道如果没有任何匹配的值,你也可以指定要返回的默认值吗?你可能有很多原因想要这样做。你可以使用它来提供回退值,并在有人误输属性拼写时使其更加透明。它也很有用,因为即使有人不知道他们需要激活配置文件或其他内容,你也会得到一个可能有用的值。
package com.example.configuration.value;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigurationApplication.class, args);
}
@Bean
ApplicationRunner applicationRunner(
@Value("${message-from-application-properties:OOPS!}") String valueDoesExist,
@Value("${mesage-from-application-properties:OOPS!}") String valueDoesNotExist) {
return args -> {
log.info("message from application.properties " + valueDoesExist);
log.info("missing message from application.properties " + valueDoesNotExist);
};
}
}
方便吧?此外,请注意,你提供的默认字符串可以反过来内插其他属性。因此,你可以执行以下操作,假设你的应用程序配置中确实存在像default-error-message
这样的键
${message-from-application-properties:${default-error-message:YIKES!}}
如果存在,它将评估第一个属性,然后评估第二个属性,最后评估字符串YIKES!
。
前面,我们了解了如何使用环境变量或程序参数指定配置文件。这种机制——使用环境变量或程序参数配置 Spring Boot——是一种通用机制。你可以将其用于任何任意键,Spring Boot 将为你规范化配置。你可以通过这种方式在外部指定你放在 application.properties 中的任何键。让我们看一些例子。假设你想为数据源连接指定 URL。你可以将该值硬编码在 application.properties 中,但这并不安全。创建一个仅存在于生产环境中的环境变量可能会更好。这样,开发人员就无法访问生产数据库的密钥等等。
让我们试试看。这是示例的 Java 代码。
package com.example.configuration.envvars;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
// simulate program arguments
String[] actualArgs = new String[]{"spring.datasource.url=jdbc:postgres://127.0.0.1/some-prod-db"};
SpringApplication.run(ConfigurationApplication.class, actualArgs);
}
@Bean
ApplicationRunner applicationRunner(Environment environment) {
return args -> {
log.info("our database URL connection will be " + environment.getProperty("spring.datasource.url"));
};
}
}
在运行它之前,请确保导出你用来运行应用程序的 shell 中的环境变量,或者指定程序参数。我通过拦截我们在这里传递给 Spring Boot 应用程序的public static void main(String [] args)
来模拟后者——程序参数。你也可以这样指定环境变量
export SPRING_DATASOURCE_URL=some-arbitrary-value
mvn -DskipTests=true spring-boot:run
多次运行程序,尝试不同的方法,你将看到输出中的值。应用程序中没有自动配置可以连接到数据库,因此我们使用此属性作为示例。URL 不必是有效的 URL(至少在你将 Spring 的 JDBC 支持和 JDBC 驱动程序添加到类路径之前)。
Spring Boot 在其值的来源方面非常灵活。它并不关心你是否使用SPRING_DATASOURCE_URL
、spring.datasource.url
等。Spring Boot 将此称为*宽松绑定*。它允许你以最适合不同环境的方式做事,同时仍然适用于 Spring Boot。
这个想法——从环境中外部化应用程序的配置——并不是什么新鲜事。它在12 要素宣言中得到了很好的理解和描述。12 要素宣言指出,特定于环境的配置应该存在于该环境中,而不是代码本身。这是因为我们希望为所有环境构建一个版本。应该更改的内容应该是外部的。到目前为止,我们已经看到 Spring Boot 可以从命令行参数(程序参数)和环境变量中提取配置。它还可以读取来自 JOpt 的配置。如果碰巧在带有此类上下文的应用程序服务器中运行,它甚至可以来自 JNDI 上下文!
Spring Boot能够读取任何环境变量,这在此处非常有用。它也比使用程序参数更安全,因为程序参数会显示在操作系统的工具输出中。环境变量更合适。
到目前为止,我们已经看到Spring Boot可以从许多不同的地方提取配置。它知道配置文件,知道.yml
和.properties
文件。它非常灵活!但是,如果它不知道如何做你想让它做的事情呢?你可以很容易地使用自定义的PropertySource<T>
来教它新技巧。例如,如果你想将你的应用程序与存储在外部数据库、目录或Spring Boot不知道的其他位置的配置集成,你可能需要这样做。
package com.example.configuration.propertysource;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.PropertySource;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(ConfigurationApplication.class)
.initializers(context -> context
.getEnvironment()
.getPropertySources()
.addLast(new BootifulPropertySource())
)
.run(args);
}
@Bean
ApplicationRunner applicationRunner(@Value("${bootiful-message}") String bootifulMessage) {
return args -> {
log.info("message from custom PropertySource: " + bootifulMessage);
};
}
}
class BootifulPropertySource extends PropertySource<String> {
BootifulPropertySource() {
super("bootiful");
}
@Override
public Object getProperty(String name) {
if (name.equalsIgnoreCase("bootiful-message")) {
return "Hello from " + BootifulPropertySource.class.getSimpleName() + "!";
}
return null;
}
}
上面的例子是在足够早的阶段注册PropertySource
的最安全方法,这样所有需要它的地方都能找到它。你也可以在Spring开始连接对象并在你访问已配置对象时在运行时进行,但我不能保证这在每种情况下都能工作。这就是它可能的样子。
package com.example.configuration.propertysource;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigurationApplication.class, args);
}
@Bean
ApplicationRunner applicationRunner(@Value("${bootiful-message}") String bootifulMessage) {
return args -> {
log.info("message from custom PropertySource: " + bootifulMessage);
};
}
@Autowired
void contributeToTheEnvironment(ConfigurableEnvironment environment) {
environment.getPropertySources().addLast(new BootifulPropertySource());
}
}
class BootifulPropertySource extends PropertySource<String> {
BootifulPropertySource() {
super("bootiful");
}
@Override
public Object getProperty(String name) {
if (name.equalsIgnoreCase("bootiful-message")) {
return "Hello from " + BootifulPropertySource.class.getSimpleName() + "!";
}
return null;
}
}
到目前为止,我们几乎完全关注了如何从其他地方获取属性值。然而,我们还没有讨论一旦字符串进入我们的工作内存并可用于应用程序,它们会变成什么。大多数情况下,它们只是字符串,我们可以按原样使用它们。但是,有时将它们转换为其他类型的值(整数、日期、双精度数等)很有用。这项工作——将字符串转换为其他类型——可以成为另一个Spring Tips视频的主题,也许我很快就会做一个。需要说明的是,这里有很多相互关联的部分——ConversionService
、Converter<T>
、Spring Boot的Binder
等等。对于常见情况,这会正常工作。例如,你可以指定属性server.port = 8080
,然后将其作为整数注入到你的应用程序中。
@Value("${server.port}") int port
将这些值自动绑定到对象可能会有所帮助。这正是Spring Boot的ConfigurationProperties
为你做的。让我们看看它是如何工作的。
假设你有一个包含以下属性的application.properties文件
bootiful.message = Hello from a @ConfiguratinoProperties
然后你可以运行应用程序,并看到配置值已为我们绑定到对象。
package com.example.configuration.cp;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@Log4j2
@SpringBootApplication
@EnableConfigurationProperties(BootifulProperties.class)
public class ConfigurationApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigurationApplication.class, args);
}
@Bean
ApplicationRunner applicationRunner(BootifulProperties bootifulProperties) {
return args -> {
log.info("message from @ConfigurationProperties " + bootifulProperties.getMessage());
};
}
}
@Data
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties("bootiful")
class BootifulProperties {
private final String message;
}
BootifulProperties
对象上的@Data
和@RequiredArgsConstructor
注解来自Lombok。@Data
为final字段合成getter,为非final字段合成getter和setter。@RequiredArgsConstructor
为类中的所有final字段合成一个构造函数。结果是一个一旦通过构造函数初始化就不可变的对象。Spring Boot的ConfigurationProperties机制默认情况下不知道不可变对象;你需要使用@ConstructorBinding
注解(Spring Boot中一个相当新的补充)才能使其在此处正确工作。这在其他编程语言(如Kotlin(data class ...
)和Scala(case class ...
))中更有用,这些语言具有创建不可变对象的语法糖。
我们已经看到Spring可以加载应用程序.jar
旁边的配置,并且它可以从环境变量和程序参数加载配置。将信息放入Spring Boot应用程序并不难,但它有点零散。很难对环境变量进行版本控制或保护程序参数。
为了解决其中一些问题,Spring Cloud团队构建了Spring Cloud Config Server。Spring Cloud Config Server是一个HTTP API,它位于后端存储引擎的前面。存储是可插入的,最常见的是Git存储库,尽管也支持其他存储库。这些包括Subversion、本地文件系统,甚至MongoDB。
我们将设置一个新的Spring Cloud Config Server。转到Spring Initializr,选择“Config Server”,然后点击“Generate”。在您喜欢的IDE中打开它。
我们需要做两件事才能使其工作:首先,我们必须使用一个注解,然后提供一个配置值以将其指向包含我们的配置文件的Git存储库。以下是application.properties
。
spring.cloud.config.server.git.uri=https://github.com/joshlong/greetings-config-repository.git
server.port=8888
这就是你的主类应该是什么样子。
package com.example.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
运行应用程序——mvn spring-boot:run
或只是在您喜欢的IDE中运行应用程序。它现在可用。它将充当Github存储库中Git配置的代理。其他客户端可以使用Spring Cloud Config Client从Spring Cloud Config Server中提取他们的配置,Spring Cloud Config Server又会从Git存储库中提取配置。注意:为了演示方便,我尽可能地降低了安全性,但是你可以也应该保护链中的两个链接——从配置客户端到配置服务器,以及从配置服务器到Git存储库。Spring Cloud Config Server、Spring Cloud Config Client和Github可以很好地协同工作,并且安全地工作。
现在,返回我们的配置应用程序的构建,并确保取消注释Spring Cloud Config Client依赖项。要启动Spring Cloud Config Server,它需要一些——你猜对了!——配置。一个经典的先有鸡还是先有蛋的问题。此配置需要在其余配置之前更早地进行评估。你可以将此配置放在名为bootstrap.properties
的文件中。
你需要标识你的应用程序以赋予其名称,以便当它连接到Spring Cloud Config Server时,它将知道要提供哪个配置。我们在此处指定的名称将与Git存储库中的属性文件匹配。以下是应放入文件中的内容。
spring.cloud.config.uri=https://127.0.0.1:8888
spring.application.name=bootiful
现在我们可以读取Git存储库中bootiful.properties
文件中的任何我们想要的值,其内容为
message-from-config-server = Hello, Spring Cloud Config Server
我们可以像这样提取配置文件
package com.example.configuration.configclient;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigurationApplication.class, args);
}
@Bean
ApplicationRunner applicationRunner(@Value("${message-from-config-server}") String configServer) {
return args -> {
log.info("message from the Spring Cloud Config Server: " + configServer);
};
}
}
你应该在输出中看到该值。不错!Spring Cloud Config Server为我们做了很多很酷的事情。它可以为我们加密值。它可以帮助对属性进行版本控制。我最喜欢的一件事是,你可以独立于代码库的更改来更改配置。你可以将其与Spring Cloud的@RefreshScope
结合使用,以在应用程序启动运行后动态重新配置应用程序。(我应该做一个关于刷新范围及其多种用途的视频……)Spring Cloud Config Server之所以成为最受欢迎的Spring Cloud模块之一是有原因的——它可以与单体架构和微服务一起使用。
如果你正确配置Spring Cloud Config Server,它可以加密属性文件中的值。它有效。许多人还使用HashiCorp的优秀Vault产品,这是一个功能更全面的安全产品。Vault可以使用UI、CLI或HTTP API安全地存储和严格控制对令牌、密码、证书、用于保护秘密的加密密钥和其他敏感数据的访问。你也可以使用Spring Cloud Vault项目轻松地将其用作属性源。从构建中取消注释Sring Cloud Vault依赖项,让我们看看如何设置HashiCorp Vault。
下载最新版本,然后运行以下命令。我假设使用的是Linux或类Unix环境。尽管如此,将其转换为Windows应该相当简单。我不会尝试解释Vault的所有内容;相反,我会将你推荐给HashiCorp Vault的优秀的入门指南HashiCorp Vault。以下是我知道的设置和运行所有这些的最不安全但最快的方法。首先,运行Vault服务器。我在这里提供了一个root令牌,但你通常会使用Vault在启动时提供的令牌。
export VAULT_ADDR="https://127.0.0.1:8200"
export VAULT_SKIP_VERIFY=true
export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
vault server --dev --dev-root-token-id="00000000-0000-0000-0000-000000000000"
启动后,在另一个shell中,将一些值安装到Vault服务器中,如下所示。
export VAULT_ADDR="https://127.0.0.1:8200"
export VAULT_SKIP_VERIFY=true
export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
vault kv put secret/bootiful message-from-vault-server="Hello Spring Cloud Vault"
这将密钥message-from-vault-server
和值Hello Spring Cloud Vault
放入Vault服务中。现在,让我们更改应用程序以连接到该Vault实例以读取安全值。我们将需要一个bootstrap.properties
,就像Spring Cloud Config Client一样。
spring.application.name=bootiful
spring.cloud.vault.token=${VAULT_TOKEN}
spring.cloud.vault.scheme=http
然后,你可以像使用任何其他配置值一样使用该属性。
package com.example.configuration.vault;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@Log4j2
@SpringBootApplication
public class ConfigurationApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigurationApplication.class, args);
}
@Bean
ApplicationRunner applicationRunner(@Value("${message-from-vault-server:}") String valueFromVaultServer) {
return args -> {
log.info("message from the Spring Cloud Vault Server : " + valueFromVaultServer);
};
}
}
现在,在运行此程序之前,请确保还配置了我们在与vault
CLI 的两次交互中使用的相同三个环境变量:VAULT_TOKEN
、VAULT_SKIP_VERIFY
和VAULT_ADDR
。然后运行它,你应该在控制台中看到写入HashiCorp Vault的值。
希望你已经了解了Spring中丰富多彩且引人入胜的配置世界。掌握了这些信息后,你就可以更好地使用支持属性解析的其他项目了。掌握了这些工作原理的知识后,你就可以集成来自不同Spring集成的配置了,这些集成有很多!你可能会使用Spring Cloud Netflix的Archaius集成,或者与Spring Cloud Kubernetes的Configmaps集成,或者Spring Cloud GCP的Google Runtime Configuration API集成,或者Spring Cloud Azure的Microsoft Azure Key Vault集成等等。
我在这里只提到了几个产品,但列表是否详尽无所谓,如果集成正确,它们的用法将相同:云是极限!