Skip to content

Commit 449b85f

Browse files
committed
Avoid overhead for parsing plain values and simple placeholders
Closes gh-35594
1 parent c2a66e7 commit 449b85f

File tree

3 files changed

+98
-125
lines changed

3 files changed

+98
-125
lines changed

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

Lines changed: 85 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.HashSet;
21-
import java.util.LinkedList;
2221
import java.util.List;
2322
import java.util.Map;
2423
import java.util.Set;
@@ -36,10 +35,10 @@
3635
* that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
3736
* <code>${</code> the prefix, and <code>}</code> the suffix.
3837
*
39-
* <p>A placeholder can also have a default value if its key does not represent a
40-
* known property. The default value is separated from the key using a
41-
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if
42-
* the placeholder resolver does not provide a value for the {@code name}
38+
* <p>A placeholder can also have a default value if its key does not represent
39+
* a known property. The default value is separated from the key using a
40+
* {@code separator}. For instance {@code ${name:John}} resolves to {@code John}
41+
* if the placeholder resolver does not provide a value for the {@code name}
4342
* property.
4443
*
4544
* <p>Placeholders can also have a more complex structure, and the resolution of
@@ -50,13 +49,14 @@
5049
* must be rendered as is, the placeholder can be escaped using an {@code escape}
5150
* character. For instance {@code \${name}} resolves as {@code ${name}}.
5251
*
53-
* <p>The prefix, suffix, separator, and escape characters are configurable. Only
54-
* the prefix and suffix are mandatory, and the support for default values or
55-
* escaping is conditional on providing non-null values for them.
52+
* <p>The prefix, suffix, separator, and escape characters are configurable.
53+
* Only the prefix and suffix are mandatory, and the support for default values
54+
* or escaping is conditional on providing non-null values for them.
5655
*
5756
* <p>This parser makes sure to resolves placeholders as lazily as possible.
5857
*
5958
* @author Stephane Nicoll
59+
* @author Juergen Hoeller
6060
* @since 6.2
6161
*/
6262
final class PlaceholderParser {
@@ -120,51 +120,47 @@ final class PlaceholderParser {
120120
* @return the supplied value with placeholders replaced inline
121121
*/
122122
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
123-
Assert.notNull(value, "'value' must not be null");
124-
ParsedValue parsedValue = parse(value);
123+
List<Part> parts = parse(value, false);
124+
if (parts == null) {
125+
return value;
126+
}
127+
ParsedValue parsedValue = new ParsedValue(value, parts);
125128
PartResolutionContext resolutionContext = new PartResolutionContext(placeholderResolver,
126129
this.prefix, this.suffix, this.ignoreUnresolvablePlaceholders,
127130
candidate -> parse(candidate, false));
128131
return parsedValue.resolve(resolutionContext);
129132
}
130133

131-
/**
132-
* Parse the specified value.
133-
* @param value the value containing the placeholders to be replaced
134-
* @return the different parts that have been identified
135-
*/
136-
ParsedValue parse(String value) {
137-
List<Part> parts = parse(value, false);
138-
return new ParsedValue(value, parts);
139-
}
140-
141-
private List<Part> parse(String value, boolean inPlaceholder) {
142-
LinkedList<Part> parts = new LinkedList<>();
134+
private @Nullable List<Part> parse(String value, boolean inPlaceholder) {
143135
int startIndex = nextStartPrefix(value, 0);
144136
if (startIndex == -1) {
145-
Part part = (inPlaceholder ? createSimplePlaceholderPart(value) : new TextPart(value));
146-
parts.add(part);
147-
return parts;
137+
return null;
148138
}
139+
List<Part> parts = new ArrayList<>(4);
149140
int position = 0;
150141
while (startIndex != -1) {
151142
int endIndex = nextValidEndPrefix(value, startIndex);
152-
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
143+
if (endIndex == -1) { // Not a valid placeholder, consume the prefix and continue
153144
addText(value, position, startIndex + this.prefix.length(), parts);
154145
position = startIndex + this.prefix.length();
155146
startIndex = nextStartPrefix(value, position);
156147
}
157-
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
148+
else if (isEscaped(value, startIndex)) { // Not a valid index, accumulate and skip the escape character
158149
addText(value, position, startIndex - 1, parts);
159150
addText(value, startIndex, startIndex + this.prefix.length(), parts);
160151
position = startIndex + this.prefix.length();
161152
startIndex = nextStartPrefix(value, position);
162153
}
163-
else { // Found valid placeholder, recursive parsing
154+
else { // Found valid placeholder, recursive parsing
164155
addText(value, position, startIndex, parts);
165156
String placeholder = value.substring(startIndex + this.prefix.length(), endIndex);
166157
List<Part> placeholderParts = parse(placeholder, true);
167-
parts.addAll(placeholderParts);
158+
if (placeholderParts == null) {
159+
parts.add(createSimplePlaceholderPart(placeholder));
160+
}
161+
else {
162+
parts.addAll(placeholderParts);
163+
}
168164
startIndex = nextStartPrefix(value, endIndex + this.suffix.length());
169165
position = endIndex + this.suffix.length();
170166
}
@@ -241,29 +237,6 @@ private ParsedSection parseSection(String value) {
241237
return new ParsedSection(buffer.toString(), null);
242238
}
243239

244-
private static void addText(String value, int start, int end, LinkedList<Part> parts) {
245-
if (start > end) {
246-
return;
247-
}
248-
String text = value.substring(start, end);
249-
if (!text.isEmpty()) {
250-
if (!parts.isEmpty()) {
251-
Part current = parts.removeLast();
252-
if (current instanceof TextPart textPart) {
253-
parts.add(new TextPart(textPart.text() + text));
254-
}
255-
else {
256-
parts.add(current);
257-
parts.add(new TextPart(text));
258-
}
259-
}
260-
else {
261-
parts.add(new TextPart(text));
262-
}
263-
}
264-
}
265-
266-
267240
private int nextStartPrefix(String value, int index) {
268241
return value.indexOf(this.prefix, index);
269242
}
@@ -296,15 +269,46 @@ private boolean isEscaped(String value, int index) {
296269
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
297270
}
298271

299-
record ParsedSection(String key, @Nullable String fallback) {
272+
private static void addText(String value, int start, int end, List<Part> parts) {
273+
if (start >= end) {
274+
return;
275+
}
276+
String text = value.substring(start, end);
277+
if (!parts.isEmpty() && parts.get(parts.size() - 1) instanceof TextPart textPart) {
278+
parts.set(parts.size() - 1, new TextPart(textPart.text() + text));
279+
}
280+
else {
281+
parts.add(new TextPart(text));
282+
}
283+
}
284+
285+
286+
/**
287+
* A representation of the parsing of an input string.
288+
* @param text the raw input string
289+
* @param parts the parts that appear in the string, in order
290+
*/
291+
private record ParsedValue(String text, List<Part> parts) {
300292

293+
public String resolve(PartResolutionContext resolutionContext) {
294+
try {
295+
return Part.resolveAll(this.parts, resolutionContext);
296+
}
297+
catch (PlaceholderResolutionException ex) {
298+
throw ex.withValue(this.text);
299+
}
300+
}
301+
}
302+
303+
304+
private record ParsedSection(String key, @Nullable String fallback) {
301305
}
302306

303307

304308
/**
305309
* Provide the necessary context to handle and resolve underlying placeholders.
306310
*/
307-
static class PartResolutionContext implements PlaceholderResolver {
311+
private static class PartResolutionContext implements PlaceholderResolver {
308312

309313
private final String prefix;
310314

@@ -319,7 +323,6 @@ static class PartResolutionContext implements PlaceholderResolver {
319323
@Nullable
320324
private Set<String> visitedPlaceholders;
321325

322-
323326
PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix,
324327
boolean ignoreUnresolvablePlaceholders, Function<String, List<Part>> parser) {
325328
this.prefix = prefix;
@@ -352,7 +355,7 @@ private String toPlaceholderText(String text) {
352355
return this.prefix + text + this.suffix;
353356
}
354357

355-
public List<Part> parse(String text) {
358+
public @Nullable List<Part> parse(String text) {
356359
return this.parser.apply(text);
357360
}
358361

@@ -367,17 +370,17 @@ public void flagPlaceholderAsVisited(String placeholder) {
367370
}
368371

369372
public void removePlaceholder(String placeholder) {
370-
Assert.state(this.visitedPlaceholders != null, "Visited placeholders must not be null");
371-
this.visitedPlaceholders.remove(placeholder);
373+
if (this.visitedPlaceholders != null) {
374+
this.visitedPlaceholders.remove(placeholder);
375+
}
372376
}
373-
374377
}
375378

376379

377380
/**
378381
* A part is a section of a String containing placeholders to replace.
379382
*/
380-
interface Part {
383+
private interface Part {
381384

382385
/**
383386
* Resolve this part using the specified {@link PartResolutionContext}.
@@ -408,30 +411,12 @@ static String resolveAll(Iterable<Part> parts, PartResolutionContext resolutionC
408411
}
409412

410413

411-
/**
412-
* A representation of the parsing of an input string.
413-
* @param text the raw input string
414-
* @param parts the parts that appear in the string, in order
415-
*/
416-
record ParsedValue(String text, List<Part> parts) {
417-
418-
public String resolve(PartResolutionContext resolutionContext) {
419-
try {
420-
return Part.resolveAll(this.parts, resolutionContext);
421-
}
422-
catch (PlaceholderResolutionException ex) {
423-
throw ex.withValue(this.text);
424-
}
425-
}
426-
}
427-
428-
429414
/**
430415
* A base {@link Part} implementation.
431416
*/
432-
abstract static class AbstractPart implements Part {
417+
private abstract static class AbstractPart implements Part {
433418

434-
private final String text;
419+
final String text;
435420

436421
protected AbstractPart(String text) {
437422
this.text = text;
@@ -454,37 +439,27 @@ public String text() {
454439
@Nullable
455440
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
456441
String resolvedValue = resolutionContext.resolvePlaceholder(key);
457-
if (resolvedValue != null) {
458-
resolutionContext.flagPlaceholderAsVisited(key);
459-
// Let's check if we need to recursively resolve that value
460-
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
461-
String value = toText(nestedParts);
462-
if (!isTextOnly(nestedParts)) {
463-
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
464-
}
465-
resolutionContext.removePlaceholder(key);
466-
return value;
442+
if (resolvedValue == null) {
443+
// Not found
444+
return null;
467445
}
468-
// Not found
469-
return null;
470-
}
471-
472-
private boolean isTextOnly(List<Part> parts) {
473-
return parts.stream().allMatch(TextPart.class::isInstance);
474-
}
475-
476-
private String toText(List<Part> parts) {
477-
StringBuilder sb = new StringBuilder();
478-
parts.forEach(part -> sb.append(part.text()));
479-
return sb.toString();
446+
// Let's check if we need to recursively resolve that value
447+
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
448+
if (nestedParts == null) {
449+
return resolvedValue;
450+
}
451+
resolutionContext.flagPlaceholderAsVisited(key);
452+
String value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
453+
resolutionContext.removePlaceholder(key);
454+
return value;
480455
}
481456
}
482457

483458

484459
/**
485460
* A {@link Part} implementation that does not contain a valid placeholder.
486461
*/
487-
static class TextPart extends AbstractPart {
462+
private static class TextPart extends AbstractPart {
488463

489464
/**
490465
* Create a new instance.
@@ -496,7 +471,7 @@ public TextPart(String text) {
496471

497472
@Override
498473
public String resolve(PartResolutionContext resolutionContext) {
499-
return text();
474+
return this.text;
500475
}
501476
}
502477

@@ -505,7 +480,7 @@ public String resolve(PartResolutionContext resolutionContext) {
505480
* A {@link Part} implementation that represents a single placeholder with
506481
* a hard-coded fallback.
507482
*/
508-
static class SimplePlaceholderPart extends AbstractPart {
483+
private static class SimplePlaceholderPart extends AbstractPart {
509484

510485
private final String key;
511486

@@ -533,13 +508,13 @@ public String resolve(PartResolutionContext resolutionContext) {
533508
else if (this.fallback != null) {
534509
return this.fallback;
535510
}
536-
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
511+
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
537512
}
538513

539514
@Nullable
540515
private String resolveRecursively(PartResolutionContext resolutionContext) {
541-
if (!this.text().equals(this.key)) {
542-
String value = resolveRecursively(resolutionContext, this.text());
516+
if (!this.text.equals(this.key)) {
517+
String value = resolveRecursively(resolutionContext, this.text);
543518
if (value != null) {
544519
return value;
545520
}
@@ -553,7 +528,7 @@ private String resolveRecursively(PartResolutionContext resolutionContext) {
553528
* A {@link Part} implementation that represents a single placeholder
554529
* containing nested placeholders.
555530
*/
556-
static class NestedPlaceholderPart extends AbstractPart {
531+
private static class NestedPlaceholderPart extends AbstractPart {
557532

558533
private final List<Part> keyParts;
559534

@@ -582,7 +557,7 @@ public String resolve(PartResolutionContext resolutionContext) {
582557
else if (this.defaultParts != null) {
583558
return Part.resolveAll(this.defaultParts, resolutionContext);
584559
}
585-
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
560+
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
586561
}
587562
}
588563

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf
9797
* @param properties the {@code Properties} to use for replacement
9898
* @return the supplied value with placeholders replaced inline
9999
*/
100-
public String replacePlaceholders(String value, final Properties properties) {
100+
public String replacePlaceholders(String value, Properties properties) {
101101
Assert.notNull(properties, "'properties' must not be null");
102102
return replacePlaceholders(value, properties::getProperty);
103103
}
@@ -111,9 +111,10 @@ public String replacePlaceholders(String value, final Properties properties) {
111111
*/
112112
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
113113
Assert.notNull(value, "'value' must not be null");
114-
return parseStringValue(value, placeholderResolver);
114+
return this.parser.replacePlaceholders(value, placeholderResolver);
115115
}
116116

117+
@Deprecated(since = "6.2.12", forRemoval = true)
117118
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver) {
118119
return this.parser.replacePlaceholders(value, placeholderResolver);
119120
}

0 commit comments

Comments
 (0)