I need to load a pretty complex object from application properties. The problem is, that I have to operate inside an ApplicationListener<ApplicationPreparedEvent>
, aka the application is not fully loaded yet. So I guess, any annotation based approach is off reach.
Is there a way to get the same functionality in a programmatic way? I have access to the ConfigurableEnvironment and all the properties are already loaded.
What I tried so far:
JavaPropsMapper
to convert this string to my modelSuper hacky. It kinda works, except one detail: arrays coming from different PropertySources get "merged" instead of the expected behaviour, where the array with the higher precedence completely overrides the lower precedence ones. I need this to work as expected since I have profile-specific application-xxx.yaml files that are expected to override the default arrays from application.yaml. The expected array-overriding behaviour is described here: Combine list from multiple spring-boot YAML files
So, seems like my hacky solution implements some @ConfiruationProperties features, but not others. Feels like I am reinventing the wheel, and doing so badly.
Is there a solution where I get the proper functionality of @ConfigurationProperties in my ApplicationListener?
Spring Boot exposes org.springframework.boot.context.properties.bind.Binder
, which you can use like this:
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
public class MyApplicationPreparedListener implements ApplicationListener<ApplicationPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationPreparedEvent event) {
ConfigurableEnvironment env = event.getApplicationContext().getEnvironment();
Binder binder = new Binder(ConfigurationPropertySources.get(env));
MyComplexConfig config = binder.bind("my.config", MyComplexConfig.class)
.orElseThrow(() -> new IllegalStateException("Could not bind config"));
System.out.println("Loaded config:");
System.out.println(" name: " + config.getName());
System.out.println(" servers: " + config.getServers());
System.out.println(" timeout: " + config.getNested().getTimeout());
System.out.println(" labels: " + config.getNested().getLabels());
}
}
Lets take a configuration properties file and two YAMLs like this:
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.Map;
@Getter
@Setter
public class MyComplexConfig {
private String name;
private List<String> servers;
private NestedConfig nested;
@Getter
@Setter
public static class NestedConfig {
private int timeout;
private Map<String, String> labels;
}
}
application.yaml
my:
config:
name: DefaultConfig
servers:
- default1.server.com
- default2.server.com
nested:
timeout: 30
labels:
env: prod
region: us-east
application-dev.yaml
my:
config:
name: DevConfig
servers:
- dev1.server.local
nested:
timeout: 10
labels:
env: dev
debug: true
SpringBootApplication class:
import com.example.MyApplicationPreparedListener;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class ExampleApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ExampleApplication.class)
.profiles("dev") // remove it to run with default profile
.listeners(new MyApplicationPreparedListener())
.run(args);
}
}
Output for default profile:
Loaded config:
name: DefaultConfig
servers: [default1.server.com, default2.server.com]
timeout: 30
labels: {env=prod, region=us-east}
Output for dev profile:
Loaded config:
name: DevConfig
servers: [dev1.server.local]
timeout: 10
labels: {env=dev, debug=true, region=us-east}