Spring 重新加载 Properties 属性

1、概览

本文将带你了解如何在 Spring 中重新加载 Properties 配置属性。

2、Spring 读取 Properties

Spring 有几种不同的方式来访问 Properties:

  1. Environment - 可以注入 Environment,然后使用 Environment#getProperty 来读取给定的属性。Environment 包含不同的属性源,如系统属性(System Properties)、-D 参数和 application.properties(或者 .yml) 等。还可以使用 @PropertySource 将额外的属性源添加到 Environment 中。
  2. Properties - 可以将 properties 文件加载到 Properties 实例中,然后在 Bean 中通过调用 properties.get("property") 使用它。
  3. @Value - 可以使用 @Value(${'property'}) 注解在 Bean 中注入特定属性。
  4. @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 只会创建具有 prototyperequest Scope 的组件新实例。

因此,重新加载 Environment 的技术也适用于它们,但对于 Singleton,只能实现一个端点来销毁和重新创建 Bean,或者在 Bean 本身内部处理属性重新加载。

3.4、使用 @Value 重新加载 Bean

@Value 注解与 @ConfigurationProperties 具有相同的限制。

4、通过 Actuator 和 Cloud 重新加载属性

Spring Actuator 为 HealthMetricConfig 提供了不同的端点,但没有为刷新 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