60

I have a Spring 3.1 @Configuration that needs a property foo to build a bean. The property is defined in defaults.properties but may be overridden by the property in overrides.properties if the application has an active override Spring profile.

Without the override, the code would look like this, and work...

@Configuration
@PropertySource("classpath:defaults.properties")
public class MyConfiguration {

    @Autowired
    private Environment environment;

    @Bean
    public Bean bean() {
        ...
        // this.environment.getRequiredProperty("foo");
        ...
    }
}

I would like a @PropertySource for classpath:overrides.properties contingent on @Profile("overrides"). Does anyone have any ideas on how this could be achieved? Some options I've considered are a duplicate @Configuration, but that would violate DRY, or programmatic manipulation of the ConfigurableEnvironment, but I'm not sure where the environment.getPropertySources.addFirst() call would go.

Placing the following in an XML configuration works if I inject the property directly with @Value, but not when I use Environment and the getRequiredProperty() method.

<context:property-placeholder ignore-unresolvable="true" location="classpath:defaults.properties"/>

<beans profile="overrides">
    <context:property-placeholder ignore-unresolvable="true" order="0"
                                  location="classpath:overrides.properties"/>
</beans>

Update

If you're trying to do this now, check out Spring Boot's YAML support, particularly the 'Using YAML instead of Properties' section. The profile support there would make this question moot, but there isn't @PropertySource support yet.

Emerson Farrugia
  • 11,153
  • 5
  • 43
  • 51

7 Answers7

57

Add the overriding @PropertySource in a static inner class. Unfortunately, you must specify all property sources together which means creating a "default" profile as the alternative to "override".

@Configuration
public class MyConfiguration
{
    @Configuration
    @Profile("default")
    @PropertySource("classpath:defaults.properties")
    static class Defaults
    { }

    @Configuration
    @Profile("override")
    @PropertySource({"classpath:defaults.properties", "classpath:overrides.properties"})
    static class Overrides
    {
        // nothing needed here if you are only overriding property values
    }

    @Autowired
    private Environment environment;

    @Bean
    public Bean bean() {
        ...
        // this.environment.getRequiredProperty("foo");
        ...
    }
}
yatskevich
  • 2,085
  • 16
  • 25
David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • 10
    Can't understand why this answer got so many upvotes. Hard-coding the profile names is against the point of profiles. Isn't there a similar way that let's you specify the profile through the 'spring.profiles.active' parameter? – jjoller Jul 04 '17 at 15:55
  • 1
    Today you coul use profile specific property files. I do not know if that has already been possible when this answer has been written (look [here](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-profile-specific-properties)). – Fencer Mar 15 '18 at 10:54
  • @Fencer That is available only if you are using Spring Boot. – Nima Ajdari Oct 13 '18 at 10:09
  • @NimaAJ That may be true, but since jjoller was referring to 'spring.profiles.active' my hint might be a valuable Information for those stumbling over his question. Probably the use of 'spring.profiles.active' implicates the use of Spring Boot or the possibility of using profile specific property files. I should have added '@jjoller' though. – Fencer Nov 23 '18 at 10:54
  • @jjoller Check out my answer https://stackoverflow.com/a/57756856/425238 – whitebrow Jan 09 '20 at 12:33
  • @jjoller if hard-coding like this were against the point of profiles, there would be no point in the @Profile() annotation existing. It exists for a good reason: to allow code to be activated by profiles. I think this is a good use. (Although I'm using Spille's approach below) – Rhubarb Apr 10 '20 at 20:54
  • 1
    Update: actually I like this approach better than Spille's and whitebrows, because using the ${spring.profiles.active} variable fails if you have multiple active profles. This @Profile approach, on the other hand, enables very flexible logic (documented here: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html) – Rhubarb Apr 10 '20 at 21:56
24

I suggest, defining two files, where the second is optional with the profile as suffix:

@Configuration
@PropertySources({
        @PropertySource("classpath:/myconfig.properties"),
        @PropertySource(value = "classpath:/myconfig-${spring.profiles.active}.properties", ignoreResourceNotFound = true)
})
public class MyConfigurationFile {

    @Value("${my.prop1}")
    private String prop1;

    @Value("${my.prop2}")
    private String prop2;

}
Spille
  • 527
  • 4
  • 12
  • For me this part is doing the magic: `${spring.profiles.active}`, now I can have two files: `config-dev.properties` and `config-prod.properties` and the java class have the next line `@PropertySource("classpath:config-${spring.profiles.active}.properties")` – Jairo Martínez Mar 21 '20 at 18:54
  • 10
    This fails if you have multiple profiles (E.g. I had active profiles = "dev,mock" – Rhubarb Apr 10 '20 at 21:44
  • im lucky to find this answer. simple and straightforward. you saved me. – Vodka Dec 10 '20 at 18:30
  • 1
    You can use `${spring.active.profiles}` to fix for multiple profiles – Joel Eze Feb 22 '22 at 07:19
  • `${spring.active.profiles}` doesn't exist, and i don't see how it would solve for multiple profiles anyway – Ben M Jun 23 '23 at 21:48
10

You can do:

  <context:property-placeholder location="classpath:${spring.profiles.active}.properties" />

Edit: if you need something more advanced, you can register your PropertySources on application startup.

web.xml

  <context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>com.xxx.core.spring.properties.PropertySourcesApplicationContextInitializer</param-value>
  </context-param>

file you create:

public class PropertySourcesApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  private static final Logger LOGGER = LoggerFactory.getLogger(PropertySourcesApplicationContextInitializer.class);

  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
    LOGGER.info("Adding some additional property sources");
    String[] profiles = applicationContext.getEnvironment().getActiveProfiles()
    // ... Add property sources according to selected spring profile 
    // (note there already are some property sources registered, system properties etc)
    applicationContext.getEnvironment().getPropertySources().addLast(myPropertySource);
  }

}

