Browse Source

Avoid overhead for parsing plain values and simple placeholders

Closes gh-35594
pull/35629/head
Juergen Hoeller 4 months ago
parent
commit
449b85f446
  1. 195
      spring-core/src/main/java/org/springframework/util/PlaceholderParser.java
  2. 5
      spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
  3. 23
      spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java

195
spring-core/src/main/java/org/springframework/util/PlaceholderParser.java

@ -18,7 +18,6 @@ package org.springframework.util; @@ -18,7 +18,6 @@ package org.springframework.util;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -36,10 +35,10 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -36,10 +35,10 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
* that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
* <code>${</code> the prefix, and <code>}</code> the suffix.
*
* <p>A placeholder can also have a default value if its key does not represent a
* known property. The default value is separated from the key using a
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if
* the placeholder resolver does not provide a value for the {@code name}
* <p>A placeholder can also have a default value if its key does not represent
* a known property. The default value is separated from the key using a
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John}
* if the placeholder resolver does not provide a value for the {@code name}
* property.
*
* <p>Placeholders can also have a more complex structure, and the resolution of
@ -50,13 +49,14 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -50,13 +49,14 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
* must be rendered as is, the placeholder can be escaped using an {@code escape}
* character. For instance {@code \${name}} resolves as {@code ${name}}.
*
* <p>The prefix, suffix, separator, and escape characters are configurable. Only
* the prefix and suffix are mandatory, and the support for default values or
* escaping is conditional on providing non-null values for them.
* <p>The prefix, suffix, separator, and escape characters are configurable.
* Only the prefix and suffix are mandatory, and the support for default values
* or escaping is conditional on providing non-null values for them.
*
* <p>This parser makes sure to resolves placeholders as lazily as possible.
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 6.2
*/
final class PlaceholderParser {
@ -120,51 +120,47 @@ final class PlaceholderParser { @@ -120,51 +120,47 @@ final class PlaceholderParser {
* @return the supplied value with placeholders replaced inline
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
ParsedValue parsedValue = parse(value);
List<Part> parts = parse(value, false);
if (parts == null) {
return value;
}
ParsedValue parsedValue = new ParsedValue(value, parts);
PartResolutionContext resolutionContext = new PartResolutionContext(placeholderResolver,
this.prefix, this.suffix, this.ignoreUnresolvablePlaceholders,
candidate -> parse(candidate, false));
return parsedValue.resolve(resolutionContext);
}
/**
* Parse the specified value.
* @param value the value containing the placeholders to be replaced
* @return the different parts that have been identified
*/
ParsedValue parse(String value) {
List<Part> parts = parse(value, false);
return new ParsedValue(value, parts);
}
private List<Part> parse(String value, boolean inPlaceholder) {
LinkedList<Part> parts = new LinkedList<>();
private @Nullable List<Part> parse(String value, boolean inPlaceholder) {
int startIndex = nextStartPrefix(value, 0);
if (startIndex == -1) {
Part part = (inPlaceholder ? createSimplePlaceholderPart(value) : new TextPart(value));
parts.add(part);
return parts;
return null;
}
List<Part> parts = new ArrayList<>(4);
int position = 0;
while (startIndex != -1) {
int endIndex = nextValidEndPrefix(value, startIndex);
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
addText(value, position, startIndex + this.prefix.length(), parts);
position = startIndex + this.prefix.length();
startIndex = nextStartPrefix(value, position);
}
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
addText(value, position, startIndex - 1, parts);
addText(value, startIndex, startIndex + this.prefix.length(), parts);
position = startIndex + this.prefix.length();
startIndex = nextStartPrefix(value, position);
}
else { // Found valid placeholder, recursive parsing
else { // Found valid placeholder, recursive parsing
addText(value, position, startIndex, parts);
String placeholder = value.substring(startIndex + this.prefix.length(), endIndex);
List<Part> placeholderParts = parse(placeholder, true);
parts.addAll(placeholderParts);
if (placeholderParts == null) {
parts.add(createSimplePlaceholderPart(placeholder));
}
else {
parts.addAll(placeholderParts);
}
startIndex = nextStartPrefix(value, endIndex + this.suffix.length());
position = endIndex + this.suffix.length();
}
@ -241,29 +237,6 @@ final class PlaceholderParser { @@ -241,29 +237,6 @@ final class PlaceholderParser {
return new ParsedSection(buffer.toString(), null);
}
private static void addText(String value, int start, int end, LinkedList<Part> parts) {
if (start > end) {
return;
}
String text = value.substring(start, end);
if (!text.isEmpty()) {
if (!parts.isEmpty()) {
Part current = parts.removeLast();
if (current instanceof TextPart textPart) {
parts.add(new TextPart(textPart.text() + text));
}
else {
parts.add(current);
parts.add(new TextPart(text));
}
}
else {
parts.add(new TextPart(text));
}
}
}
private int nextStartPrefix(String value, int index) {
return value.indexOf(this.prefix, index);
}
@ -296,15 +269,46 @@ final class PlaceholderParser { @@ -296,15 +269,46 @@ final class PlaceholderParser {
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
}
record ParsedSection(String key, @Nullable String fallback) {
private static void addText(String value, int start, int end, List<Part> parts) {
if (start >= end) {
return;
}
String text = value.substring(start, end);
if (!parts.isEmpty() && parts.get(parts.size() - 1) instanceof TextPart textPart) {
parts.set(parts.size() - 1, new TextPart(textPart.text() + text));
}
else {
parts.add(new TextPart(text));
}
}
/**
* A representation of the parsing of an input string.
* @param text the raw input string
* @param parts the parts that appear in the string, in order
*/
private record ParsedValue(String text, List<Part> parts) {
public String resolve(PartResolutionContext resolutionContext) {
try {
return Part.resolveAll(this.parts, resolutionContext);
}
catch (PlaceholderResolutionException ex) {
throw ex.withValue(this.text);
}
}
}
private record ParsedSection(String key, @Nullable String fallback) {
}
/**
* Provide the necessary context to handle and resolve underlying placeholders.
*/
static class PartResolutionContext implements PlaceholderResolver {
private static class PartResolutionContext implements PlaceholderResolver {
private final String prefix;
@ -319,7 +323,6 @@ final class PlaceholderParser { @@ -319,7 +323,6 @@ final class PlaceholderParser {
@Nullable
private Set<String> visitedPlaceholders;
PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix,
boolean ignoreUnresolvablePlaceholders, Function<String, List<Part>> parser) {
this.prefix = prefix;
@ -352,7 +355,7 @@ final class PlaceholderParser { @@ -352,7 +355,7 @@ final class PlaceholderParser {
return this.prefix + text + this.suffix;
}
public List<Part> parse(String text) {
public @Nullable List<Part> parse(String text) {
return this.parser.apply(text);
}
@ -367,17 +370,17 @@ final class PlaceholderParser { @@ -367,17 +370,17 @@ final class PlaceholderParser {
}
public void removePlaceholder(String placeholder) {
Assert.state(this.visitedPlaceholders != null, "Visited placeholders must not be null");
this.visitedPlaceholders.remove(placeholder);
if (this.visitedPlaceholders != null) {
this.visitedPlaceholders.remove(placeholder);
}
}
}
/**
* A part is a section of a String containing placeholders to replace.
*/
interface Part {
private interface Part {
/**
* Resolve this part using the specified {@link PartResolutionContext}.
@ -408,30 +411,12 @@ final class PlaceholderParser { @@ -408,30 +411,12 @@ final class PlaceholderParser {
}
/**
* A representation of the parsing of an input string.
* @param text the raw input string
* @param parts the parts that appear in the string, in order
*/
record ParsedValue(String text, List<Part> parts) {
public String resolve(PartResolutionContext resolutionContext) {
try {
return Part.resolveAll(this.parts, resolutionContext);
}
catch (PlaceholderResolutionException ex) {
throw ex.withValue(this.text);
}
}
}
/**
* A base {@link Part} implementation.
*/
abstract static class AbstractPart implements Part {
private abstract static class AbstractPart implements Part {
private final String text;
final String text;
protected AbstractPart(String text) {
this.text = text;
@ -454,29 +439,19 @@ final class PlaceholderParser { @@ -454,29 +439,19 @@ final class PlaceholderParser {
@Nullable
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
String resolvedValue = resolutionContext.resolvePlaceholder(key);
if (resolvedValue != null) {
resolutionContext.flagPlaceholderAsVisited(key);
// Let's check if we need to recursively resolve that value
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
String value = toText(nestedParts);
if (!isTextOnly(nestedParts)) {
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
}
resolutionContext.removePlaceholder(key);
return value;
if (resolvedValue == null) {
// Not found
return null;
}
// Not found
return null;
}
private boolean isTextOnly(List<Part> parts) {
return parts.stream().allMatch(TextPart.class::isInstance);
}
private String toText(List<Part> parts) {
StringBuilder sb = new StringBuilder();
parts.forEach(part -> sb.append(part.text()));
return sb.toString();
// Let's check if we need to recursively resolve that value
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
if (nestedParts == null) {
return resolvedValue;
}
resolutionContext.flagPlaceholderAsVisited(key);
String value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
resolutionContext.removePlaceholder(key);
return value;
}
}
@ -484,7 +459,7 @@ final class PlaceholderParser { @@ -484,7 +459,7 @@ final class PlaceholderParser {
/**
* A {@link Part} implementation that does not contain a valid placeholder.
*/
static class TextPart extends AbstractPart {
private static class TextPart extends AbstractPart {
/**
* Create a new instance.
@ -496,7 +471,7 @@ final class PlaceholderParser { @@ -496,7 +471,7 @@ final class PlaceholderParser {
@Override
public String resolve(PartResolutionContext resolutionContext) {
return text();
return this.text;
}
}
@ -505,7 +480,7 @@ final class PlaceholderParser { @@ -505,7 +480,7 @@ final class PlaceholderParser {
* A {@link Part} implementation that represents a single placeholder with
* a hard-coded fallback.
*/
static class SimplePlaceholderPart extends AbstractPart {
private static class SimplePlaceholderPart extends AbstractPart {
private final String key;
@ -533,13 +508,13 @@ final class PlaceholderParser { @@ -533,13 +508,13 @@ final class PlaceholderParser {
else if (this.fallback != null) {
return this.fallback;
}
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
}
@Nullable
private String resolveRecursively(PartResolutionContext resolutionContext) {
if (!this.text().equals(this.key)) {
String value = resolveRecursively(resolutionContext, this.text());
if (!this.text.equals(this.key)) {
String value = resolveRecursively(resolutionContext, this.text);
if (value != null) {
return value;
}
@ -553,7 +528,7 @@ final class PlaceholderParser { @@ -553,7 +528,7 @@ final class PlaceholderParser {
* A {@link Part} implementation that represents a single placeholder
* containing nested placeholders.
*/
static class NestedPlaceholderPart extends AbstractPart {
private static class NestedPlaceholderPart extends AbstractPart {
private final List<Part> keyParts;
@ -582,7 +557,7 @@ final class PlaceholderParser { @@ -582,7 +557,7 @@ final class PlaceholderParser {
else if (this.defaultParts != null) {
return Part.resolveAll(this.defaultParts, resolutionContext);
}
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
}
}

5
spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java

@ -97,7 +97,7 @@ public class PropertyPlaceholderHelper { @@ -97,7 +97,7 @@ public class PropertyPlaceholderHelper {
* @param properties the {@code Properties} to use for replacement
* @return the supplied value with placeholders replaced inline
*/
public String replacePlaceholders(String value, final Properties properties) {
public String replacePlaceholders(String value, Properties properties) {
Assert.notNull(properties, "'properties' must not be null");
return replacePlaceholders(value, properties::getProperty);
}
@ -111,9 +111,10 @@ public class PropertyPlaceholderHelper { @@ -111,9 +111,10 @@ public class PropertyPlaceholderHelper {
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver);
return this.parser.replacePlaceholders(value, placeholderResolver);
}
@Deprecated(since = "6.2.12", forRemoval = true)
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver) {
return this.parser.replacePlaceholders(value, placeholderResolver);
}

23
spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java

@ -26,8 +26,6 @@ import org.junit.jupiter.params.provider.Arguments; @@ -26,8 +26,6 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InOrder;
import org.springframework.util.PlaceholderParser.ParsedValue;
import org.springframework.util.PlaceholderParser.TextPart;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
import static org.assertj.core.api.Assertions.assertThat;
@ -43,10 +41,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -43,10 +41,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*
* @author Stephane Nicoll
* @author Sam Brannen
* @author Juergen Hoeller
*/
class PlaceholderParserTests {
@Nested // Tests with only the basic placeholder feature enabled
@Nested // Tests with only the basic placeholder feature enabled
class OnlyPlaceholderTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", null, null, true);
@ -82,7 +81,7 @@ class PlaceholderParserTests { @@ -82,7 +81,7 @@ class PlaceholderParserTests {
Map<String, String> properties = Map.of(
"p1", "v1",
"p2", "v2",
"p3", "${p1}:${p2}", // nested placeholders
"p3", "${p1}:${p2}", // nested placeholders
"p4", "${p3}", // deeply nested placeholders
"p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
@ -154,14 +153,13 @@ class PlaceholderParserTests { @@ -154,14 +153,13 @@ class PlaceholderParserTests {
@Test
void textWithInvalidPlaceholderSyntaxIsMerged() {
String text = "test${of${with${and${";
ParsedValue parsedValue = this.parser.parse(text);
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class,
textPart -> assertThat(textPart.text()).isEqualTo(text));
assertThat(this.parser.replacePlaceholders(text,
placeholder -> {throw new UnsupportedOperationException();})).isEqualTo(text);
}
}
@Nested // Tests with the use of a separator
@Nested // Tests with the use of a separator
class DefaultValueTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", null, true);
@ -195,7 +193,7 @@ class PlaceholderParserTests { @@ -195,7 +193,7 @@ class PlaceholderParserTests {
Map<String, String> properties = Map.of(
"p1", "v1",
"p2", "v2",
"p3", "${p1}:${p2}", // nested placeholders
"p3", "${p1}:${p2}", // nested placeholders
"p4", "${p3}", // deeply nested placeholders
"p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder
"p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
@ -259,9 +257,9 @@ class PlaceholderParserTests { @@ -259,9 +257,9 @@ class PlaceholderParserTests {
assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
}
}
/**
* Tests that use the escape character.
*/
@ -341,9 +339,9 @@ class PlaceholderParserTests { @@ -341,9 +339,9 @@ class PlaceholderParserTests {
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
}
}
@Nested
class ExceptionTests {
@ -378,7 +376,6 @@ class PlaceholderParserTests { @@ -378,7 +376,6 @@ class PlaceholderParserTests {
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
.withNoCause();
}
}

Loading…
Cancel
Save