diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java index 21e8b998e13..9ee6469eb2b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java @@ -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 patternCache; + + private final ConcurrentLruCache 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. + *

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 patternCache, int startPos, int endPos, SpelNodeImpl... operands) { + @Deprecated(since = "6.2.19", forRemoval = true) + public OperatorMatches(ConcurrentMap 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 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 { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index dc95f9fb956..c94bea7277e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -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 constructedNodes = new ArrayDeque<>(); // Shared cache for compiled regex patterns - private final ConcurrentMap patternCache = new ConcurrentHashMap<>(); + private final ConcurrentLruCache patternCache = + new ConcurrentLruCache<>(OperatorMatches.MAX_PATTERN_CACHE_SIZE, Pattern::compile); + // The expression being parsed private String expressionString = "";