Skip to content

Commit 5d22aa9

Browse files
committed
Add support for escaping prefix and separator in property placeholders
Closes gh-9628 Closes gh-26268
2 parents 0fbfecd + e3aa5b6 commit 5d22aa9

File tree

27 files changed

+1114
-188
lines changed

27 files changed

+1114
-188
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ 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`, or `setValueSeparator` to customize
105-
placeholders.
104+
`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or
105+
`setEscapeCharacter` to customize placeholders.
106106

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

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -100,6 +100,8 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
100100
/** Default value separator: {@value}. */
101101
public static final String DEFAULT_VALUE_SEPARATOR = ":";
102102

103+
/** Default escape character: {@value}. */
104+
public static final Character DEFAULT_ESCAPE_CHARACTER = '\\';
103105

104106
/** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */
105107
protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;
@@ -111,6 +113,10 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
111113
@Nullable
112114
protected String valueSeparator = DEFAULT_VALUE_SEPARATOR;
113115

116+
/** Defaults to {@value #DEFAULT_ESCAPE_CHARACTER}. */
117+
@Nullable
118+
protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER;
119+
114120
protected boolean trimValues = false;
115121

116122
@Nullable
@@ -151,6 +157,17 @@ public void setValueSeparator(@Nullable String valueSeparator) {
151157
this.valueSeparator = valueSeparator;
152158
}
153159

160+
/**
161+
* Specify the escape character to use to ignore placeholder prefix
162+
* or value separator, or {@code null} if no escaping should take
163+
* place.
164+
* <p>Default is {@value #DEFAULT_ESCAPE_CHARACTER}.
165+
* @since 6.2
166+
*/
167+
public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) {
168+
this.escapeCharacter = escsEscapeCharacter;
169+
}
170+
154171
/**
155172
* Specify whether to trim resolved values before applying them,
156173
* removing superfluous whitespace from the beginning and end.

Diff for: spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -234,7 +234,8 @@ private class PlaceholderResolvingStringValueResolver implements StringValueReso
234234

235235
public PlaceholderResolvingStringValueResolver(Properties props) {
236236
this.helper = new PropertyPlaceholderHelper(
237-
placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders);
237+
placeholderPrefix, placeholderSuffix, valueSeparator,
238+
ignoreUnresolvablePlaceholders, escapeCharacter);
238239
this.resolver = new PropertyPlaceholderConfigurerResolver(props);
239240
}
240241

Diff for: spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -193,6 +193,7 @@ protected void processProperties(ConfigurableListableBeanFactory beanFactoryToPr
193193
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
194194
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
195195
propertyResolver.setValueSeparator(this.valueSeparator);
196+
propertyResolver.setEscapeCharacter(this.escapeCharacter);
196197

197198
StringValueResolver valueResolver = strVal -> {
198199
String resolved = (this.ignoreUnresolvablePlaceholders ?

Diff for: spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,6 +40,7 @@
4040
import org.springframework.core.io.support.EncodedResource;
4141
import org.springframework.core.io.support.PropertiesLoaderUtils;
4242
import org.springframework.core.io.support.PropertySourceFactory;
43+
import org.springframework.util.PlaceholderResolutionException;
4344

4445
import static org.assertj.core.api.Assertions.assertThat;
4546
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -132,7 +133,7 @@ void withCustomFactoryAsMeta() {
132133
void withUnresolvablePlaceholder() {
133134
assertThatExceptionOfType(BeanDefinitionStoreException.class)
134135
.isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class))
135-
.withCauseInstanceOf(IllegalArgumentException.class);
136+
.withCauseInstanceOf(PlaceholderResolutionException.class);
136137
}
137138

138139
@Test

Diff for: spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
import org.springframework.context.support.GenericXmlApplicationContext;
2929
import org.springframework.core.io.ClassPathResource;
3030
import org.springframework.mock.env.MockEnvironment;
31+
import org.springframework.util.PlaceholderResolutionException;
3132

3233
import static org.assertj.core.api.Assertions.assertThat;
3334
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -136,7 +137,7 @@ void propertyPlaceholderLocationWithSystemPropertyMissing() {
136137
assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() ->
137138
new ClassPathXmlApplicationContext("contextNamespaceHandlerTests-location-placeholder.xml", getClass()))
138139
.havingRootCause()
139-
.isInstanceOf(IllegalArgumentException.class)
140+
.isInstanceOf(PlaceholderResolutionException.class)
140141
.withMessage("Could not resolve placeholder 'foo' in value \"${foo}\"");
141142
}
142143

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.core.io.Resource;
3838
import org.springframework.core.testfixture.env.MockPropertySource;
3939
import org.springframework.mock.env.MockEnvironment;
40+
import org.springframework.util.PlaceholderResolutionException;
4041

4142
import static org.assertj.core.api.Assertions.assertThat;
4243
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -170,7 +171,7 @@ void ignoreUnresolvablePlaceholders_falseIsDefault() {
170171
assertThatExceptionOfType(BeanDefinitionStoreException.class)
171172
.isThrownBy(() -> ppc.postProcessBeanFactory(bf))
172173
.havingCause()
173-
.isExactlyInstanceOf(IllegalArgumentException.class)
174+
.isExactlyInstanceOf(PlaceholderResolutionException.class)
174175
.withMessage("Could not resolve placeholder 'my.name' in value \"${my.name}\"");
175176
}
176177

@@ -201,8 +202,8 @@ public void ignoreUnresolvablePlaceholdersInAtValueAnnotation__falseIsDefault()
201202
assertThatExceptionOfType(BeanCreationException.class)
202203
.isThrownBy(context::refresh)
203204
.havingCause()
204-
.isExactlyInstanceOf(IllegalArgumentException.class)
205-
.withMessage("Could not resolve placeholder 'enigma' in value \"${enigma}\"");
205+
.isExactlyInstanceOf(PlaceholderResolutionException.class)
206+
.withMessage("Could not resolve placeholder 'enigma' in value \"${enigma}\" <-- \"${my.key}\"");
206207
}
207208

208209
@Test

Diff for: spring-core/spring-core.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ dependencies {
102102
testImplementation("jakarta.xml.bind:jakarta.xml.bind-api")
103103
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
104104
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
105+
testImplementation("org.mockito:mockito-core")
105106
testImplementation("org.skyscreamer:jsonassert")
106107
testImplementation("org.xmlunit:xmlunit-assertj")
107108
testImplementation("org.xmlunit:xmlunit-matchers")

Diff for: spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -521,6 +521,11 @@ public void setValueSeparator(@Nullable String valueSeparator) {
521521
this.propertyResolver.setValueSeparator(valueSeparator);
522522
}
523523

524+
@Override
525+
public void setEscapeCharacter(@Nullable Character escapeCharacter) {
526+
this.propertyResolver.setEscapeCharacter(escapeCharacter);
527+
}
528+
524529
@Override
525530
public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) {
526531
this.propertyResolver.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders);

Diff for: spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,6 +61,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
6161
@Nullable
6262
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
6363

64+
@Nullable
65+
private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER;
66+
6467
private final Set<String> requiredProperties = new LinkedHashSet<>();
6568

6669

@@ -121,6 +124,19 @@ public void setValueSeparator(@Nullable String valueSeparator) {
121124
this.valueSeparator = valueSeparator;
122125
}
123126

127+
/**
128+
* Specify the escape character to use to ignore placeholder prefix
129+
* or value separator, or {@code null} if no escaping should take
130+
* place.
131+
* <p>The default is "\".
132+
* @since 6.2
133+
* @see org.springframework.util.SystemPropertyUtils#ESCAPE_CHARACTER
134+
*/
135+
@Override
136+
public void setEscapeCharacter(@Nullable Character escapeCharacter) {
137+
this.escapeCharacter = escapeCharacter;
138+
}
139+
124140
/**
125141
* Set whether to throw an exception when encountering an unresolvable placeholder
126142
* nested within the value of a given property. A {@code false} value indicates strict
@@ -232,7 +248,7 @@ protected String resolveNestedPlaceholders(String value) {
232248

233249
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
234250
return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
235-
this.valueSeparator, ignoreUnresolvablePlaceholders);
251+
this.valueSeparator, ignoreUnresolvablePlaceholders, this.escapeCharacter);
236252
}
237253

238254
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {

Diff for: spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -74,6 +74,14 @@ public interface ConfigurablePropertyResolver extends PropertyResolver {
7474
*/
7575
void setValueSeparator(@Nullable String valueSeparator);
7676

