在運行時更改 Spring Boot 屬性
1. 概述
動態管理應用程式配置可能是許多現實場景中的關鍵要求。在微服務架構中,由於擴充操作或變更的負載條件,不同的服務可能需要動態配置變更。在其他情況下,應用程式可能需要根據使用者偏好、來自外部 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 可能需要對程式碼進行細微調整。
2. 使用原型範圍的 Bean
當我們需要動態調整特定 bean 的屬性而不影響已創建的 bean 實例或更改全域應用程式狀態時,直接注入@Value
的簡單@Service
類別是不夠的,因為屬性在生命週期中是靜態的應用程式上下文的。
相反,我們可以使用@Configuration
類別中的@Bean
方法來建立具有可修改屬性的 bean 。這種方法允許在應用程式執行期間動態更改屬性:
@Configuration
public class CustomConfig {
@Bean
@Scope("prototype")
public MyService myService(@Value("${custom.property:default}") String property) {
return new MyService(property);
}
}
透過使用@Scope(“prototype”)
,我們確保每次呼叫myService(…)
時都會建立一個新的MyService
實例,從而允許在運行時進行不同的配置。在此範例中, MyService
是一個最小的 POJO:
public class MyService {
private final String property;
public MyService(String property) {
this.property = property;
}
public String getProperty() {
return property;
}
}
為了驗證動態行為,我們可以使用這些測試:
@Autowired
private ApplicationContext context;
@Test
void whenPropertyInjected_thenServiceUsesCustomProperty() {
MyService service = context.getBean(MyService.class);
assertEquals("default", service.getProperty());
}
@Test
void whenPropertyChanged_thenServiceUsesUpdatedProperty() {
System.setProperty("custom.property", "updated");
MyService service = context.getBean(MyService.class);
assertEquals("updated", service.getProperty());
}
這種方法使我們能夠靈活地在運行時更改配置,而無需重新啟動應用程式。這些變更是臨時的,僅影響由CustomConfig
實例化的 beans。
3.使用Environment
、 MutablePropertySources
和@RefreshScope
與前面的情況不同,我們想要更新已經實例化的 bean 的屬性。為此,我們將使用 Spring Cloud 的@RefreshScope
註解以及/actuator/refresh
端點。該執行器刷新所有@RefreshScope
bean,以反映最新配置的新實例替換舊實例,從而允許即時更新屬性而無需重新啟動應用程式。再次強調,這些改變並不是持久的。
3.1.基本配置
讓我們先將這些依賴項新增到pom.xml
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
<version>4.2.0</version>
</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
management.endpoints.web.exposure.include=refresh
此外,如果我們想在每次呼叫執行器時記錄日誌,讓我們設定此日誌記錄等級:
logging.level.org.springframework.boot.actuate=DEBUG
最後,讓我們為我們的測試添加一個範例屬性:
my.custom.property=defaultValue
我們的基本配置就完成了。
3.2.範例 Bean
當我們將@RefreshScope
註解應用於 bean 時,Spring Boot 不會像通常那樣直接實例化該 bean。相反,它創建一個代理對象,充當實際 bean 的佔位符或委託。
@Value
註解將application.properties
檔案中my.custom.property
的值注入到customProperty
欄位:
@RefreshScope
@Component
public class ExampleBean {
@Value("${my.custom.property}")
private String customProperty;
public String getCustomProperty() {
return customProperty;
}
}
代理物件攔截對此 bean 的方法呼叫。當/actuator/refresh
端點觸發刷新事件時,代理程式將使用更新的配置屬性重新初始化 bean。
3.3. PropertyUpdaterService
要動態更新正在執行的 Spring Boot 應用程式中的屬性,我們可以建立以程式設計方式新增或更新屬性的PropertyUpdaterService
類別。基本上,它允許我們透過在 Spring 環境中管理自訂屬性來源來在執行時間注入或修改應用程式屬性。
在深入研究程式碼之前,我們先澄清一些關鍵概念:
-
Environment
→ 提供對屬性來源、設定檔和系統環境變數的存取的介面 -
ConfigurableEnvironment
→Environment
的子接口,允許動態更新應用程式的屬性 -
MutablePropertySources
→ConfigurableEnvironment
所持有的PropertySource
物件的集合,它提供了新增、刪除或重新排序屬性來源的方法,例如係統屬性、環境變數或自訂屬性來源
各個元件之間關係的 UML 圖可以幫助我們理解動態屬性更新如何在應用程式中傳播:
以下是我們的PropertyUpdaterService
,它使用這些元件來動態更新屬性:
@Service
public class PropertyUpdaterService {
private static final String DYNAMIC_PROPERTIES_SOURCE_NAME = "dynamicProperties";
@Autowired
private ConfigurableEnvironment environment;
public void updateProperty(String key, String value) {
MutablePropertySources propertySources = environment.getPropertySources();
if (!propertySources.contains(DYNAMIC_PROPERTIES_SOURCE_NAME)) {
Map<String, Object> dynamicProperties = new HashMap<>();
dynamicProperties.put(key, value);
propertySources.addFirst(new MapPropertySource(DYNAMIC_PROPERTIES_SOURCE_NAME, dynamicProperties));
} else {
MapPropertySource propertySource = (MapPropertySource) propertySources.get(DYNAMIC_PROPERTIES_SOURCE_NAME);
propertySource.getSource().put(key, value);
}
}
}
讓我們來分解一下:
-
updateProperty(…)
方法檢查MutablePropertySources
集合中是否存在名為dynamicProperties
的自訂屬性來源 - 如果沒有,它將使用給定屬性建立一個新的
MapPropertySource
對象,並將其新增為第一個屬性來源 -
propertySources.addFirst(…)
確保我們的動態屬性優先於環境中的其他屬性 - 如果
dynamicProperties
來源已經存在,該方法將使用新值更新現有屬性,或者如果鍵不存在則新增它
透過使用此服務,我們可以在運行時以程式設計方式更新應用程式中的任何屬性。
3.4.使用PropertyUpdaterService
替代策略
雖然直接透過控制器公開屬性更新功能對於測試目的來說很方便,但在生產環境中通常並不安全。當使用控制器進行測試時,我們應該確保它得到充分的保護,防止未經授權的存取。
在生產環境中,有幾種安全有效地使用PropertyUpdaterService
的替代策略:
- 規劃任務 → 屬性可能會根據時間敏感條件或外部來源的資料而改變
- 基於條件的邏輯 → 對特定應用程式事件或觸發器的回應,例如負載變更、使用者活動或外部 API 回應
- 受限存取工具 →僅授權人員可以存取的安全管理工具
- 自訂執行器端點 → 自訂執行器提供對公開功能的更多控制,並且可以包含額外的安全性
- 應用程式事件偵聽器 → 在雲端環境中很有用,在雲端環境中,實例可能需要調整設定以回應基礎架構變更或應用程式中的其他重大事件
關於內建/actuator/refresh
端點,雖然它刷新用@RefreshScope
註解的 bean,但它不會直接更新屬性。我們可以使用PropertyUpdaterService
以程式設計方式新增或修改屬性,之後我們可以觸發/actuator/refresh
在整個應用程式中套用這些變更。但是,如果沒有PropertyUpdaterService
,僅此執行器無法更新或新增新的屬性。
總之,我們選擇的方法應符合應用程式的特定要求、配置資料的敏感度以及我們的整體安全狀況。
3.5.使用控制器手動測試
這裡我們示範如何使用簡單的控制器來測試PropertyUpdaterService
的功能:
@RestController
@RequestMapping("/properties")
public class PropertyController {
@Autowired
private PropertyUpdaterService propertyUpdaterService;
@Autowired
private ExampleBean exampleBean;
@PostMapping("/update")
public String updateProperty(@RequestParam String key, @RequestParam String value) {
propertyUpdaterService.updateProperty(key, value);
return "Property updated. Remember to call the actuator /actuator/refresh";
}
@GetMapping("/customProperty")
public String getCustomProperty() {
return exampleBean.getCustomProperty();
}
}
使用curl
執行手動測試將允許我們驗證我們的實作是否正確:
$ curl "http://localhost:8080/properties/customProperty"
defaultValue
$ curl -X POST "http://localhost:8080/properties/update?key=my.custom.property&value=baeldungValue"
Property updated. Remember to call the actuator /actuator/refresh
$ curl -X POST http://localhost:8080/actuator/refresh -H "Content-Type: application/json"
[]
$ curl "http://localhost:8080/properties/customProperty"
baeldungValue
它按預期工作。但是,如果第一次嘗試不起作用,並且我們的應用程式非常複雜,我們應該再次嘗試最後一個命令,以便給 Spring Cloud 有時間更新 bean。
3.6. JUnit5測試
自動化測試當然有幫助,但它並不是微不足道的。由於屬性更新操作是非同步的,並且沒有 API 可以知道它何時完成,因此我們需要使用逾時來避免阻塞 JUnit5。它是異步的,因為對/actuator/refresh
呼叫會立即返回,而不會等到所有 bean 實際重新建立。
await statement
使我們免於使用複雜的邏輯來測試我們感興趣的 bean 的刷新。
最後,要使用RestTemplate
,我們需要請求啟動@SpringBootTest(…)
註解中指定的 Web 環境:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PropertyUpdaterServiceUnitTest {
@Autowired
private PropertyUpdaterService propertyUpdaterService;
@Autowired
private ExampleBean exampleBean;
@LocalServerPort
private int port;
@Test
@Timeout(5)
public void whenUpdatingProperty_thenPropertyIsUpdatedAndRefreshed() throws InterruptedException {
// Injects a new property into the test context
propertyUpdaterService.updateProperty("my.custom.property", "newValue");
// Trigger the refresh by calling the actuator endpoint
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(null, headers);
RestTemplate restTemplate = new RestTemplate();
restTemplate.postForEntity("http://localhost:" + port + "/actuator/refresh", entity, String.class);
// Awaitility to wait until the property is updated
await().atMost(5, TimeUnit.SECONDS).until(() -> "newValue".equals(exampleBean.getCustomProperty()));
}
}
當然,我們需要使用我們感興趣的所有屬性和 bean 來自訂測試。
4. 使用外部設定檔
在某些情況下,有必要在應用程式部署套件之外管理組態更新,以確保屬性的持久變更。這也允許我們將更改分發到多個應用程式。
在本例中,我們將使用與之前相同的 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/
替換為實際路徑:
讓我們驗證 Spring Boot 是否正確載入此外部文件,並且其屬性是否覆寫application.properties
中的屬性:
$ curl "http://localhost:8080/properties/customProperty"
externalValue
它按計劃工作,因為external-config.properties
中的externalValue
替換了application.properties
中的defaultValue
。現在讓我們嘗試透過編輯external-config.properties
檔案來更改此屬性的值:
my.custom.property=external-Baeldung-Value
像往常一樣,我們需要呼叫執行器:
$ curl -X POST http://localhost:8080/actuator/refresh -H "Content-Type: application/json"
["my.custom.property"]
最後結果如預期,這次持久化了:
$ curl "http://localhost:8080/properties/customProperty"
external-Baeldung-Value
這種方法的一個優點是,每次修改external-config.properties
檔案時,我們都可以輕鬆地自動執行執行器呼叫。為此,我們可以在 Linux 和 macOS 上使用跨平台fswatch
工具,只需記住將/path/to/
替換為實際路徑:
$ fswatch -o /path/to/external-config.properties | while read f; do
curl -X POST http://localhost:8080/actuator/refresh -H "Content-Type: application/json";
done
Windows 使用者可能會發現基於 PowerShell 的替代解決方案更方便,但我們不會深入討論。
5. 結論
在本文中,我們探索了在 Spring Boot 應用程式中動態更新屬性的各種方法,而無需直接修改application.properties
檔案。
我們首先討論了在 bean 中使用自訂配置,使用@Configuration
、 @Bean
和@Scope(“prototype”)
註解來允許運行時更改 bean 屬性而無需重新啟動應用程式。此方法可確保靈活性並隔離對特定 Bean 實例的變更。
然後,我們檢查了 Spring Cloud 的@RefreshScope
和/actuator/refresh
端點,以對已實例化的 bean 進行即時更新,並討論了使用外部設定檔進行持久性屬性管理。這些方法為動態和集中配置管理提供了強大的選項,增強了 Spring Boot 應用程式的可維護性和適應性。
與往常一樣,完整的源代碼可以在 GitHub 上取得。