The objective for this is to fetch dynamically a property by using @Value.
MyApplication
application.yml
app:
name: my-app-scv
my-app-svc: # This value will be dynamic acoording to each other app implementing Auth-Library
oauth:
tokenUrl: https://anOAuthImpl.com
jwksUrl: https://anOAuthImpl.com/jwks
validation:
url: https://anOAuthImpl.com/validate/
client:
id: client_id
secret: secret
Auth-Library
AuthConfigurationProperties java record
@Component
@Validated
public record AuthConfigurationProperties(
@Value("${#{authAppConfiguration.appName}.oauth.tokenUrl}}")
@NotEmpty
String tokenUrl,
@Value("${#{authAppConfiguration.appName}.oauth.jwksUrl}")
String jwksUrl,
@Valid
OauthClient client,
@Valid
ValidationProperties validation
) {
}
AuthAppConfiguration java record
@Component
public record AuthAppConfiguration(
@Value("${app.name}")
String appName
) {
}
Solution before these changes had @ConfigurationProperties
, but that doesn't support SpEL.
It this the correct way or there is an easier one? Also I'm having problems with the bean AuthAppConfiguration
; Spring cannot resolve the bean correctly.
So far, I'm trying to set in another bean the value off the app so SpEL evaluates first the expression and then @Value picks up property expected.
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder '#{authAppConfiguration.appName}.oauth.tokenUrl' in value "${#{authAppConfiguration.appName}.oauth.tokenUrl}"
Unfortunately, I don't think nested evaluations are supported. You have several options in order to fetch environment variables dynamically.
Environment
bean to do custom logic.This is probably the easiest and therefore the best solution for your case
Environment
bean.The straightforward option of simply using environment
@Component
@Validated
public class AuthConfigurationProperties {
@NotEmpty
private String tokenUrl;
private String jwksUrl;
@Valid
private OauthClient client;
@Valid
private ValidationProperties validation;
public AuthConfigurationProperties(OauthClient client, ValidationProperties validation, Environment environment) {
this.client = client;
this.validation = validation;
String appName = environment.getProperty("authAppConfiguration.appName");
this.tokenUrl = environment.getProperty("%s.oauth.tokenUrl".formatted(appName));
this.jwksUrl = environment.getProperty("%s.oauth.jwksUrl".formatted(appName));
}
}
By registering a PostProcess for the environment you can add your own dynamic values:
public class CustomPropertySource extends PropertySource<String> {
private Environment environment;
public CustomPropertySource(String name, Environment environment) {
super(name); //name of the PropertySource, doesn't matter in our implementation
this.environment = environment;
}
@Override
public Object getProperty(String name) {
if (name.equals("customUsername")) {
// do similar magic with environment
return "MY CUSTOM RUNTIME VALUE";
}
return null; // when no such value we return null
}
}
class EnvironmentConfig implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
environment.getPropertySources()
.addFirst(new CustomPropertySource("customPropertySource", environment));
}
}
the last step is that you have to register the EnvironmentPostProcess, by creating spring.factories file under path of src/main/resources/META-INF/spring.factories and inside of it just add following line:
org.springframework.boot.env.EnvironmentPostProcessor=package.to.environment.config.EnvironmentConfig
PropertySourcesPlaceholderConfigurer
A class that is responsible for resolving these placeholders is a BeanPostProcessor called PropertySourcesPlaceholderConfigurer (see here).
So you could override it and provide our custom PropertySource that would resolve the property like so:
@Component
public class CustomConfigurer extends PropertySourcesPlaceholderConfigurer {
@Override
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, ConfigurablePropertyResolver propertyResolver) throws BeansException {
// again you would need to do some logic with environment bean to get your values and then set them as you wish
((ConfigurableEnvironment) beanFactoryToProcess.getBean("environment"))
.getPropertySources()
.addFirst(new CustomPropertySource("customPropertySource"));
super.processProperties(beanFactoryToProcess, propertyResolver);
}
}
This last option might require setting @Order
to make sure it runs after other values have been set.
In general I recommend the first Option, the other options seem unnecessarily complicated, but I just wanted to provide all the options I was aware of.