From de9bedcb131ef43df93ce1848a690be3c46ad4ae Mon Sep 17 00:00:00 2001 From: Dominique Villard Date: Wed, 2 Jul 2025 17:43:10 +0200 Subject: [PATCH] Reuse parsed expression instead of reparsing it on every invocation. Signed-off-by: Dominique Villard --- .../SpelValueExpressionResolver.java | 16 ++++++++++++---- .../SpelValueExpressionResolverTests.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolver.java b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolver.java index b62c2e7d556e..e54948172633 100644 --- a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolver.java +++ b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolver.java @@ -16,10 +16,12 @@ package org.springframework.boot.observation.autoconfigure; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import io.micrometer.common.annotation.ValueExpressionResolver; import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; @@ -30,17 +32,23 @@ */ class SpelValueExpressionResolver implements ValueExpressionResolver { + private final Map expressionMap = new ConcurrentHashMap<>(); + @Override public String resolve(String expression, Object parameter) { try { SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); - ExpressionParser expressionParser = new SpelExpressionParser(); - Expression expressionToEvaluate = expressionParser.parseExpression(expression); - return expressionToEvaluate.getValue(context, parameter, String.class); + Expression parsedExpression = this.expressionMap.computeIfAbsent(expression, + SpelValueExpressionResolver::parseExpression); + return parsedExpression.getValue(context, parameter, String.class); } catch (Exception ex) { throw new IllegalStateException("Unable to evaluate SpEL expression '%s'".formatted(expression), ex); } } + private static Expression parseExpression(String expression) { + return new SpelExpressionParser().parseExpression(expression); + } + } diff --git a/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolverTests.java b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolverTests.java index 1492a0dfec6c..b5b61163b4e5 100644 --- a/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolverTests.java +++ b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/SpelValueExpressionResolverTests.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -44,6 +46,19 @@ void checkInvalidExpression() { assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve("['bar'].first", value)); } + @Test + void checkParserReuse() { + var map = (Map) ReflectionTestUtils.getField(this.resolver, "expressionMap"); + + this.resolver.resolve("length", "foo"); + this.resolver.resolve("length", "bar"); + + assertThat(map).hasSize(1); + + this.resolver.resolve("isEmpty", "foo"); + assertThat(map).hasSize(2); + } + record Pair(int first, int second) { static Pair of(int first, int second) {