Skip to content

Commit 03ae97b

Browse files
committed
Introduce Spring property for default escape character for placeholders
Spring Framework 6.2 introduced support for an escape character for property placeholders (by default '\'). However, as of Spring Framework 6.2.6, there was no way to either escape the escape character or disable escape character support. For example, given a `username` property configured with the value of `Jane.Smith` and a `DOMAIN\${username}` configuration string, property placeholder replacement used to result in `DOMAIN\Jane.Smith` prior to 6.2 but now results in `DOMAIN${username}`. Similarly, an attempt to escape the escape character via `DOMAIN\\${username}` results in `DOMAIN\${username}`. In theory, one should be able to disable use of an escape character altogether, and that is currently possible by invoking setEscapeCharacter(null) on AbstractPropertyResolver and PlaceholderConfigurerSupport (the superclass of PropertySourcesPlaceholderConfigurer). However, in reality, there are two hurdles. - As of 6.2.6, an invocation of setEscapeCharacter(null) on a PropertySourcesPlaceholderConfigurer applied to its internal top-level PropertySourcesPropertyResolver but not to any nested PropertySourcesPropertyResolver, which means that the `null` escape character could not be effectively applied. - Users may not have an easy way to explicitly set the escape character to `null` for a PropertyResolver or PropertySourcesPlaceholderConfigurer. For example, Spring Boot auto-configures a PropertySourcesPlaceholderConfigurer with the default escape character enabled. This first issue above has recently been addressed by gh-34861. This commit therefore addresses the second issue as follows. - To allow developers to easily revert to the pre-6.2 behavior without changes to code or configuration strings, this commit introduces a `spring.placeholder.escapeCharacter.default` property for use with SpringProperties which globally sets the default escape character that is automatically configured in AbstractPropertyResolver and PlaceholderConfigurerSupport. - Setting the property to an empty string sets the default escape character to `null`, effectively disabling the default support for escape characters. spring.placeholder.escapeCharacter.default = - Setting the property to any other character sets the default escape character to that specific character. spring.placeholder.escapeCharacter.default = ~ - Setting the property to a string containing more than one character results in an exception. - Developers are still able to configure an explicit escape character in AbstractPropertyResolver and PlaceholderConfigurerSupport if they choose to do so. - Third-party components that wish to rely on the same feature can invoke AbstractPropertyResolver.getDefaultEscapeCharacter() to obtain the globally configured default escape character. See gh-9628 See gh-34315 See gh-34861 Closes gh-34865
1 parent e34cdc2 commit 03ae97b

File tree

8 files changed

+373
-17
lines changed

8 files changed

+373
-17
lines changed

framework-docs/modules/ROOT/pages/appendix.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ for details.
103103
{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
104104
for details.
105105

106+
| `spring.placeholder.escapeCharacter.default`
107+
| The default escape character for property placeholder support. If not set, `'\'` will
108+
be used. Can be set to a custom escape character or an empty string to disable support
109+
for an escape character. The default escape character be explicitly overridden in
110+
`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See
111+
{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`]
112+
for details.
113+
106114
| `spring.test.aot.processing.failOnError`
107115
| A boolean flag that controls whether errors encountered during AOT processing in the
108116
_Spring TestContext Framework_ should result in an exception that fails the overall process.

framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig
101101

102102
Using the above configuration ensures Spring initialization failure if any `${}`
103103
placeholder could not be resolved. It is also possible to use methods like
104-
`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or
105-
`setEscapeCharacter` to customize placeholders.
104+
`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or
105+
`setEscapeCharacter()` to customize the placeholder syntax. In addition, the default
106+
escape character can be changed or disabled globally by setting the
107+
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
108+
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
106109

107110
NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that
108111
will get properties from `application.properties` and `application.yml` files.

framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the
314314

315315

316316
[[beans-factory-placeholderconfigurer]]
317-
=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer`
317+
=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer`
318318

319319
You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values
320320
from a bean definition in a separate file by using the standard Java `Properties` format.
@@ -341,7 +341,7 @@ with placeholder values is defined:
341341

342342
The example shows properties configured from an external `Properties` file. At runtime,
343343
a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some
344-
properties of the DataSource. The values to replace are specified as placeholders of the
344+
properties of the `DataSource`. The values to replace are specified as placeholders of the
345345
form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style.
346346

347347
The actual values come from another file in the standard Java `Properties` format:
@@ -355,10 +355,13 @@ jdbc.password=root
355355
----
356356

357357
Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and
358-
the same applies for other placeholder values that match keys in the properties file.
359-
The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
360-
attributes of a bean definition. Furthermore, you can customize the placeholder prefix, suffix,
361-
default value separator, and escape character.
358+
the same applies for other placeholder values that match keys in the properties file. The
359+
`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
360+
attributes of a bean definition. Furthermore, you can customize the placeholder prefix,
361+
suffix, default value separator, and escape character. In addition, the default escape
362+
character can be changed or disabled globally by setting the
363+
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
364+
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
362365

363366
With the `context` namespace, you can configure property placeholders
364367
with a dedicated configuration element. You can provide one or more locations as a

framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ NOTE: If you use Spring Boot, you should probably use
190190
instead of `@Value` annotations.
191191

192192
As an alternative, you can customize the property placeholder prefix by declaring the
193-
following configuration bean:
193+
following `PropertySourcesPlaceholderConfigurer` bean:
194194

195195
[source,kotlin,indent=0]
196196
----
@@ -200,8 +200,10 @@ following configuration bean:
200200
}
201201
----
202202

203-
You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`)
204-
that uses the `${...}` syntax, with configuration beans, as the following example shows:
203+
You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use
204+
the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by
205+
declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example
206+
shows:
205207

