In this article, we will discuss how spring parses and resolves a string with placeholders. We will look into the algorithm and all the related scenarios.
Example
Lets suppose "Buy ${${quantity} ${size}} ${container} of fresh ${${berries}}" is the string which needs to be parsed and resolved. If the placeholder values are:
![]() |
Property resolver algorithm |
The final parsed string would be “Buy 1 large basket of fresh Marionberries”.
Test case
In the below test case, we create the property sources and resolve the placeholders based on it. PropertySourcesPropertyResolver is the property resolver. resolver.resolveRequiredPlaceholders(string)
parses the string passed in, resolves the property place holders and returns us the resolved one. We create the property sources using MutablePropertySources.
@Test public void resolveMultiplePlaceholders() { MutablePropertySources propertySources = new MutablePropertySources(); PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); propertySources.addLast(new MockPropertySource() .withProperty("berries", "${berriesType}Berries") .withProperty("berriesType", "black") .withProperty("blackBerries", "Marionberries") .withProperty("container", "basket") .withProperty("size", "large") .withProperty("quantity", "one") .withProperty("one large", "1 large")); assertThat(resolver.resolveRequiredPlaceholders("Buy ${${quantity} ${size}} ${container} of fresh ${${berries}}"), equalTo("Buy 1 large basket of fresh Marionberries")); }
Place Holder Scenarios
What are the scenarios involved?
- A string might contain more than one place holder.
- The place holder value itself can be composed of sub placeholders.
- In case there is no place holder value but still there should be a way to resort to a default value.
- In case the we don’t have a default value, we should either fail it or proceed with further parsing ignoring the unresolved placeholder.
Place Holder Resolver logic
The parsing logic and the mechanism used to resolve the placeholders is decoupled as there can be many ways to resolve a property. For example the property source might come from system environment, system properties, servlet etc.
In this article we will only concentrate on the parsing logic. Since there can be more than one place holder in a string and the resolved value itself can contain further placeholders, the parsing is done recursively.
Default values can be supplied using the “:” separator between key and value.
Below diagram contains the flow.
![]() |
Property resolver algorithm |
Based on the above mentioned example, where the string is "Buy ${${quantity} ${size}} ${container} of fresh ${${berries}}".
And the placeholder values:
![]() |
Property resolver algorithm |
Below diagram shows how each placeholder is parsed in sequence.
![]() |
Property resolver algorithm |
Placeholder’s default value
If a placeholder’s value is not specified, it can be set to a default value.
To test this, we modify our example. In the test below, “berries” is not added to the property sources. For the test to work, we should a default value to the placeholder “berries”.
This is done by using a separator. Instead of ${berries}, we now pass a default value to it using a separator “:” -> ${berries:blackberries}.>
@Test public void defaultPlaceholderValue() { MutablePropertySources propertySources = new MutablePropertySources(); PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); propertySources.addLast(new MockPropertySource() .withProperty("berriesType", "black") .withProperty("blackBerries", "Marionberries") .withProperty("container", "Basket") .withProperty("size", "large") .withProperty("quantity", "one") .withProperty("one large", "1 large")); assertThat(resolver.resolveRequiredPlaceholders("Buy ${${quantity} ${size}} ${container} of fresh ${${berries:blackBerries}}"), equalTo("Buy 1 large Basket of fresh Marionberries")); }
The default value itself can be another placeholder. For example, instead of ${berries:blackBerries}, we now use ${berries:${blackBerries}} where the default value is ${blackBerries}. A new placeholder is been added for “Marionberries”-> “Marion blackberry, Orgeon”. The final parsed string would be "Buy 1 large Basket of fresh Marion blackberry, Orgeon".
@Test public void defaultValueAsPlaceholder() { MutablePropertySources propertySources = new MutablePropertySources(); PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); propertySources.addLast(new MockPropertySource() .withProperty("berriesType", "black") .withProperty("blackBerries", "Marionberries") .withProperty("Marionberries", "Marion blackberry, Orgeon") .withProperty("container", "Basket") .withProperty("size", "large") .withProperty("quantity", "one") .withProperty("one large", "1 large")); assertThat(resolver.resolveRequiredPlaceholders("Buy ${${quantity} ${size}} ${container} of fresh ${${berries:${blackBerries}}}"), equalTo("Buy 1 large Basket of fresh Marion blackberry, Orgeon")); }
Missing placeholder value
By default, if a placeholder’s value is not specified, the test is going to fail with an IllegalArgumentException.
@Test public void missingPlaceholderError() { MutablePropertySources propertySources = new MutablePropertySources(); PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); propertySources.addLast(new MockPropertySource() .withProperty("two", "2")); try { resolver.resolveRequiredPlaceholders("${one} and ${two}"); fail("should have failed as placeholder value for "two" is missing"); } catch (IllegalArgumentException e) { assertTrue(true); } }
We may want to ignore the missing placeholder and parse rest of the string instead of aborting the parsing, in such case, we should use resolvePlaceholders instead of the stricter version resolveRequiredPlaceholders
@Test public void ignoreMissingPlaceholder() { MutablePropertySources propertySources = new MutablePropertySources(); PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); propertySources.addLast(new MockPropertySource() .withProperty("two", "2")); assertThat(resolver.resolvePlaceholders("${one} and ${two}"), equalTo("${one} and 2")); }