運行時修改 Spring Boot 屬性的 N 種方式
環(huán)境:Spring Boot3.2.5
1. 簡介
在許多實際場景中,動態(tài)管理應(yīng)用程序配置可能是一個關(guān)鍵需求。在微服務(wù)架構(gòu)中,由于某些操作,不同的服務(wù)可能需要實時配置更改。應(yīng)用可能需要根據(jù)用戶環(huán)境、外部API的數(shù)據(jù)或遵守動態(tài)變化的要求來調(diào)整其行為。
application.properties文件是靜態(tài)的,應(yīng)用不重啟的情況下就無法更改。然而,Spring Boot 提供了幾種強大的方法來在運行時調(diào)整配置,而無需停機。無論是在實時應(yīng)用中功能切換,還是更改第三方相關(guān)配置都無需重啟應(yīng)用。
本篇文章將介紹幾種在不直接修改application.properties文件的情況下,動態(tài)更新 Spring Boot 應(yīng)用程序中屬性的策略。
2. 實戰(zhàn)案例
2.1 將Bean定義為Prototype作用域
當我們需要在不影響已創(chuàng)建的 Bean 實例或不改變?nèi)謶?yīng)用程序狀態(tài)的情況下動態(tài)調(diào)整特定 Bean 的屬性時,直接注入帶有@Value 注解的 @Service類是不行的,因為這些屬性在應(yīng)用程序上下文的生命周期中是靜態(tài)的,簡單說注入是一次性的。
我們可以使用@Configuration 配置類中的@Bean方法創(chuàng)建具有可修改屬性的 Bean。這種方法允許在應(yīng)用程序執(zhí)行過程中動態(tài)更改屬性:
@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() {
// 設(shè)置系統(tǒng)屬性值;
// 注意你不能吧屬性定義在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對象生成代理,再通過代理對象執(zhí)行時每次都重新獲取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
如果你想在每次調(diào)用該接口時都記錄日志,進行如下日志級別設(shè)置:
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 ;
}
}
接下來需要完成的是,動態(tài)向Environment中添加PropertySource該對象中存入的是我們需要動態(tài)刷新的值。
@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() ;
}
}
首先調(diào)用/configprops/update接口更新屬性,接著調(diào)用 /actuator/refresh 接口進行刷新操作,最后調(diào)用/configprops/title接口時,AppComponent代理對象會會使用更新的配置屬性重新初始化 Bean。
2.3 使用外部配置文件
在某些情況下,有必要在應(yīng)用程序部署包之外管理配置更新,以確保對屬性進行持續(xù)更改。這也允許我們將更改分發(fā)給多個應(yīng)用程序。
在本例中,我將使用與之前相同的 Spring Cloud 設(shè)置來啟用@RefreshScope和/actuator/refresh支持。我們使用外部文件external-config.properties。
我們還是借助上面的/configprops/title接口訪問屬性,通過 /actuator/refresh 接口屬性配置。
我們需要做的是在啟動服務(wù)是添加如下啟動參數(shù),指定外部配置文件
圖片
初始文件內(nèi)容;
pack.app.title=xxxooo
圖片
當我們修改次文件內(nèi)容后,直接調(diào)用/actuator/refresh 接口;
圖片
返回了已經(jīng)更新的配置屬性key,再次訪問/title接口;
圖片
得到了最新的值。