206208
[source,kotlin,indent=0]
207209
----
@@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl
215217
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
216218
----
217219

220+
In addition, the default escape character can be changed or disabled globally by setting
221+
the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or
222+
via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
218223

219224

220225
[[checked-exceptions]]

spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.springframework.beans.factory.BeanFactory;
2121
import org.springframework.beans.factory.BeanFactoryAware;
2222
import org.springframework.beans.factory.BeanNameAware;
23+
import org.springframework.core.env.AbstractPropertyResolver;
2324
import org.springframework.lang.Nullable;
2425
import org.springframework.util.StringValueResolver;
2526
import org.springframework.util.SystemPropertyUtils;
@@ -85,6 +86,7 @@
8586
*
8687
* @author Chris Beams
8788
* @author Juergen Hoeller
89+
* @author Sam Brannen
8890
* @since 3.1
8991
* @see PropertyPlaceholderConfigurer
9092
* @see org.springframework.context.support.PropertySourcesPlaceholderConfigurer
@@ -101,7 +103,11 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
101103
/** Default value separator: {@value}. */
102104
public static final String DEFAULT_VALUE_SEPARATOR = SystemPropertyUtils.VALUE_SEPARATOR;
103105

104-
/** Default escape character: {@code '\'}. */
106+
/**
107+
* Default escape character: {@code '\'}.
108+
* @since 6.2
109+
* @see AbstractPropertyResolver#getDefaultEscapeCharacter()
110+
*/
105111
public static final Character DEFAULT_ESCAPE_CHARACTER = SystemPropertyUtils.ESCAPE_CHARACTER;
106112

107113

@@ -115,9 +121,11 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
115121
@Nullable
116122
protected String valueSeparator = DEFAULT_VALUE_SEPARATOR;
117123

118-
/** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */
124+
/**
125+
* The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
126+
*/
119127
@Nullable
120-
protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER;
128+
protected Character escapeCharacter = AbstractPropertyResolver.getDefaultEscapeCharacter();
121129

122130
protected boolean trimValues = false;
123131

