9

I am moving a working project from using SpringBoot command line arguments to reading properties from a file. Here are the involved portions of the @Configuration class:

@Configuration
class RemoteCommunication {

    @Inject
    StandardServletEnvironment env


    @Bean
    static PropertySourcesPlaceholderConfigurer placeholderConfigurer () {
        // VERIFIED this is executing...
        PropertySourcesPlaceholderConfigurer target = new PropertySourcesPlaceholderConfigurer()
        // VERIFIED this files exists, is readable, is a valid properties file
        target.setLocation (new FileSystemResource ('/Users/me/Desktop/mess.properties'))
        // A Debugger does NOT show this property source in the inject Environment
        target
    }


    @Bean  // There are many of these for different services, only one shown here.
    MedicalSorIdService medicalSorIdService () {
        serviceInstantiator (MedicalSorIdService_EpicSoap, 'uri.sor.id.lookup.internal')
    }


    // HELPER METHODS...


    private <T> T serviceInstantiator (final Class<T> classToInstantiate, final String propertyKeyPrimary) {
        def value = retrieveSpringPropertyFromConfigurationParameter (propertyKeyPrimary)
        classToInstantiate.newInstance (value)
    }


    private def retrieveSpringPropertyFromConfigurationParameter (String propertyKeyPrimary) {
        // PROBLEM: the property is not found in the Environment
        def value = env.getProperty (propertyKeyPrimary, '')
        if (value.isEmpty ()) throw new IllegalStateException ('Missing configuration parameter: ' + "\"$propertyKeyPrimary\"")
        value
    }

Using @Value to inject the properties does work, however I'd rather work with the Environment directly if at all possible. If the settings are not in the Environment then I am not exactly sure where @Value is pulling them from...

env.getProperty() continues to work well when I pass in command line arguments specifying the properties though.

Any suggestions are welcome!

node42
  • 695
  • 4
  • 10
  • 19
  • Have you tried `@PropertySource("classpath:path.to.properties.file")`? – Ori Dar Jan 13 '14 at 20:43
  • How does it fail? Unresolved placeholder? – Sotirios Delimanolis Jan 13 '14 at 20:47
  • @orid The file location is not fixed. It will be set via a System or Environment property by our ops team later, but right now I am just trying to get it to work from a hard coded location (baby steps). – node42 Jan 13 '14 at 21:27
  • @SotiriosDelimanolis Because I am using env.getProperty (propertyKeyPrimary, '') it comes back as the specified '' (i.e. empty string). Then my error handling throws State Exception stating which property file is missing. No dramatic failures on Springs part, just a silent failure (unfortunately). – node42 Jan 13 '14 at 21:29
  • What happens with `@Value`? – Sotirios Delimanolis Jan 13 '14 at 21:32
  • 1
    @SotiriosDelimanolis Wow. Thanks for asking actually. With `@Value` the property _is_ resolved. It is just the env.getProperty() that is failing to find the value (which baffles me). I'll have to adjust my question. I'd still rather use env.getProperty(), but at least I have a fallback position... – node42 Jan 13 '14 at 21:44
  • @node42 why do you want to use env.getProperty() ? In Spring it's an antipattern because you inverse tell dont ask model - with '@Value' as Solitros suggested it's dependency injection – Jakub Kubrynski Jan 13 '14 at 23:20
  • @JakubK I _personally_ find that interrogating environment state allows for more flexibility and cleaner code. For instance I can collect _all_ the missing parameters and send an SNMP alert. But a more general argument has been better articulated Chris Beams at https://jira.springsource.org/browse/SPR-8539?focusedCommentId=75569&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-75569 Mostly I am trying to hold onto as much flexibility as possible (and it _is_ weird to hear myself playing against proper IOC... ;) – node42 Jan 13 '14 at 23:38
  • When you use Value annotation you can configure your application to fail on startup when there are missing properties (BTW, it's a default behaviour). And the second thing - always using getter method (in your case getProperty) instead of injecting dependencies is just breaking IoC rules. – Jakub Kubrynski Jan 14 '14 at 08:50

4 Answers4

21

The issue here is the distinction between PropertySourcesPlaceholderConfigurer and StandardServletEnvironment, or Environment for simplicity.

The Environment is an object that backs the whole ApplicationContext and can resolve a bunch of properties (the Environment interface extends PropertyResolver). A ConfigurableEnvironment has a MutablePropertySources object which you can retrieve through getPropertySources(). This MutablePropertySources holds a LinkedList of PropertySource objects which are checked in order to resolve a requested property.

PropertySourcesPlaceholderConfigurer is a separate object with its own state. It holds its own MutablePropertySources object for resolving property placeholders. PropertySourcesPlaceholderConfigurer implements EnvironmentAware so when the ApplicationContext gets hold of it, it gives it its Environment object. The PropertySourcesPlaceholderConfigurer adds this Environment's MutablePropertySources to its own. It then also adds the various Resource objects you specified with setLocation() as additional properties. These Resource objects are not added to the Environment's MutablePropertySources and therefore aren't available with env.getProperty(String).

So you cannot get the properties loaded by the PropertySourcesPlaceholderConfigurer into the Environment directly. What you can do instead is add directly to the Environment's MutablePropertySouces. One way is with

@PostConstruct
public void setup() throws IOException {
    Resource resource = new FileSystemResource("spring.properties"); // your file
    Properties result = new Properties();
    PropertiesLoaderUtils.fillProperties(result, resource);
    env.getPropertySources().addLast(new PropertiesPropertySource("custom", result));
}

or simply (thanks @M.Deinum)

@PostConstruct
public void setup() throws IOException {
    env.getPropertySources().addLast(new ResourcePropertySource("custom", "file:spring.properties")); // the name 'custom' can come from anywhere
}

Note that adding a @PropertySource has the same effect, ie. adding directly to the Environment, but you're doing it statically rather than dynamically.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • 2
    Small tip, there is also a `ResourcePropertySource` which simply takes a `Resource` or location `String`. Saves you a couple of lines of reading the properties yourself. I.e. `env.getPropertySources().addLast(new ResourcePropertySource("custom", "file:spring.properties"));` will yield the same result, with less coding. – M. Deinum Jan 14 '14 at 07:03
  • @M.Deinum I was looking for something similar, but missed it in my IDE's type hierarchy lookup. Thanks. – Sotirios Delimanolis Jan 14 '14 at 13:33
  • @SotiriosDelimanolis Thank you for taking the time to write this up - works perfectly! Dev ops can put the property file where they want by updating the environment variable that defines the location. And I get to remove about 30 `@Value` injections. _So_ much cleaner! Thanks again for your help. – node42 Jan 16 '14 at 16:03
  • You solve my problem that made me to spend about 2 hours in searching to solve it, thanks! My problem was unable to add properties to `Environment` only to `ConfigurableEnvironment`. – Frankie Drake Sep 28 '17 at 14:07
  • In my case `@Autowired StandardEnvironment env;` loading properties from an external library `classpath:spring.properties` – Sebastian Correa Zuluaica Jun 30 '23 at 01:54
5

In SpringBoot it's enough to use @EnableConfigurationProperties annotation - you don't need to setup PropertySourcesPlaceholderConfigurer.

Then on POJO you add annotation @ConfigurationProperties and Spring automatically injects your properties defined in application.properties.

You can also use YAML files - you just need to add proper dependency (like SnakeYaml) to classpath

You can find detailed example here: http://spring.io/blog/2013/10/30/empowering-your-apps-with-spring-boot-s-property-support

Jakub Kubrynski
  • 13,724
  • 6
  • 60
  • 85
  • Thanks for responding. I'm still not seeing how to register an arbitrary (name determined at runtime via a system variable) external .properties file with the Spring `Environment`, which is ultimately my goal with this application configuration. Not knowing what the property file name is until runtime prevents me from specifying it in a Spring annotation. – node42 Jan 13 '14 at 22:57
  • Did you try a `@PropertySource`? Someone else suggested it, but it should work, and if you use in in a `SpringApplication` source file (not just a regular `@Component`) you will find that the property source *is* injected into the `Environment`. – Dave Syer Jan 14 '14 at 11:04
  • `@PropertySource` is a standard Spring annotation while Spring Boot introduces its own ones. It allows you to easily create POJOs containing injected properties – Jakub Kubrynski Jan 14 '14 at 11:16
4

I achieved this during PropertySourcesPlaceholderConfigurer instantiation.

@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurerBean(Environment env) {
    PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
    YamlPropertiesFactoryBean yamlFactorybean = new YamlPropertiesFactoryBean();
    yamlFactorybean.setResources(determineResources(env));

    PropertiesPropertySource yampProperties = new PropertiesPropertySource("yml", yamlFactorybean.getObject());

    ((AbstractEnvironment)env).getPropertySources().addLast(yampProperties);

    propertySourcesPlaceholderConfigurer.setProperties(yamlFactorybean.getObject());

    return propertySourcesPlaceholderConfigurer;
}


private static Resource[] determineResources(Environment env){
    int numberOfActiveProfiles = env.getActiveProfiles().length;
    ArrayList<Resource> properties =  new ArrayList(numberOfActiveProfiles);
    properties.add( new ClassPathResource("application.yml") );

    for (String profile : env.getActiveProfiles()){
        String yamlFile = "application-"+profile+".yml";
        ClassPathResource props = new ClassPathResource(yamlFile);

        if (!props.exists()){
            log.info("Configuration file {} for profile {} does not exist");
            continue;
        }

        properties.add(props);
    }

    if (log.isDebugEnabled())
        log.debug("Populating application context with properties files: {}", properties);

    return properties.toArray(new Resource[properties.size()]);
}
Peter Jurkovic
  • 2,686
  • 6
  • 36
  • 55
  • 1
    Thanks for this solution as its exactly what I needed!! – Partha Feb 23 '19 at 02:25
  • OH GOODNESS, thanks to this code, I didn't realize that `PropertySourcesPlaceholderConfigurer` needs to be defined as a **static** Spring Bean. I was having trouble trying to instantiate this class object when it keeps telling me no default constructor was found. – tom_mai78101 Jun 30 '23 at 02:24
1

Maybe all you need is to set -Dspring.config.location=... (alternatively SPRING_CONFIG_LOCATION as an env var)? That has the effect of adding an additional config file to the default path for the app at runtime which takes precedence over the normal application.properties? See howto docs for details.

Dave Syer
  • 56,583
  • 10
  • 155
  • 143