javaspringproperties

Is there a way to fetch dynamically properties in Spring Java?


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}"


Solution

  • Unfortunately, I don't think nested evaluations are supported. You have several options in order to fetch environment variables dynamically.

    Option 1: Use Environment bean to do custom logic.

    This is probably the easiest and therefore the best solution for your case

    1. Autowire Environment bean.
    2. Extract your variables from it

    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));
        }
    }
    

    Option 2: Register Custom EnvironmentPostProcessor

    By registering a PostProcess for the environment you can add your own dynamic values:

    1. Define Your CustomPropertySource:
    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
        }
    }
    

    1. Define Your EnvironmentPostProcessor:
    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
    

    Option 3: Custom 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.