Problem
I have following spring properties
myservice:
domains:
domain1:
supplier:
baseUrl:
getEndpoint:
customer:
postEndpoint:
domain2:
supplier:
baseUrl:
getEndpoint:
retryCount:
customer:
baseUrl:
postEndpoint:
fallbackDomain:
supplier:
baseUrl:
getEndpoint:
retryCount:
customer:
baseUrl:
postEndpoint:
The idea is that there is constant configuration for multiple domains, can be “domain1”, “domain2”, “domain3” etc. And in addition there is fallback domain.
If property cannot be found on specific domain, then uses fallback value.
Example:
- get customer post endpoint for domain1 -> “myservice.domains.domain1.customer.postEndpoint”
- get supplier retry count for domain1 -> “myservice.domains.fallbackDomain.supplier.retryCount” as it does not exist for domain1.
I am using spring boot 2.3.3 and spring 5.2.8, but considering upgrade if needed.
Solution 1
One of options is to get properties from org.springframework.core.env.Environment, like
public <T> T getPropertyWithFallback(String nameTemplate, String domain, Class<T> clazz){
return Optional.ofNullable(environment.getProperty(nameTemaplte.format(domain), clazz))
.orElse(environment.getProperty(nameTemplate.format("fallbackDomain"), clazz));
}
int count = getPropertyValueWithFallback("myservice.domains.%s.supplier.retryCount", "domain1", Integer.class);
But then:
- I need to deal with string names and it’s easy to make a typo in property name
- I need to remember the convention with template using “%s”, not a big deal, but still could be better
- I need to remember type for each property
- It’s hard to remove property as it requires to find all the references in the code, which may not be straightforward.
Solution 2
I can use spring Configuration properties feature like:
@Component
@ConfigurationProperties(prefix = "myservice")
public class MyServiceConfigurationProperties {
// skipping gettters and setters for readability
private Map<String, Domain> domains;
public static class Domain {
private Supplier supplier;
private Customer customer;
}
public static class Supplier {
private String baseUrl;
private String getEndpoint;
private Integer retryCount;
}
public static class Customer {
private String baseUrl;
private String postEndpoint;
}
}
This works fine, I do not need to remember property names once they are configured, types are already assigned. It’s easy to browse/list all properties for the domain.
- There is slightly less flexibility in defining properties – it assumes common structure for each domain – but that helps avoiding mess.
- There a bit more work when adding/removing properties, but for the cost of easier maintenance.
- There is one problem – implementing fallback becomes complex for nested objects.
I tried to implement it using proxies
private static class FallbackInvocationHandler implements InvocationHandler {
private final Object primary;
private final Object fallback;
public FallbackInvocationHandler(Object primary, Object fallback) {
this.primary = primary;
this.fallback = fallback;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(primary, args);
if (result == null) {
result = method.invoke(fallback, args);
}
if (result != null && result.getClass().getPackage().getName().startsWith("no.eg.apps.pda6k.server.config")) {
return Proxy.newProxyInstance(
result.getClass().getClassLoader(),
new Class[]{result.getClass()},
new FallbackInvocationHandler(result, method.invoke(fallback, args))
);
}
return result;
}
}
private <T> T getWithFallback(T value, T fallbackValue){
if (value==null){
return fallbackValue;
}
return (T) Proxy.newProxyInstance(
value.getClass().getClassLoader(),
new Class[]{value.getClass()},
new FallbackInvocationHandler(value, fallbackValue)
}
// and then each getter implement in such way
public Domain getDomainWithFallback(String domain){
Domain value = domains.get(domain);
Domain fallbackValue = domains.get("fallbackDomain");
return getWithFallback(value, fallbackValue);
}
It becomes more complex/error prone for nested properties. “Optimistic” implementation may look like:
// witin Supplier class
public Integer getRetryCountWithFallback(){
return getWithFallback(this.retryCount, domains.get("fallbackValue").getSupplier().getRetryCount());
}
But what if fallbackDomain has no/empty supplier property – some extra null checks are needed and that complicates code for all simple thing.
Also adding new property becomes more complex.
Is it even possible to reuse config structure if there are repeating properties e.g. customer and supplier have the same structure and I want to use same java object for them?
Question
Do you see any better solution than those presented above that allows me to solve the problem? I prefer not to change structure of properties, but it’s an option if needed.