我们首先讨论在 bean 中使用自定义配置,使用@Configuration、@Bean和@Scope(“prototype”)注释允许在运行时更改 bean 属性而无需重新启动应用程序。此方法可确保灵活性并将更改隔离到 bean 的特定实例。
然后,我们研究 Spring Cloud 的@RefreshScope和/actuator/refresh端点,以便实时更新已实例化的 bean,并讨论了使用外部配置文件进行持久属性管理。这些方法为动态和集中配置管理提供了强大的选项,增强了我们的 Spring Boot 应用程序的可维护性和适应性。
动态管理应用程序配置是许多实际场景中的关键要求。在微服务架构中,由于扩展操作或不同的负载条件,不同的服务可能需要动态更改配置。在其他情况下,应用程序可能需要根据用户偏好、来自外部 API 的数据调整其行为,或遵守动态变化的要求。
application.properties文件是静态的,如果不重新启动应用程序就无法更改。但是,Spring Boot 提供了几种可靠的方法来在运行时调整配置而无需停机。无论是在实时应用程序中切换功能、更新数据库连接以进行负载平衡,还是在不重新部署应用程序的情况下更改第三方集成的 API 密钥,Spring Boot 的动态配置功能都可以为这些复杂环境提供所需的灵活性。
在本教程中,我们将探讨几种在 Spring Boot 应用程序中动态更新属性的策略,而无需直接修改 application.properties文件。这些方法可以满足不同的需求,从非持久性内存更新到使用外部文件的持久性更改。
我们的示例参考了带有 JDK17 的 Spring Boot 3.2.4。我们还将使用 Spring Cloud 4.1.3。不同版本的 Spring Boot 可能需要对代码进行细微调整。
使用原型范围的 Bean
当我们需要动态调整特定bean的属性而不影响已创建的 bean 实例或改变全局应用程序状态时,直接注入@Value的简单@Service类是不够的,因为属性在应用程序上下文的生命周期内是静态的。
相反,我们可以使用@Configuration类中的@Bean方法创建具有可修改属性的 bean 。此方法允许在应用程序执行期间动态更改属性:
@Configuration |
通过使用@Scope(“prototype”),我们确保每次调用myService(…)时都会创建一个新的MyService实例,从而允许在运行时进行不同的配置。在此示例中,MyService是一个最小 POJO:
public class MyService { |
为了验证动态行为,我们可以使用这些测试:
@Autowired |
这种方法使我们能够灵活地在运行时更改配置,而无需重新启动应用程序。更改是临时的,仅影响CustomConfig实例化的 bean 。
使用Environment,MutablePropertySources和@RefreshScope
与前一种情况不同,我们想要更新已实例化的 bean 的属性。为此,我们将使用 Spring Cloud 的@RefreshScope注释以及/actuator/refresh端点。此执行器刷新所有@RefreshScope bean,用反映最新配置的新实例替换旧实例,从而允许实时更新属性而无需重新启动应用程序。同样,这些更改不是持久的。
基本配置
让我们首先将这些依赖项添加到pom.xml:
<dependency> |
spring -cloud-starter和spring-cloud-starter-config依赖项是 Spring Cloud 框架的一部分,而spring-boot-starter-actuator依赖项是公开/actuator/refresh端点所必需的。最后,awaitility依赖项是一个用于处理异步操作的测试实用程序,正如我们将在 JUnit5 测试中看到的那样。
现在让我们看一下application.properties。由于在此示例中我们不使用Spring Cloud Config Server来集中跨多个服务的配置,而只需要在单个 Spring Boot 应用程序中更新属性,因此我们应该禁用尝试连接到外部配置服务器的默认行为:
spring.cloud.config.enabled=false |
我们仍在使用 Spring Cloud 功能,只是与分布式客户端-服务器架构不同。如果我们忘记了spring.cloud.config.enabled=false,应用程序将无法启动,并抛出java.lang.IllegalStateException。
然后我们需要启用 Spring Boot Actuator 端点来公开/actuator/refresh:
management.endpoint.refresh.enabled=true |
此外,如果我们想要在每次调用执行器时进行记录,我们可以设置以下日志记录级别:
logging.level.org.springframework.boot.actuate=DEBUG |
最后,让我们为测试添加一个示例属性:
my.custom.property=defaultValue |
我们的基本配置已经完成。
示例 Bean
当我们将@RefreshScope注释应用于 bean 时,Spring Boot 不会像平常一样直接实例化该 bean。相反,它会创建一个代理对象,作为实际 bean 的占位符或委托。
@Value注释将application.properties文件中my.custom.property的值注入到customProperty字段中:
@RefreshScope |
代理对象会拦截对此 bean 的方法调用。当/actuator/refresh端点触发刷新事件时,代理会使用更新后的配置属性重新初始化该 bean。
属性更新服务
为了动态更新正在运行的 Spring Boot 应用程序中的属性,我们可以创建 PropertyUpdaterService类,以编程方式添加或更新属性。基本上,它允许我们通过在 Spring 环境中管理自定义属性源来在运行时注入或修改应用程序属性。
在深入研究代码之前,让我们先澄清一些关键概念:
环境→ 提供对属性源、配置文件和系统环境变量的访问的接口
ConfigurableEnvironment → Environment的子接口,允许动态更新应用程序的属性
MutablePropertySources → ConfigurableEnvironment持有的PropertySource对象集合,提供添加、删除或重新排序属性源的方法,例如系统属性、环境变量或自定义属性源
下面是我们的PropertyUpdaterService,它使用这些组件来动态更新属性:
@Service |
让我们分解一下:
- updateProperty (…)方法检查MutablePropertySources集合中是否存在名为dynamicProperties的自定义属性源
- 如果没有,它将使用给定的属性创建一个新的MapPropertySource对象,并将其添加为第一个属性源
- propertySources.addFirst(…)确保我们的动态属性优先于环境中的其他属性
- 如果dynamicProperties源已存在,则该方法将使用新值更新现有属性,如果键不存在,则添加它
- 通过使用此服务,我们可以在运行时以编程方式更新应用程序中的任何属性。
使用PropertyUpdaterService的替代策略
虽然直接通过控制器公开属性更新功能对于测试目的来说很方便,但在生产环境中通常并不安全。使用控制器进行测试时,我们应确保对其进行充分保护,以防止未经授权的访问。
在生产环境中,有几种安全有效地使用 PropertyUpdaterService 的替代策略:
- 计划任务→属性可能会根据时间敏感条件或来自外部来源的数据而改变
- 基于条件的逻辑→响应特定的应用程序事件或触发器,例如负载变化、用户活动或外部 API 响应
- 限制访问工具→仅授权人员可访问的安全管理工具
- 自定义执行器端点→自定义执行器可以更好地控制公开的功能,并可以包含额外的安全性
- 应用程序事件监听器→在云环境中很有用,在云环境中,实例可能需要调整设置以响应基础设施的变化或应用程序内的其他重要事件
总之,我们选择的方法应该符合我们的应用程序的具体要求、配置数据的敏感性以及我们的整体安全态势。
使用控制器进行手动测试
在这里我们演示如何使用一个简单的控制器来测试PropertyUpdaterService的功能:
@RestController |
使用curl执行手动测试将使我们能够验证我们的实现是否正确:
$ curl "http://localhost:8080/properties/customProperty" |
它按预期工作。但是,如果第一次尝试没有成功,并且我们的应用程序非常复杂,我们应该再次尝试最后一个命令,以便 Spring Cloud 有时间更新 bean。
JUnit5 测试
自动化测试当然很有帮助,但并非易事。由于属性更新操作是异步的,并且没有 API 来了解它何时完成,因此我们需要使用超时来避免阻塞 JUnit5。它是异步的,因为对/actuator/refresh 的调用会立即返回,而不会等到所有 bean 都实际重新创建。
await 语句使我们无需使用复杂的逻辑来测试我们感兴趣的 bean 的刷新。它使我们能够避免轮询等不太优雅的设计。
最后,要使用RestTemplate ,我们需要按照@SpringBootTest(…)注释中指定的方式请求启动 Web 环境:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) |
当然,我们需要使用我们感兴趣的所有属性和 bean 来定制测试。
使用外部配置文件
在某些情况下,需要在应用程序部署包之外管理配置更新,以确保属性的持久更改。 这也使我们能够将更改分发到多个应用程序。
在这种情况下,我们将使用相同的先前的 Spring Cloud 设置来启用@RefreshScope和/actuator/refresh支持,以及相同的示例控制器和 bean。
我们的目标是使用外部文件external-config.properties测试ExampleBean上的动态更改。让我们使用以下内容保存它:
my.custom.property=externalValue
我们可以使用–spring.config.additional-location参数告诉 Spring Boot external-config.properties的位置,如 Eclipse 屏幕截图所示。请记住将示例/path/to/替换为实际路径:
Eclipse 运行配置外部属性文件
让我们验证 Spring Boot 是否正确加载此外部文件,以及其属性是否覆盖application.properties中的属性:
$ curl "http://localhost:8080/properties/customProperty" |
它按计划工作,因为external-config.properties中的externalValue替换了application.properties中的defaultValue 。现在让我们尝试通过编辑external-config.properties文件来更改此属性的值:
my.custom.property=external-Jdon-Value
像往常一样,我们需要调用执行器:
$ curl -X POST http://localhost:8080/actuator/refresh -H "Content-Type: application/json" |
最后,结果正如预期的那样,这次是持久化的:
$ curl "http://localhost:8080/properties/customProperty" |
这种方法的一个优点是,每次修改external-config.properties文件时,我们都可以轻松地自动执行执行器调用。为此,我们可以在 Linux 和 macOS 上使用跨平台fswatch工具,只需记住将/path/to/替换为实际路径:
$ fswatch -o /path/to/external-config.properties | while read f; do |
Windows 用户可能会发现基于 PowerShell 的替代解决方案更加方便,但我们不会讨论这个。