@@ -164,6 +172,7 @@ public void setValueSeparator(@Nullable String valueSeparator) {
164172
* {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the
165173
* {@linkplain #setValueSeparator(String) value separator}, or {@code null}
166174
* if no escaping should take place.
175+
* <p>The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
167176
* @since 6.2
168177
*/
169178
public void setEscapeCharacter(@Nullable Character escapeCharacter) {

spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616

1717
package org.springframework.context.support;
1818

19+
import java.lang.reflect.Field;
1920
import java.util.ArrayList;
2021
import java.util.Collections;
2122
import java.util.List;
2223
import java.util.Optional;
2324
import java.util.Properties;
2425

26+
import org.junit.jupiter.api.AfterAll;
27+
import org.junit.jupiter.api.BeforeEach;
2528
import org.junit.jupiter.api.Nested;
2629
import org.junit.jupiter.api.Test;
2730
import org.junit.jupiter.params.ParameterizedTest;
@@ -37,7 +40,9 @@
3740
import org.springframework.context.annotation.Bean;
3841
import org.springframework.context.annotation.Configuration;
3942
import org.springframework.context.annotation.Scope;
43+
import org.springframework.core.SpringProperties;
4044
import org.springframework.core.convert.support.DefaultConversionService;
45+
import org.springframework.core.env.AbstractPropertyResolver;
4146
import org.springframework.core.env.EnumerablePropertySource;
4247
import org.springframework.core.env.MutablePropertySources;
4348
import org.springframework.core.env.PropertySource;
@@ -47,12 +52,15 @@
4752
import org.springframework.core.testfixture.env.MockPropertySource;
4853
import org.springframework.mock.env.MockEnvironment;
4954
import org.springframework.util.PlaceholderResolutionException;
55+
import org.springframework.util.ReflectionUtils;
5056

5157
import static org.assertj.core.api.Assertions.assertThat;
5258
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
59+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
5360
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
5461
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition;
5562
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition;
63+
import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME;
5664

5765
/**
5866
* Tests for {@link PropertySourcesPlaceholderConfigurer}.
@@ -667,6 +675,108 @@ private static DefaultListableBeanFactory createBeanFactory() {
667675
}
668676

669677

678+
/**
679+
* Tests that globally set the default escape character (or disable it) and
680+
* rely on nested placeholder resolution.
681+
*/
682+
@Nested
683+
class GlobalDefaultEscapeCharacterTests {
684+
685+
private static final Field defaultEscapeCharacterField =
686+
ReflectionUtils.findField(AbstractPropertyResolver.class, "defaultEscapeCharacter");
687+
688+
static {
689+
ReflectionUtils.makeAccessible(defaultEscapeCharacterField);
690+
}
691+
692+
693+
@BeforeEach
694+
void resetStateBeforeEachTest() {
695+
resetState();
696+
}
697+
698+
@AfterAll
699+
static void resetState() {
700+
ReflectionUtils.setField(defaultEscapeCharacterField, null, Character.MIN_VALUE);
701+
setSpringProperty(null);
702+
}
703+
704+
705+
@Test // gh-34865
706+
void defaultEscapeCharacterSetToXyz() {
707+
setSpringProperty("XYZ");
708+
709+
assertThatIllegalArgumentException()
710+
.isThrownBy(PropertySourcesPlaceholderConfigurer::new)
711+
.withMessage("Value [XYZ] for property [%s] must be a single character or an empty string",
712+
DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME);
713+
}
714+
715+
@Test // gh-34865
716+
void defaultEscapeCharacterDisabled() {
717+
setSpringProperty("");
718+
719+
MockEnvironment env = new MockEnvironment()
720+
.withProperty("user.home", "admin")
721+
.withProperty("my.property", "\\DOMAIN\\${user.home}");
722+
723+
DefaultListableBeanFactory bf = createBeanFactory();
724+
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
725+
ppc.setEnvironment(env);
726+
ppc.postProcessBeanFactory(bf);
727+
728+
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin");
729+
}
730+
731+
@Test // gh-34865
732+
void defaultEscapeCharacterSetToBackslash() {
733+
setSpringProperty("\\");
734+
735+
MockEnvironment env = new MockEnvironment()
736+
.withProperty("user.home", "admin")
737+
.withProperty("my.property", "\\DOMAIN\\${user.home}");
738+
739+
DefaultListableBeanFactory bf = createBeanFactory();
740+
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
741+
ppc.setEnvironment(env);
742+
ppc.postProcessBeanFactory(bf);
743+
744+
// \DOMAIN\${user.home} resolves to \DOMAIN${user.home} instead of \DOMAIN\admin
745+
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN${user.home}");
746+
}
747+
748+
@Test // gh-34865
749+
void defaultEscapeCharacterSetToTilde() {
750+
setSpringProperty("~");
751+
752+
MockEnvironment env = new MockEnvironment()
753+
.withProperty("user.home", "admin\\~${nested}")
754+
.withProperty("my.property", "DOMAIN\\${user.home}\\~${enigma}");
755+
756+
DefaultListableBeanFactory bf = createBeanFactory();
757+
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
758+
ppc.setEnvironment(env);
759+
ppc.postProcessBeanFactory(bf);
760+
761+
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}");
762+
}
763+
764+
private static void setSpringProperty(String value) {
765+
SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value);
766+
}
767+
768+
private static DefaultListableBeanFactory createBeanFactory() {
769+
BeanDefinition beanDefinition = genericBeanDefinition(TestBean.class)
770+
.addPropertyValue("name", "${my.property}")
771+
.getBeanDefinition();
772+
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
773+
bf.registerBeanDefinition("testBean",beanDefinition);
774+
return bf;
775+
}
776+
777+
}
778+
779+
670780
private static class OptionalTestBean {
671781

672782
private Optional<String> name;

0 commit comments

Comments
 (0)