1. Overview

In this tutorial, we’ll learn how to reload properties in a Spring application.

2. Reading Properties in Spring

We have several 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). Extra property sources can also be added to the Environment using @PropertySource.
  2. Properties — We can load properties files into a Properties instance, and 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 tell Spring where it is with the command-line parameter –spring.config.location=file://{path to file}. Alternatively, we can put it in application.properties.

In file-based properties, we 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’ll 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 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.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);
    }
}

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

Now we’ll 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 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, we first 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 the prototype or request scope.

Consequently, our technique to reload the environment will also work for them, but for singletons, we have no choice but to implement 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 EnvironmentChangeEvent.

Spring Cloud has also 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 the 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 we’ll 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>2021.0.3</spring-cloud.version>
</properties>

Next, we’ll add spring-cloud-starter:

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

Finally, we’ll 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 can also 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 demonstrate 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 the “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 that we can get the new value from the bean on our 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’s necessary to note that /refresh won’t work for beans with an explicit singleton scope.

5. Conclusion

In this article, we learned how to reload properties with or without Spring Cloud features. We also illustrated the pitfalls and exceptions of each of the techniques.

The complete code is available in our GitHub project.