Reloading Properties Files in Spring

1. Overview

In this tutorial, we’re going to show how to reload properties in a Spring application.

2. Reading Properties in Spring

We have different options to access properties in Spring:

  1. Environment — We can inject Environment and then use Environment#getProperty to read a given property. Environment contains different property sources like system properties, -D parameters, and application.properties (.yml). Also, extra property sources can be added to the Environment using @PropertySource.

  2. Properties — We can load properties files into a Properties instance, then use it in a bean by calling properties.get(“property”).

  3. @Value — We can inject a specific property in a bean with the @Value($\{‘property’}) annotation.

  4. @ConfigurationProperties — we can use @ConfigurationProperties to load hierarchical properties in a bean.

3. Reloading Properties from External File

To change properties in a file during runtime, we should place that file somewhere outside the jar. Then, we’ll tell Spring where it is with the command-line parameter –spring.config.location=file://\{path to file}. Or, we can put it in application.properties.

In file-based properties, we’ll have to choose a way to reload the file. For example, we can develop an endpoint or scheduler to read the file and update the properties.

One handy library to reload the file is Apache’s commons-configuration. We can use PropertiesConfiguration with different ReloadingStrategy.

Let’s add commons-configuration to our pom.xml:

<dependency>
    <groupId>commons-configuration</groupId>
    <artifactId>commons-configuration</artifactId>
    <version>1.10</version>
</dependency>

Then, we add a method to create a PropertiesConfiguration bean, which we’ll use later:

@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;
}

In the above code, we’ve set FileChangedReloadingStrategy as the reloading strategy with a default refresh delay. This means that PropertiesConfiguration checks for the file modification date if its last check was before 5000ms ago.

We can customize the delay using FileChangedReloadingStrategy#setRefreshDelay.

3.1. Reloading Environment Properties

If we want to reload the properties loaded through an Environment instance, we have to extend the PropertySource and then use PropertiesConfiguration to return new values from the external property file.

Let’s start with extending the 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.isEmpty(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);
    }
}

We have overridden the getProperty method to delegate it to PropertiesConfiguration#getProperty. Hence, it’ll check for updated values in intervals according to our refresh delay.

Now, we are going to add our ReloadablePropertySource to Environment‘s property sources:

@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;
    }
}

We’ve added the new property source as the first item because we want it to override any existing property with the same key.

Let’s create a bean to read a property from 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");
    }
}

If we need to add other reloadable external properties sources, first we have to implement our custom 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);
    }
}

Then we can annotate the class of a component with @PropertySource:

@PropertySource(value = "file:path-to-config", factory = ReloadablePropertySourceFactory.class)

3.2. Reloading Properties Instance

Environment is a better choice than Properties, especially when we need to reload properties from a file. However, if we need it, we can extend the 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
}

We’ve overridden getProperty and its overloads, then delegated it to a PropertiesConfiguration instance. Now, we can create a bean of this class and inject it in our components.

3.3. Reloading Bean with @ConfigurationProperties

To get the same effect with @ConfigurationProperties, we’d need to reconstruct the instance.

But, Spring will only create a new instance of components with prototype or request scope.

Thus, our technique to reload the environment also will work for them, but for singletons, we have no choice except implementing an endpoint to destroy and recreate the bean, or to handle the property reload inside the bean itself.

3.4. Reloading Bean with @Value

The @Value annotation presents the same limitations as @ConfigurationProperties.

4. Reloading Properties by Actuator and Cloud

Spring Actuator provides different endpoints for health, metrics, and configs, but nothing for refreshing beans. Thus, we need Spring Cloud to add a /refresh endpoint to it. This endpoint reloads all property sources of Environment and then publishes an https://static.javadoc.io/org.springframework.cloud/spring-cloud-commons-parent/1.1.9.RELEASE/org/springframework/cloud/context/environment/EnvironmentChangeEvent.html

Spring Cloud also has introduced @RefreshScope, and we can use it for configuration classes or beans. As a result, the default scope will be refresh instead of singleton.

Using refresh scope, Spring will clear its internal cache of these components on an EnvironmentChangeEvent. Then, on the next access to the bean, a new instance is created._
_

Let’s start by adding spring-boot-starter-actuator to our pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Then, let’s also import 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>Greenwich.SR1</spring-cloud.version>
</properties>

And then we add spring-cloud-starter:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter</artifactId>
</dependency>

Finally, let’s enable the refresh endpoint:

management.endpoints.web.exposure.include=refresh

When we use Spring Cloud, we can set up a Config Server to manage the properties, but we also can continue with our external files. Now, we can handle two other methods of reading properties: @Value and @ConfigurationProperties.

4.1. Refresh Beans with @ConfigurationProperties

Let’s show how to use @ConfigurationProperties with @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
}

Our bean is reading “color” property from the root “application.theme” property Note that we do need the setter method, per Spring’s documentation.

After we change the value of “application.theme.color” in our external config file, we can call /refresh, so then, we can get the new value from the bean on next access.

4.2. Refresh Beans with @Value

Let’s create our sample component:

@Component
@RefreshScope
public class ValueRefreshConfigBean {
    private String color;

    public ValueRefreshConfigBean(@Value("${application.theme.color}") String color) {
        this.color = color;
    }
    //put getter here
}

The process of refreshing is the same as above.

However, it is necessary to note that /refresh won’t work for beans with an explicit singleton scope.

5. Conclusion

In this tutorial, we’ve demonstrated how to reload properties with or without Spring Cloud features. Also, we’ve shown the pitfalls and exceptions of each of the techniques.

The complete code is available in our GitHub project.

Leave a Reply

Your email address will not be published.