环境:Spring Boot3.2.5
1. 简介
在许多实际场景中,动态管理应用程序配置可能是一个关键需求。在微服务架构中,由于某些操作,不同的服务可能需要实时配置更改。应用可能需要根据用户环境、外部API的数据或遵守动态变化的要求来调整其行为。
application.properties文件是静态的,应用不重启的情况下就无法更改。然而,Spring Boot 提供了几种强大的方法来在运行时调整配置,而无需停机。无论是在实时应用中功能切换,还是更改第三方相关配置都无需重启应用。
本篇文章将介绍几种在不直接修改application.properties文件的情况下,动态更新 Spring Boot 应用程序中属性的策略。
2. 实战案例
2.1 将Bean定义为Prototype作用域
当我们需要在不影响已创建的 Bean 实例或不改变全局应用程序状态的情况下动态调整特定 Bean 的属性时,直接注入带有@Value 注解的 @Service类是不行的,因为这些属性在应用程序上下文的生命周期中是静态的,简单说注入是一次性的。
我们可以使用@Configuration 配置类中的@Bean方法创建具有可修改属性的 Bean。这种方法允许在应用程序执行过程中动态更改属性:
@Configuration
public class AppConfig {
@Bean
// 声明为多例bean作用域
@Scope("prototype")
public UserService userService(@Value("${pack.app.title:}") String title) {
return new UserService(title) ;
}
}
public class UserService {
private final String title;
public UserService(String title) {
this.title= title;
}
// getters, setters
}
通过@Scope将UserService作用域声明为多例,这样能保证每次通过getBean都拿到的是一个新的对象,既然是新对象那么每次都会重新注入title值。如下示例:
@RestController
public class UserController {
@Resource
private ApplicationContext context;
@Resource
private ApplicationContext context ;
@GetMapping("/update")
public String update() {
// 设置系统属性值;
// 注意你不能吧属性定义在application.yml配置文件中
System.setProperty("pack.app.title", "xxxooo - " + new Random().nextInt(10000)) ;
return "update success" ;
}
@GetMapping("/title")
public String title() {
return this.context.getBean("us", UserService.class).getTitle() ;
}
}
每次都是getBean获取新对象,所以属性title每次注入的都是最新的值。
2.2 使用@RefreshScope
我们可以使用 Spring Cloud 的@RefreshScope注解和/actuator/refresh端点。该接口会刷新所有@RefreshScopeBean,其实此种方式与上面2.1类似,首先是将bean对象生成代理,再通过代理对象执行时每次都重新获取getBean,以此来达到实时更新属性。
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
开启refresh接口
management:
endpoint:
refresh:
enabled: true
endpoints:
web:
exposure:
include: refresh
如果你想在每次调用该接口时都记录日志,进行如下日志级别设置:
logging:
level:
org.springframework.boot.actuate: debug
以上配置完成后,接下来就可以定义需要实时刷新的bean对象了;
@RefreshScope
@Component
public class AppComponent {
@Value("${pack.app.title:}")
private String title ;
public String getTitle() {
return this.title ;
}
}
接下来需要完成的是,动态向Environment中添加PropertySource该对象中存入的是我们需要动态刷新的值。
@Service
public class PackPropertyService {
private static final String PACK_PROPERTIES_SOURCE_NAME = "packDynamicProperties" ;
private final ConfigurableEnvironment environment ;
public PackPropertyService(ConfigurableEnvironment environment) {
this.environment = environment ;
}
// 更新或者添加PropertySource操作
public void updateProperty(String key, String value) {
MutablePropertySources propertySources = environment.getPropertySources() ;
if (!propertySources.contains(PACK_PROPERTIES_SOURCE_NAME)) {
Map<String, Object> properties = new HashMap<>() ;
properties.put(key, value) ;
propertySources.addFirst(new MapPropertySource(PACK_PROPERTIES_SOURCE_NAME, properties)) ;
} else {
// 替换更新值
MapPropertySource propertySource = (MapPropertySource) propertySources.get(PACK_PROPERTIES_SOURCE_NAME) ;
propertySource.getSource().put(key, value) ;
}
}
}
接下来写一个接口进行更新属性值;
@RestController
@RequestMapping("/configprops")
public class PropertyController {
private final PackPropertyService pps ;
private final AppComponent app ;
public PropertyController(PackPropertyService pps, AppComponent app) {
this.pps = pps ;
this.app = app ;
}
@PostMapping("/update")
public String updateProperty(String key, String value) {
pps.updateProperty(key, value) ;
return "update success" ;
}
@GetMapping("/title")
public String title() {
return app.getTitle() ;
}
}
首先调用/configprops/update接口更新属性,接着调用 /actuator/refresh 接口进行刷新操作,最后调用/configprops/title接口时,AppComponent代理对象会会使用更新的配置属性重新初始化 Bean。
2.3 使用外部配置文件
在某些情况下,有必要在应用程序部署包之外管理配置更新,以确保对属性进行持续更改。这也允许我们将更改分发给多个应用程序。
在本例中,我将使用与之前相同的 Spring Cloud 设置来启用@RefreshScope和/actuator/refresh支持。我们使用外部文件external-config.properties。
我们还是借助上面的/configprops/title接口访问属性,通过 /actuator/refresh 接口属性配置。
我们需要做的是在启动服务是添加如下启动参数,指定外部配置文件
图片
初始文件内容;
pack.app.title=xxxooo
图片
当我们修改次文件内容后,直接调用/actuator/refresh 接口;
图片
返回了已经更新的配置属性key,再次访问/title接口;
图片
得到了最新的值。