Question: what’s the output?

Given this application.yml:

freeMarker: # with upper-case M
  templatePath: "/freeMarker/upper-case"
 
freemarker:
  templatePath: "/freemarker/lower-case"

and this configuration bean:

@Component
@ConfigurationProperties(prefix = "freeMarker")
public class FreeMarkerConfigurationSettings {
 
    String templatePath;
 
    public String getTemplatePath() {
        return templatePath;
    }
 
    public void setTemplatePath(String templatePath) {
        this.templatePath = templatePath;
    }
}

what’s the output of this snippet?

@Component
public class PropertiesPrinter implements CommandLineRunner {
 
    @Autowired
    FreeMarkerConfigurationSettings freeMarkerConfigurationSettings;
 
    @Override
    public void run(String... strings) throws Exception {
        System.out.println("templatePath using ConfigurationProperties: " + freeMarkerConfigurationSettings.templatePath);
    }
}

looks like the property should be resolved to “/freeMarker/upper-case”, but actually it’s not. the output is:

templatePath using ConfigurationProperties: /freemarker/lower-case

What? I found a bug in Spring Boot?

No. actually this is a feature: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-relaxed-binding

Spring Boot uses some relaxed rules for binding Environment properties to @ConfigurationProperties beans, so there does not need to be an exact match between the Environment property name and the bean property name. Common examples where this is useful include dash-separated environment properties (for example, context-path binds to contextPath), and capitalized environment properties (for example, PORT binds to port).

ok, it’s a feature, then how does Spring boot choose the one? (from a list of candidates?)

I didn’t find documents with concrete details, so I digged into the source code. Spring did the magic during property binding which is part of the ‘auto-wired’ initialisation: getPropertyValuesForNamePrefix finds the final property value from a list of candidates with a list of “RelaxedNames”, i.e the 2 “propertyValues” we defined in the application.yml:

private MutablePropertyValues getPropertyValuesForNamePrefix(
      MutablePropertyValues propertyValues) {
   if (!StringUtils.hasText(this.namePrefix) && !this.ignoreNestedProperties) {
      return propertyValues;
   }
   MutablePropertyValues rtn = new MutablePropertyValues();
   for (PropertyValue value : propertyValues.getPropertyValues()) {
      String name = value.getName();
      for (String prefix : new RelaxedNames(stripLastDot(this.namePrefix))) {
         for (String separator : new String[] { ".", "_" }) {
            String candidate = (StringUtils.hasLength(prefix) ? prefix + separator
                  : prefix);
            if (name.startsWith(candidate)) {
               name = name.substring(candidate.length());
               if (!(this.ignoreNestedProperties && name.contains("."))) {
                  PropertyOrigin propertyOrigin = OriginCapablePropertyValue
                        .getOrigin(value);
                  rtn.addPropertyValue(new OriginCapablePropertyValue(name,
                        value.getValue(), propertyOrigin));
               }
            }
         }
      }
   }
   return rtn;
}

what is the “RelaxedNames” for “freeMarker” we defined in our configuration bean?

values = {LinkedHashSet@3080} size = 7
 0 = "freeMarker"
 1 = "free_marker"
 2 = "free-marker"
 3 = "freemarker"
 4 = "FREEMARKER"
 5 = "FREE_MARKER"
 6 = "FREE-MARKER"

I would say that “rtn.addPropertyValue()” is not a good name:

/**
 * Add a PropertyValue object, replacing any existing one for the
 * corresponding property or getting merged with it (if applicable).
 * @param pv PropertyValue object to add
 * @return this in order to allow for adding multiple property values in a chain
 */
public MutablePropertyValues addPropertyValue(PropertyValue pv) {
   for (int i = 0; i < this.propertyValueList.size(); i++) {
      PropertyValue currentPv = this.propertyValueList.get(i);
      if (currentPv.getName().equals(pv.getName())) {
         pv = mergeIfRequired(pv, currentPv);
         setPropertyValueAt(pv, i);
         return this;
      }
   }
   this.propertyValueList.add(pv);
   return this;
}

“Add a PropertyValue object, replacing any existing one for the corresponding property or getting merged with it (if applicable).”

Spring boot resolves the property’s value by comparing each properties with a list of “RelaxedNames” and the return the last one it found.

Conclusions

  • Relaxed binding is a nice feature, but it could confuse you with the resolved value: spring boot will return the last found property’s value based on the list of “RelaxedNames”
  • Choose better “prefix” names. e.g. “freemarker” is too general, “obsFreemarker” is better.