1. 概述
在本教程中,我們將學習如何在 Spring 應用中重新加載屬性。
2. 在 Spring 中讀取屬性
我們有多種選項可以訪問 Spring 中的屬性:
- Environment — 可以通過注入 Environment 並使用 Environment#getProperty 讀取指定屬性。 Environment 包含不同的屬性源,例如系統屬性、-D 參數和 application.properties (.yml) 文件。 還可以使用 @PropertySource 添加額外的屬性源到 Environment 中。
- Properties — 可以將屬性文件加載到 Properties 實例中,然後通過調用 properties.get(“property”) 在 Bean 中使用它。
- @Value — 可以使用 @Value(${‘property’}) 註解在 Bean 中注入特定的屬性。
- @ConfigurationProperties — 可以使用 @ConfigurationProperties 加載 Bean 中的層次化屬性。
3. 從外部文件重新加載屬性
為了在運行時更改文件中的屬性,我們應該將該文件放置在 JAR 外。然後,我們使用命令行參數 <em –spring.config.location=file://{路徑到文件} 告知 Spring 它的位置。 另一種方法是將它放在 <em>application.properties</em> 中。
在基於文件進行的屬性中,我們必須選擇一種重新加載文件的方式。 例如,我們可以開發一個端點或調度器來讀取文件並更新屬性。
Apache 的 <em>commons-configuration</em> 是一個方便的庫,用於重新加載文件。 我們可以使用 <em>PropertiesConfiguration</em>,並結合不同的 <em>ReloadingStrategy</em>。
讓我們將 <em>commons-configuration</em> 添加到我們的 <em>pom.xml</em> 中:
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>然後我們將添加一個創建 PropertiesConfiguration 類型的 Bean 的方法,稍後我們會使用它:
@Bean
@ConditionalOnProperty(name = "spring.config.location", matchIfMissing = false)
public PropertiesConfiguration propertiesConfiguration(
@Value("${spring.config.location}") String path) throws Exception {
String filePath = new File(path.substring("file:".length())).getCanonicalPath();
PropertiesConfiguration configuration = new PropertiesConfiguration(
new File(filePath));
configuration.setReloadingStrategy(new FileChangedReloadingStrategy());
return configuration;
}在上述代碼中,我們設置了 FileChangedReloadingStrategy 作為默認的刷新策略,並設置了默認刷新延遲。這意味着 PropertiesConfiguration 會檢查文件修改日期,如果其上次檢查時間距現在5000毫秒前。
我們可以使用 FileChangedReloadingStrategy#setRefreshDelay 自定義延遲。
3.1. 重新加載 環境 屬性
如果想要通過 Environment 實例加載的屬性進行重新加載,則需要 擴展 PropertySource,然後使用 PropertiesConfiguration 從外部屬性文件中返回新的值。
讓我們從擴展 PropertySource 開始:
public class ReloadablePropertySource extends PropertySource {
PropertiesConfiguration propertiesConfiguration;
public ReloadablePropertySource(String name, PropertiesConfiguration propertiesConfiguration) {
super(name);
this.propertiesConfiguration = propertiesConfiguration;
}
public ReloadablePropertySource(String name, String path) {
super(StringUtils.hasText(name) ? path : name);
try {
this.propertiesConfiguration = new PropertiesConfiguration(path);
this.propertiesConfiguration.setReloadingStrategy(new FileChangedReloadingStrategy());
} catch (Exception e) {
throw new PropertiesException(e);
}
}
@Override
public Object getProperty(String s) {
return propertiesConfiguration.getProperty(s);
}
}我們已覆蓋了 getProperty 方法,將其委託給 PropertiesConfiguration#getProperty。因此,它將根據我們的刷新延遲間隔檢查更新後的值。
現在我們將向 Environment 的屬性源中添加我們的 ReloadablePropertySource。
@Configuration
public class ReloadablePropertySourceConfig {
private ConfigurableEnvironment env;
public ReloadablePropertySourceConfig(@Autowired ConfigurableEnvironment env) {
this.env = env;
}
@Bean
@ConditionalOnProperty(name = "spring.config.location", matchIfMissing = false)
public ReloadablePropertySource reloadablePropertySource(PropertiesConfiguration properties) {
ReloadablePropertySource ret = new ReloadablePropertySource("dynamic", properties);
MutablePropertySources sources = env.getPropertySources();
sources.addFirst(ret);
return ret;
}
}我們將新的屬性源作為第一項,是因為我們希望它覆蓋任何具有相同鍵的現有屬性。
讓我們創建一個 Bean 來從 Environment 讀取屬性:
@Component
public class EnvironmentConfigBean {
private Environment environment;
public EnvironmentConfigBean(@Autowired Environment environment) {
this.environment = environment;
}
public String getColor() {
return environment.getProperty("application.theme.color");
}
}如果我們需要添加其他可重新加載的外部屬性源,首先必須實現我們的自定義PropertySourceFactory:
public class ReloadablePropertySourceFactory extends DefaultPropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String s, EncodedResource encodedResource)
throws IOException {
Resource internal = encodedResource.getResource();
if (internal instanceof FileSystemResource)
return new ReloadablePropertySource(s, ((FileSystemResource) internal)
.getPath());
if (internal instanceof FileUrlResource)
return new ReloadablePropertySource(s, ((FileUrlResource) internal)
.getURL()
.getPath());
return super.createPropertySource(s, encodedResource);
}
}然後,我們可以使用 @PropertySource 標記組件的類:
@PropertySource(value = "file:path-to-config", factory = ReloadablePropertySourceFactory.class)3.2. 重新加載屬性實例
Environment 比 Properties 更適合,尤其當我們需要從文件重新加載屬性時。但是,如果需要,我們可以擴展 java.util.Properties:
public class ReloadableProperties extends Properties {
private PropertiesConfiguration propertiesConfiguration;
public ReloadableProperties(PropertiesConfiguration propertiesConfiguration) throws IOException {
super.load(new FileReader(propertiesConfiguration.getFile()));
this.propertiesConfiguration = propertiesConfiguration;
}
@Override
public String getProperty(String key) {
String val = propertiesConfiguration.getString(key);
super.setProperty(key, val);
return val;
}
// other overrides
}我們已覆蓋了 getProperty 方法及其所有重載,然後將其委託給一個 PropertiesConfiguration 實例。現在我們可以創建該類的 Bean,並將其注入到我們的組件中。
3.3. 使用 <em @ConfigurationProperties@ 重新加載 Bean
為了達到與 <em @ConfigurationProperties@ 相同的效果,我們需要重建該實例。但是 Spring 只會為具有 <em prototype 或 <em request 作用域的組件創建新的實例。
因此,我們用於重新加載環境的技術同樣適用於這些組件,但對於單例,我們只能實現一個端點來銷燬和重建 Bean,或者在 Bean 本身處理屬性重新加載。
3.4. 使用 @Value 重新加載 Bean
@Value 註解具有與 @ConfigurationProperties 相同的限制。
4. 通過 Actuator 和雲端刷新屬性
Spring Actuator 提供健康檢查、指標和配置的不同端點,但沒有提供刷新 Bean 的功能。因此,我們需要 Spring Cloud 添加一個 /refresh 端點到 Actuator 中。該端點會刷新 Environment 的所有屬性源,然後發佈一個 EnvironmentChangeEvent。
Spring Cloud 還引入了 @RefreshScope,我們可以將其用於配置類或 Bean。因此,默認的 scope 將會是 refresh 而不是 singleton。
使用 refresh scope,Spring 會在 EnvironmentChangeEvent 發生時清除這些組件的內部緩存。然後,在 Bean 下一次訪問時,會創建一個新的實例。
讓我們從添加 spring-boot-starter-actuator 到我們的 pom.xml 開始:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然後我們將導入 spring-cloud-dependencies:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<spring-cloud.version>2023.0.1/spring-cloud.version>
</properties>
接下來,我們將添加 spring-cloud-starter:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>最後,我們將啓用刷新端點:
management.endpoints.web.exposure.include=refresh當我們使用 Spring Cloud 時,可以使用 Config Server 來管理屬性,但我們也可以繼續使用外部文件。現在,我們可以處理兩種其他讀取屬性的方法:<em @Value</em> 和 <em @ConfigurationProperties</em>。
4.1. 使用 <em @ConfigurationProperties@ 刷新 Bean
讓我們演示如何使用 <em @ConfigurationProperties@ 與 <em @RefreshScope@ 結合使用:
@Component
@ConfigurationProperties(prefix = "application.theme")
@RefreshScope
public class ConfigurationPropertiesRefreshConfigBean {
private String color;
public void setColor(String color) {
this.color = color;
}
//getter and other stuffs
}我們的 Bean 正在讀取根屬性 “color” 以及 “application.theme”.application.theme.color” 的值後,我們可以調用 /refresh 以在下次訪問時從 Bean 中獲取新值。
4.2. 使用 @Value 刷新 Bean
讓我們創建我們的示例組件:
@Component
@RefreshScope
public class ValueRefreshConfigBean {
private String color;
public ValueRefreshConfigBean(@Value("${application.theme.color}") String color) {
this.color = color;
}
//put getter here
}刷新過程與上述相同。
但是,需要注意的是,對於具有明確 singleton 作用域的 Bean,使用 /refresh 將不起作用。
5. 結論
在本文中,我們學習瞭如何使用帶有或不帶 Spring Cloud 功能的情況下重新加載屬性。我們還闡述了每種技術的潛在問題和例外情況。