Once you've done it you just need to add in your context:

<context:property-placeholder/>

I can't really answer to your question about multiple profiles but I guess you activate them on such an initializer, and you could register the appropriate PropertySource items during profile activations.

Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
5

I can't think of any other way than one you have suggested Emerson, which is to define this bean in a separate @Configuration file with an @Profile annotation:

@Configuration
@Profile("override")
@PropertySource("classpath:override.properties")
public class OverriddenConfig {

    @Autowired
    private Environment environment;

    @Bean
    public Bean bean() {
        //if..
    }
}
Biju Kunjummen
  • 49,138
  • 14
  • 112
  • 125
5

In case you need to support multiple profiles you could do something like this:

@Configuration
public class Config {

    @Configuration
    @Profile("default")
    @PropertySource("classpath:application.properties")
    static class DefaultProperties {
    }

    @Configuration
    @Profile("!default")
    @PropertySource({"classpath:application.properties", "classpath:application-${spring.profiles.active}.properties"})
    static class NonDefaultProperties {
    }
}

That way you don't need to define a static configuration class for each profile. Thanks David Harkness for putting me into the right direction.

whitebrow
  • 2,015
  • 21
  • 24
  • fails for multiple active profiles - you're better off just creating separate @Profile settings – Rhubarb Apr 10 '20 at 21:45
3

Note: This answer provides an alternate solution to using properties files with @PropertySource. I went this route because it was too cumbersome trying to work with multiple properties files that may each have overrides while avoiding repetitive code.

Create a POJO interface for each related set of properties to define their names and types.

public interface DataSourceProperties
{
    String driverClassName();
    String url();
    String user();
    String password();
}

Implement to return the default values.

public class DefaultDataSourceProperties implements DataSourceProperties
{
     public String driverClassName() { return "com.mysql.jdbc.Driver"; }
     ...
}

Subclass for each profile (e.g. development, production) and override any values that differ from the default. This requires a set of mutually-exclusive profiles, but you can easily add "default" as the alternative to "overrides".

@Profile("production")
@Configuration
public class ProductionDataSourceProperties extends DefaultDataSourceProperties
{
     // nothing to override as defaults are for production
}

@Profile("development")
@Configuration
public class DevelopmentDataSourceProperties extends DefaultDataSourceProperties
{
     public String user() { return "dev"; }
     public String password() { return "dev"; }
}

Finally, autowire the properties configurations into the other configurations that need them. The advantage here is that you don't repeat any @Bean creation code.

@Configuration
public class DataSourceConfig
{
    @Autowired
    private DataSourceProperties properties;

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource source = new BoneCPDataSource();
        source.setJdbcUrl(properties.url());
        ...
        return source;
    }
}

I am still not convinced I'll stick with this over manually configuring properties files based on the active profiles in a servlet context initializer. My thought was that doing manual configuration would not be as amenable to unit testing, but I'm not so sure now. I really prefer reading properties files to a list of property accessors.

David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • 1
    The main feature about properties and `@PropertySource` is that you can override it in various ways - for example with environment variables or application switches – Raniz Aug 19 '16 at 07:57
1

All mentioned here solutions are a bit awkward, work only with one profile preset, and they won't work with more/other profiles. Currently a Spring team refuses to introduce this feature. But here's the working workaround I've found:

package com.example;

public class MyPropertySourceFactory implements PropertySourceFactory, SpringApplicationRunListener {

    public static final Logger logger = LoggerFactory.getLogger(MyPropertySourceFactory.class);

    @NonNull private static String[] activeProfiles = new String[0];

    // this constructor is used for PropertySourceFactory
    public MyPropertySourceFactory() {
    }

    // this constructor is used for SpringApplicationRunListener
    public MyPropertySourceFactory(SpringApplication app, String[] params) {
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        activeProfiles = environment.getActiveProfiles();
    }

    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
        logger.info("Loading: {} with profiles: {}", encodedResource.toString(), activeProfiles);
        // here you know all profiles and have the source Resource with main
        // properties, just try to load other resoures in the same path with different 
        // profile names and return them as a CompositePropertySource
    }
}

To make it working you have to have src/main/resources/META-INF/spring.factories with the following content:

org.springframework.boot.SpringApplicationRunListener=com.example.MyPropertySourceFactory

Now you can put your custom properties file somewhere and load it with @PropertySources:

@Configuration
@PropertySource(value = "classpath:lib.yml", factory = MyPropertySourceFactory.class)
public class PropertyLoader {
}
Lukasz Frankowski
  • 2,955
  • 1
  • 31
  • 32
  • I wouldn't say 'refuses'. They expressed their standpoint and it makes sense... But still, helpful workaround. – Dominik21 Aug 26 '22 at 18:02