Spring 重新加载 Properties 属性
1、概览
本文将带你了解如何在 Spring 中重新加载 Properties 配置属性。
2、Spring 读取 Properties
Spring 有几种不同的方式来访问 Properties:
Environment
- 可以注入Environment
,然后使用Environment#getProperty
来读取给定的属性。Environment
包含不同的属性源,如系统属性(System Properties)、-D
参数和application.properties
(或者.yml
) 等。还可以使用@PropertySource
将额外的属性源添加到Environment
中。Properties
- 可以将 properties 文件加载到Properties
实例中,然后在 Bean 中通过调用properties.get("property")
使用它。@Value
- 可以使用@Value(${'property'})
注解在 Bean 中注入特定属性。@ConfigurationProperties
- 可以使用@ConfigurationProperties
在 Bean 中加载层次化的属性。。
3、重新加载外部属性文件
要在运行时更改文件中的属性(Properties),应该将该文件放在 Jar 之外的某个地方。然后使用命令行参数 -spring.config.location=file://{文件路径}
告诉 Spring 文件的位置。或者,也可以将其放在 application.properties
中。
对于基于磁盘文件的 Properties,可以开发一个端点或定时任务来读取文件并更新 Properties。
Apache 的 commons-configuration 是一个用于重新加载属性文件的库。可以使用 PropertiesConfiguration
和不同的 ReloadingStrategy
。
在 pom.xml
中添加 commons-configuration
依赖:
<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;
}
如上,设置了 Properties 的重新加载策略为 FileChangedReloadingStrategy
(使用默认的刷新延迟)。这意味着每隔 5000 毫秒检查一下 Properties 文件的最后修改时间。
可以使用 FileChangedReloadingStrategy#setRefreshDelay
来自定义延迟。
3.1、重新加载 Environment Properties
如果我们想重新加载通过 Environment
实例加载的属性,就必须继承 PropertySource
,然后使用 PropertiesConfiguration
从外部 Properties 文件返回新值。
创建 ReloadablePropertySource
,继承 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
。因此,它会根据刷新延迟间隔检查更新值。
现在,把 ReloadablePropertySource
添加到 Environment
的属性源中:
@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;
}
}
将新属性源添加为第一项,这样就可以覆盖任何具有相同 Key 值的现有属性。
创建一个 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、重新加载 Properties 实例
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;
}
// 覆写的其他方法
}
如上,覆写了 getProperty
及其重载方法,然后将其委托给 PropertiesConfiguration
实例。
现在可以创建一个该类的 Bean,并将其注入到组件中。
3.3、使用 @ConfigurationProperties 重新加载 Bean
要使用 @ConfigurationProperties
获得同样的效果,需要重构实例。但 Spring 只会创建具有 prototype 或 request Scope 的组件新实例。
因此,重新加载 Environment 的技术也适用于它们,但对于 Singleton,只能实现一个端点来销毁和重新创建 Bean,或者在 Bean 本身内部处理属性重新加载。
3.4、使用 @Value 重新加载 Bean
@Value
注解与 @ConfigurationProperties
具有相同的限制。
4、通过 Actuator 和 Cloud 重新加载属性
Spring Actuator 为 Health、Metric 和 Config 提供了不同的端点,但没有为刷新 Bean 提供任何端点。因此,需要 Spring Cloud 为其添加一个 /refresh
端点。该端点会重新加载 Environment
的所有属性源,然后发布 EnvironmentChangeEvent
。
Spring Cloud 还引入了 @RefreshScope
,可以将其用于配置类或 Bean。因此,默认 Scope 将是 refresh,而不是 singleton。
使用 refresh
Scope,Spring 将在环境变化事件(EnvironmentChangeEvent
)中清除这些组件的内部缓存。然后,在下一次访问 Bean 时,就会创建一个新实例。
首先,在 pom.xml
中添加 spring-boot-starter-actuator
:
<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>2021.0.3</spring-cloud.version>
</properties>
接着、添加 spring-cloud-starter
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
最后,启用 refresh
端点:
management.endpoints.web.exposure.include=refresh
使用 Spring Cloud 时,可以通过 Config Server 来管理属性,也可以继续使用外部文件。
现在,可以处理另外两种读取属性的方法:@Value
和 @ConfigurationProperties
。
4.1、使用 @ConfigurationProperties 刷新 Bean
来看看如何将 @ConfigurationProperties
与 @RefreshScope
结合使用:
@Component
@ConfigurationProperties(prefix = "application.theme")
@RefreshScope
public class ConfigurationPropertiesRefreshConfigBean {
private String color;
public void setColor(String color) {
this.color = color;
}
//get、set 等方法
}
Bean 从 application.theme
属性中读取 color
属性。注意,需要为其定义 setter 方法。
更改外部配置文件中 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;
}
//getter 方法
}
刷新的过程与上述相同。
不过,需要注意的是,/refresh
对具有显式 singleton Scope 的 Bean 无效。
5、总结
本文介绍了如何在 Spring 应用中通过 commons-configuration
库,Spring Actuator 以及 Spring Cloud Config 等技术实现在运行时重新加载属性。
Ref:https://www.baeldung.com/spring-reloading-properties