Persistent @ConfigurationProperties with EnvironmentPostProcessor in Spring Boot

Configuration properties is a great feature in Spring Boot. In a monolithic long-time running application, we often want to change the application settings. Those changes must be persisted to survive the application restart.

We can use environment post-processing for that. This post describes how.

Image for post
Image for post
Color King (cut) by František Kupka

Let’s start slowly; consider a simple class with configuration properties:

@ConfigurationProperties("hello")
@Getter
@Setter
class HelloConfigurationProperties {
private String greeting = "Hello!";
}

The property of greeting is used to print a greeting message from a REST controller:

@RestController
@RequestMapping("/hello")
@AllArgsConstructor
class HelloController {
private String greeting; @GetMapping
public String sayHello() {
return this.greeting;
}
}

After building and starting the application, we can see the default greeting to be printed:

$ curl http://localhost/hello
> Hello!

The message can be changed by setting an environment variable:

HELLO_GREETING=Hi!// restart the application...$ curl http://localhost/hello
> Hi!

As our application can’t be restarted easily (there are such cases!), we introduce a new endpoint to update the greeting message:

// ...
class HelloController {
// ...
@PutMapping
public ResponseEntity updateGreeting(String greeting) {
this.greeting = greeting;
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
$ curl http://localhost/hello?greeting=Ahoy! -X PUT
$ curl http://localhost/hello
> Ahoy!

Now it’s time to think about persistence. We introduce a class persisting properties into a database:

@RequiredArgsConstructor
public class Properties {
private final JdbcTemplate jdbcTemplate; public void store(String name, Object value) {
jdbcTemplate.update(
"UPDATE app_properties SET value = ? WHERE name = ?",
value, name);
}
public Set all() {
return jdbcTemplate.queryForList(
"SELECT name, value FROM app_properties").stream()
.map(m -> new Property((String)m.get("name"),
(String)m.get("value")))
.collect(Collectors.toSet());
}
}

And use it in the controller:

// ...
class HelloController {
private final Properties properties; // ... @PutMapping
public ResponseEntity updateGreeting(String greeting) {
this.greeting = greeting;
this.properties.store("hello.greeting", greeting);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}

Now we have the changes in the database. The last step is to fill the configuration properties at the startup. It’s where the environment post-processor comes to shine:

class ConfigurationPropertiesFromDatabasePostProcessor 
implements EnvironmentPostProcessor, Ordered {
@Override
public void postProcessEnvironment(
ConfigurableEnvironment env, SpringApplication app) {
try {
DataSource dataSource = DataSourceBuilder
.create()
.url(env.getProperty("spring.datasource.url"))
.username(env.getProperty("spring.datasource.username"))
.password(env.getProperty("spring.datasource.password"))
.driverClassName(env.getProperty("spring.datasource.driver-class-name"))
.build();
env.getPropertySources().addLast(
new MapPropertySource(
"propertiesFromDatabase",
new Properties(new JdbcTemplate(dataSource))
.all().stream()
.collect(Collectors.toMap(
Property::getName, Property::getValue))));
} catch (BadSqlGrammarException ignore) {
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

Probably the biggest gotcha is the need to create a new data-source. As the environment post-processor runs before application context is refreshed, there are no ready-to-use beans so far. We have to create everything we need from scratch.

Properties from the database are loaded and put as a new property-source with the lowest priority — this means, we can still overwrite them using environment variables, system properties etc. If this behavior is not desired, we can change the order by using addFirst, addBefore or addAfter.

We have to add the post-processor entry into META-INF/spring.factories:

org.springframework.boot.env.EnvironmentPostProcessor=\
com.ttulka.ConfigurationPropertiesFromDatabasePostProcessor

That’s it! Our configuration changes now survive a restart of the application.

You can find a working example on my GitHub.

The idea is also implemented in this small Spring Boot tool:

Written by

Software developer and occasional blogger: https://blog.ttulka.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store