Further improve pattern caching in SpEL

See gh-36755
This commit is contained in:
Sam Brannen
2026-04-17 17:11:01 +02:00
committed by Brian Clozel
parent 12b44f2545
commit 7baa86536f
2 changed files with 41 additions and 22 deletions
@@ -16,7 +16,6 @@
package org.springframework.expression.spel.ast;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -27,6 +26,7 @@ import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.BooleanTypedValue;
import org.springframework.util.ConcurrentLruCache;
/**
* Implements the matches operator. Matches takes two operands:
@@ -41,6 +41,12 @@ import org.springframework.expression.spel.support.BooleanTypedValue;
*/
public class OperatorMatches extends Operator {
/**
* Maximum number of compiled regular expressions in the pattern cache: {@value}.
* @since 6.2.19
*/
public static final int MAX_PATTERN_CACHE_SIZE = 256;
private static final int PATTERN_ACCESS_THRESHOLD = 1000000;
/**
@@ -49,29 +55,47 @@ public class OperatorMatches extends Operator {
*/
private static final int MAX_REGEX_LENGTH = 1000;
private final ConcurrentMap<String, Pattern> patternCache;
private final ConcurrentLruCache<String, Pattern> patternCache;
/**
* Create a new {@link OperatorMatches} instance.
* @deprecated as of Spring Framework 5.2.23 in favor of invoking
* {@link #OperatorMatches(ConcurrentMap, int, int, SpelNodeImpl...)}
* with a shared pattern cache instead
* @deprecated as of Spring Framework 5.2.23; for removal in Spring Framework 7.1; invoke
* {@link #OperatorMatches(ConcurrentLruCache, int, int, SpelNodeImpl...)} instead
*/
@Deprecated(since = "5.2.23")
@Deprecated(since = "5.2.23", forRemoval = true)
public OperatorMatches(int startPos, int endPos, SpelNodeImpl... operands) {
this(new ConcurrentHashMap<>(), startPos, endPos, operands);
this(new ConcurrentLruCache<>(MAX_PATTERN_CACHE_SIZE, Pattern::compile), startPos, endPos, operands);
}
/**
* Create a new {@link OperatorMatches} instance with a shared pattern cache.
* <p>As of Spring Framework 6.2.19, the supplied {@code patternCacheMap} will
* be ignored.
* @since 5.2.23
* @deprecated as of Spring Framework 6.2.19; for removal in Spring Framework 7.1; invoke
* {@link #OperatorMatches(ConcurrentLruCache, int, int, SpelNodeImpl...)} instead
*/
public OperatorMatches(ConcurrentMap<String, Pattern> patternCache, int startPos, int endPos, SpelNodeImpl... operands) {
@Deprecated(since = "6.2.19", forRemoval = true)
public OperatorMatches(ConcurrentMap<String, Pattern> patternCacheMap,
int startPos, int endPos, SpelNodeImpl... operands) {
this(startPos, endPos, operands);
}
/**
* Create a new {@link OperatorMatches} instance with a shared pattern cache.
* @since 6.2.19
*/
public OperatorMatches(ConcurrentLruCache<String, Pattern> patternCache,
int startPos, int endPos, SpelNodeImpl... operands) {
super("matches", startPos, endPos, operands);
this.patternCache = patternCache;
}
/**
* Check the first operand matches the regex specified as the second operand.
* @param state the expression state
@@ -96,12 +120,13 @@ public class OperatorMatches extends Operator {
throw new SpelEvaluationException(rightOp.getStartPosition(),
SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, right);
}
if (regex.length() > MAX_REGEX_LENGTH) {
throw new SpelEvaluationException(rightOp.getStartPosition(),
SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH);
}
try {
Pattern pattern = this.patternCache.computeIfAbsent(regex, key -> {
checkRegexLength(key);
return Pattern.compile(key);
});
Pattern pattern = this.patternCache.get(regex);
Matcher matcher = pattern.matcher(new MatcherInput(input, new AccessCount()));
return BooleanTypedValue.forValue(matcher.matches());
}
@@ -115,13 +140,6 @@ public class OperatorMatches extends Operator {
}
}
private void checkRegexLength(String regex) {
if (regex.length() > MAX_REGEX_LENGTH) {
throw new SpelEvaluationException(getRightOperand().getStartPosition(),
SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH);
}
}
private static class AccessCount {
@@ -22,8 +22,6 @@ import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
@@ -80,6 +78,7 @@ import org.springframework.expression.spel.ast.Ternary;
import org.springframework.expression.spel.ast.TypeReference;
import org.springframework.expression.spel.ast.VariableReference;
import org.springframework.lang.Contract;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.StringUtils;
/**
@@ -101,7 +100,9 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
private final Deque<SpelNodeImpl> constructedNodes = new ArrayDeque<>();
// Shared cache for compiled regex patterns
private final ConcurrentMap<String, Pattern> patternCache = new ConcurrentHashMap<>();
private final ConcurrentLruCache<String, Pattern> patternCache =
new ConcurrentLruCache<>(OperatorMatches.MAX_PATTERN_CACHE_SIZE, Pattern::compile);
// The expression being parsed
private String expressionString = "";