javaspringspring-bootproperties-filespring-context

@PropertySource doesn't override properties defined in application.properties


If any property is defined in application.properties, it cannot be overridden by re-defining it in other .properties files using the @PropertySource annotation. This bug is only valid if the original value is present in application.properties. Any other file (e.g. app.properties) will allow to successfully override its values.

E.g.:

application.properties:

test.application=original

app.properties:

test.app=original

override.properties:

test.application=overridden
test.app=overridden

ApplicationPropertiesConfig.java:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@Data
@PropertySource(value = {
        "classpath:application.properties",
        "classpath:app.properties",
        "classpath:override.properties"
})
@ConfigurationProperties(prefix = "test")
public class ApplicationPropertiesConfig {

    private String application;  // == "original"   (BAD)

    private String app;          // == "overridden" (GOOD)

}

In the configuration above, the field application won't be overridden upon bean creation and will keep its original value defined in application.properties.

This was confirmed with the most recent Spring Boot versions: 2.7.11 and 3.0.6.

The same behavior is observed even if application.properties is not listed under @PropertySource or if the @PropertySources annotation is used.

Code reproducing the issue is here: https://github.com/denisab85/spring-property-overriding

A known workaround is renaming application.properties to something else (e.g. app.properties). Overriding works normally then. But this will not work in my case due to the size of the project where many classes already rely on the default application.properties. Source: a comment by Maksim Muruev (mmuruev) in @PropertySource not overriding in order [SPR-13500].


Solution

  • The problem is that you are using Spring Boot and not plain Spring. Which has certain, opinionated views. As I mentioned in my comment you shouldn't be using @PropertySource but specify the additional configuration files through --spring.config.additional-locations or (which I didn't mention) add them through an EnvironmentPostProcessor.

    If you would inject the Environment into your test and obtain the PropertySources from it you can print them out to the console. You will then see the following.

    ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
    MapPropertySource {name='test'}
    MapPropertySource {name='Inlined Test Properties'}
    PropertiesPropertySource {name='systemProperties'}
    OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
    RandomValuePropertySource {name='random'}
    OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
    ResourcePropertySource {name='class path resource [override.properties]'}
    ResourcePropertySource {name='class path resource [app.properties]'}
    ResourcePropertySource {name='class path resource [application.properties]'}
    

    This is, regardless of using @ConfigurationProperties. Spring Boot will always load the application.properties before anything else, so before the @PropertySources in some @Configuration files (or where ever you choose to place them). So this file always takes precedence, due to Spring Boot.

    Now as I stated you can circumvent this by either specifying configuration locations at startup by adding --spring.config.additional-location=classpath:app.properties,classpath:override.properties if you do that the output will change to the following:

    ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
    MapPropertySource {name='test'}
    MapPropertySource {name='Inlined Test Properties'}
    SimpleCommandLinePropertySource {name='commandLineArgs'}
    PropertiesPropertySource {name='systemProperties'}
    OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
    RandomValuePropertySource {name='random'}
    OriginTrackedMapPropertySource {name='Config resource 'class path resource [override.properties]' via location 'classpath:override.properties''}
    OriginTrackedMapPropertySource {name='Config resource 'class path resource [app.properties]' via location 'classpath:app.properties''}
    OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
    

    And now your tests will pass (and you will see the overrides will work).

    Finally you could also use an EnvironmentPostProcessor to add these files as property sources and have them load automatically by specifying it in a spring.factories file.

    package com.example.springpropertyoverriding;
    
    import java.io.IOException;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.env.EnvironmentPostProcessor;
    import org.springframework.core.env.ConfigurableEnvironment;
    import org.springframework.core.env.MutablePropertySources;
    import org.springframework.core.io.support.ResourcePropertySource;
    
    public class AdditionalConfigurationFilesAdder implements EnvironmentPostProcessor {
    
        private static final String[] locations =
                new String[] { "classpath:app.properties", "classpath:override.properties"};
    
        @Override
        public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
            MutablePropertySources propertySources = environment.getPropertySources();
            for (String location : locations) {
                try {
                    ResourcePropertySource source = new ResourcePropertySource(location);
                    propertySources.addAfter("random", source);
                } catch (IOException ex) {
                    throw new IllegalStateException(ex);
                }
            }
        }
    }
    

    Now you can reference this in a META-INF/spring.factories file so that it will add the resources.

    org.springframework.boot.env.EnvironmentPostProcessor=com.example.springpropertyoverriding.AdditionalConfigurationFilesAdder
    

    And now again when running the tests the output will be (more or less) the same as with specifying --spring.config.additional-location and the tests will pass.

    You have to realize the fact that you are using Spring Boot and that comes with some opinionated views on what to load when. The order is actually also explained in the Spring Boot Documentation. Which clearly shows that application.properties always takes precedence over @PropertySource.

    Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Later property sources can override the values defined in earlier ones. Sources are considered in the following order:

    1. Default properties (specified by setting SpringApplication.setDefaultProperties).
    2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
    3. Config data (such as application.properties files).
    4. A RandomValuePropertySource that has properties only in random.*.
    5. OS environment variables.
    6. Java System properties (System.getProperties()).
    7. JNDI attributes from java:comp/env.
    8. ServletContext init parameters.
    9. ServletConfig init parameters.
    10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
    11. Command line arguments. 12.properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
    12. @TestPropertySource annotations on your tests.
    13. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active. Config data files are considered in the following order:
    1. Application properties packaged inside your jar (application.properties and YAML variants).
    2. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
    3. Application properties outside of your packaged jar (application.properties and YAML variants).
    4. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).

    So in short no it is not a bug it is described and intended behavior you are seeing when using Spring Boot.