Pankaj
Pankaj

Reputation: 2678

Does Spring follow any naming convention for application configuration?

Issue : Configuration defined in application.properties is not overridden by environment variable.

I'm facing strange issue with spring configuration as configuration defined in application.properties is not overridden by environment variable when configuration is named in specific way. As mentioned in Externalized Configuration OS environment variables takes precedence over application.properties but this doesn't happen when configuration is defined as myExternal_url but it works when configuration is defined as my_external_url (in sample code below, we need to change configuration to my_external_url in ApplicationProperties.java and application.properties)

Sample Code -

@SpringBootApplication
public class ConfigApplication implements ApplicationRunner {

  @Autowired private ApplicationProperties applicationProperties;

  public static void main(String[] args) {
    SpringApplication.run(ConfigApplication.class, args);
  }

  @Override
  public void run(ApplicationArguments arg0) {
    System.out.println("External URL = " + applicationProperties.getMyExternalUrl());
  }
}

Application Bean configuration -

@Configuration
public class AppConfig {

  @Bean
  @ConfigurationProperties(prefix = "")
  public ApplicationProperties applicationProperties() {
    return new ApplicationProperties();
  }
}

ApplicationProperties class -

public class ApplicationProperties {

  @Value("${myExternal_url}")
  private String myExternalUrl;

  public String getMyExternalUrl() {
    return this.myExternalUrl;
  }

  public void setMyExternalUrl(String myExternalUrl) {
    this.myExternalUrl = myExternalUrl;
  }
}

application.properties:

myExternal_url=external_url_env_application_properties

What could be reason for this ?

EDIT - adding gradle Gradle configuration

plugins {
    id 'org.springframework.boot' version '2.4.0-M1'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    compileOnly 'org.projectlombok:lombok:1.18.6'
    annotationProcessor 'org.projectlombok:lombok:1.18.6'
}

test {
    useJUnitPlatform()
}

EDIT 2

Trace log shows that myExternal_url is resolved correctly from Environment variable. Then Spring tries to resolve autowired dependencies 'applicationProperties' by calling AutowiredAnnotationBeanPostProcessor and then value is overridden by application.properties value (screen shot).

o.s.c.e.PropertySourcesPropertyResolver  : Found key 'myExternal_url' in PropertySource 'systemEnvironment' with value of type String
o.s.c.e.PropertySourcesPropertyResolver  : Found key 'myExternal_url' in PropertySource 'environmentProperties' with value of type String

Debug ApplicationProperties

Upvotes: 10

Views: 18502

Answers (3)

Pankaj
Pankaj

Reputation: 2678

Spring indeed supports some sort of naming convention in the form of relaxed binding (as pointed by @Kavithakaran Kanapathippillai). In case of @ConfigurationProperties and @Value annotation, Spring tries to resolve variable from Configuration Source in the order as defined in Externalized Configuration using relaxed binding.

As answered @夢のの夢 - spring correctly match property using @Value("${myExternal_url}") with environment variable but later overridden by @ConfigurationProperties(prefix = "").

As bean ApplicationProperties is defined as @ConfigurationProperties(prefix = ""), Spring tries to match variable with configuration source (using relaxed binding) and finds match in application.propertied variable myExternalUrl and overrides property resolved using @Value("${myExternal_url}"). Problem lies in using both @Value and @ConfigurationProperties(prefix = "").

Side note - @Value supports limited relaxed binding and Spring recommends @Value property names to be defined using kebab-case.

From documentation -

If you do want to use @Value, we recommend that you refer to property names using their canonical form (kebab-case using only lowercase letters). This will allow Spring Boot to use the same logic as it does when relaxed binding @ConfigurationProperties. For example, @Value("{demo.item-price}") will pick up demo.item-price and demo.itemPrice forms from the application.properties file, as well as DEMO_ITEMPRICE from the system environment. If you used @Value("{demo.itemPrice}") instead, demo.item-price and DEMO_ITEMPRICE would not be considered.

Upvotes: 6

夢のの夢
夢のの夢

Reputation: 5826

TL;DR @Value has correct myExternal_Url from system variables injected, but its value is later set by @ConfigurationProperties.

The trace log is correct in that the ordering from Spring will place systemEnvironment before classpath:/application.properties in the propertySource list.

The issue you are having is because of a combination of using both @Value and @ConfigurationProperties to inject/bind your properties. Say these are the values you provide:

system:
  myExternal_Url: foo
  my_external_url: bar
applications.properties:
  myExternal_url: aaa

In your ApplicationProperties:

  @Value("${myExternal_url}")
  private String myExternalUrl; // injected with foo

myExternalUrl is injected correctly with value (foo) you defined in your Environment Variables. However, @ConfigurationProperties binds the values after by using the setter methods. Since it uses relaxed bindings, it checks for different variations of myExternalUrl, it first looks for what's in your system variables and finds that myExternal_url(both camel and Underscore) isn't in one of relaxed binding forms , but then my_external_url (underscore only) is. So my_external_url's value is provided to setter:

public void setMyExternalUrl(String myExternalUrl) { // bar
  this.myExternalUrl = myExternalUrl; // myExternalUrl is reassigned from foo to bar
}

So it should be clear that your @Value would always be overridden since @ConfigurationProperties binds values after. Simply have:

public class ApplicationProperties {
    private String myExternalUrl;
    ...

then have one of binding forms - MY_EXTERNAL_URL, my-external-url, or my_external_url(Maybe there is more) - defined in your system. Then have consistent case in your application.yml in case you don't want your system variables.

my-external-url=aaa

Side Note. It is recommended that you use the form MY_EXTERNAL_URL as system environment variables.

Upvotes: 4

    public class ApplicationProperties {

      private String myExternalUrl;

      public String getMyExternalUrl() {
        return this.myExternalUrl;
      }

      public void setMyExternalUrl(String myExternalUrl) {
        this.myExternalUrl = myExternalUrl;
      }
    }  
  • Please note, both properties will be still present in the Environment but when it is binding, it seems to decide if one overrides the other. You can see this by printing. Here I am getting the environment from applicationContext but you can autowire Environment and test this
        System.out.println(context.getEnvironment()
                             .getProperty("myExternal_url"));
        System.out.println(context.getEnvironment()
                             .getProperty("my_external_url"));
  • To confirm the above point that only @ConfigurationProperties support relaxed binding but not @Value. I created the following class
   @Component
   public class ValueInjection {
    // This prints application properties.
    @Value("${myExternal_url}")
    private String myExternal_url;

    // This prints the environment variable
    @Value("${my_external_url}")
    private String my_external_url;

// If you uncomment this the application will not start saying 
// there is no such property.
// 
//    @Value("${myExternalUrl}")
//    private String myExternalUrl;

 //   ... getters and setters
    }

Upvotes: 2

Related Questions