77+
/**
78+
* Specify the escape character to use to ignore placeholder prefix
79+
* or value separator, or {@code null} if no escaping should take
80+
* place.
81+
* @since 6.2
82+
*/
83+
void setEscapeCharacter(@Nullable Character escapeCharacter);
84+
7785
/**
7886
* Set whether to throw an exception when encountering an unresolvable placeholder
7987
* nested within the value of a given property. A {@code false} value indicates strict

Diff for: spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@
3636
import org.springframework.core.io.ResourceLoader;
3737
import org.springframework.lang.Nullable;
3838
import org.springframework.util.Assert;
39+
import org.springframework.util.PlaceholderResolutionException;
3940
import org.springframework.util.ReflectionUtils;
4041

4142
/**
@@ -93,8 +94,8 @@ public void processPropertySource(PropertySourceDescriptor descriptor) throws IO
9394
}
9495
}
9596
catch (RuntimeException | IOException ex) {
96-
// Placeholders not resolvable (IllegalArgumentException) or resource not found when trying to open it
97-
if (ignoreResourceNotFound && (ex instanceof IllegalArgumentException || isIgnorableException(ex) ||
97+
// Placeholders not resolvable or resource not found when trying to open it
98+
if (ignoreResourceNotFound && (ex instanceof PlaceholderResolutionException || isIgnorableException(ex) ||
9899
isIgnorableException(ex.getCause()))) {
99100
if (logger.isInfoEnabled()) {
100101
logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());

0 commit comments

Comments
 (0)