3

How do I access the @Value machinery dynamically at run-time?

I thought that Environment might be what I was looking for, but it

@Component
public class SpringConfiguration implements ConfigurationI {
    @Autowired
    private Provider<Environment> env;

    @Override
    public String get(String key) {
        try {
            return env.get().getRequiredProperty(key);
        } catch (IllegalStateException e) {
            return null;
        }
    }
}

Unfortunately, this does not access the values exposed by our PropertyPlaceholderConfigurer bean.

EDIT: To explain my use case: This is part of making a library with a lot of spring specific pieces (that a pile of older spring applications depend on) usable from newer Guice applications by switching Spring specific annotations for JSR 330 (javax.inject) ones. I was hoping to avoid rewriting all the PropertyPlaceholderConfigurer stuff across all our Spring applications, by providing a nice entrypoint like this. If there is another better way to do this (maybe with @Named?) then I am all ears.

EDIT2: This is a (cleaned up) example of what kind of PropertyPlaceholderConfigurer exists in the apps calling into this library.

@Bean
public PropertyPlaceholderConfigurer placeholderConfigurer() {
    return new PropertyPlaceholderConfigurer() {
        @Override
        protected String resolvePlaceholder(String placeholder, Properties props) {
            // Some code to parse and cleanup key here
            String result = getPropertyFromLocalAppSpecificConfig(key);
            if (result == null) {
                result = super.resolvePlaceholder(placeholder, props);
            }
            // Some more random app specific logic for missing defaults
            return result;
        }
    };
}
kazagistar
  • 1,537
  • 8
  • 20

1 Answers1

0

PropertyPlaceholder and friends do not put the properties in your Environment (mainly because of backward compatibility reasons). Instead they use Environment and its own internal Properties object gathered generally from property files from the classpath to resolve @Value properties. Thus the properties loaded from PropertyPlaceholder can not be fetched dynamically (ie no getProperty(String..)). Some people create custom PropertyPlaceholder that store the properties publicly (through getter or whatever) but I think completely defeats Spring's new unified environment configuration handling.

What you really want is probably @PropertySource which still is pretty crappy since its not dynamic (since its an annotation you can't change where files get loaded from) but it will load properties into the Environment. I have been meaning to file issues with Spring Source about the confusion of this.

Anyway you can look at my solution here: Manually add a @PropertySource: Configuring Environment before context is refreshed

Basically you need to get hold of ConfigurableEnvironment and load your properties into it by creating PropertySources. The API for this is very powerful but not very intuitive. You can use ApplicationContextInitializers to get the Environment which has its own annoying issues (see link) or you can do what I do below.

public class ConfigResourcesEnvironment implements 
    ResourceLoaderAware, EnvironmentAware, BeanDefinitionRegistryPostProcessor, EnvironmentPropertiesMapSupplier {

    private Environment environment;
    private Map<String, String> environmentPropertiesMap;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        if (environment instanceof ConfigurableEnvironment) {
            ConfigurableEnvironment env = ((ConfigurableEnvironment) this.environment);
            List<PropertySource> propertySources;
            try {
                propertySources = loadPropertySources(); //Your custom method for propertysources
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            //Spring prefers primacy ordering so we reverse the order of the sources... You may not need to do this.
            reverse(propertySources);
            for (PropertySource rp : propertySources) {
                env.getPropertySources().addLast(rp);
            }
            environmentPropertiesMap = ImmutableMap.copyOf(environmentPropertiesToMap(env));
        }
        else {
            environmentPropertiesMap = ImmutableMap.of();
        }
    }


    public static Map<String,String> environmentPropertiesToMap(ConfigurableEnvironment e) {
        Map<String, String> properties = newLinkedHashMap();
        for (String n : propertyNames(e.getPropertySources())) {
            String v = e.getProperty(n);
            if (v != null)
                properties.put(n, v);
        }
        return properties;
    }

    public static Iterable<String> propertyNames(PropertySources propertySources) {
        LinkedHashSet<String> propertyNames = new LinkedHashSet<String>();
        for (PropertySource<?> p : propertySources) {
            if (p instanceof EnumerablePropertySource) {
                EnumerablePropertySource<?> e = (EnumerablePropertySource<?>) p;
                propertyNames.addAll(asList(e.getPropertyNames()));
            }
        }
        return propertyNames;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        //NOOP
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }


    public Map<String, String> getEnvironmentPropertiesMap() {
        return environmentPropertiesMap;
    }

}

Once you have ConfigurableEnvironment loaded you can use the EnvironmentAware interface for things that need the Environment or create your own interface.

Here is a custom interface you can use for things that need dynamic properties (the above class implements it):

public interface EnvironmentPropertiesMapSupplier {
    public Map<String, String> getEnvironmentPropertiesMap();

}
Community
  • 1
  • 1
Adam Gent
  • 47,843
  • 23
  • 153
  • 203
  • Unfortunately our configuration settings are not stored in a property file. Does this help me if we have them stored in something else? Do I have to try to jam all my configuration files into Properties so that spring can provide them? – kazagistar Sep 30 '15 at 17:45
  • Hence you should not use `@PropertySource` which is exactly what I was complaining about. Look at my other question (link). You need to make a `BeanDefinitionRegistryPostProcessor` and configure the `Environment` which is where you will do the loading of your properties. – Adam Gent Sep 30 '15 at 17:48
  • We have an `ApplicationContextInitializer` which retrieves properties from several locations (files, database, etc) and for each registers a `PropertySource`. Works very well as it integrates nicely with the Spring `Environment` and thus `@Value` stuff. – M. Deinum Sep 30 '15 at 18:13
  • I referenced my issues with the `ApplicationContextInitializer` in the linked question. With `ApplicationContextInitializer` you really need to be careful what you load (including logging) and you cannot use many spring things with out accidentally preloading things. It is the preferred and canonical approach (albeit Spring doesn't even use it for @PropertySource.. so...). – Adam Gent Sep 30 '15 at 18:21
  • @M.Deinum Just to iterate some more issues with the Initializers (slowly remembering). You can't even use most of Spring core to setup `ApplicationContextInitializer` as you will kick off Commons Logging. This includes the entire `Resource` loading framework see https://jira.spring.io/browse/SPR-12508 . So I would imagine for your db config loading you must have used raw JDBC? – Adam Gent Sep 30 '15 at 18:52
  • No a `JdbcTemplate` and logging is initalized before that (still on the old log4j which is initialized using the Log4j listener of Spring). Also we use SLF4j so we replaced Commons Logging with jcl-over-slf4j which doesn't have the commons logging issues. So commons logging and log initializing isn't really a problem, next to that it is already kicked of by the `ContextLoaderListener`s before the `ApplicationContextInitializer` even come into play. – M. Deinum Sep 30 '15 at 18:58
  • It is a problem if your using AMQP logging which requires configuration and you need to configure that prior. So you need to figure out how to load the config for that. I even had to get logback to do the right thing (https://github.com/qos-ch/logback/pull/181). Basically I have my own custom bootstrap process that is a servlet listener that uses custom config code that uses no logging. It really is annoying chicken and egg problem. It doesn't matter what logging fascade you use... it will initiate logging. – Adam Gent Sep 30 '15 at 19:07