Merge branch '7.0.x'

This commit is contained in:
Sam Brannen
2026-06-08 18:29:20 +02:00
64 changed files with 1187 additions and 314 deletions
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.core.SpringProperties;
import org.springframework.expression.AccessException;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.EvaluationContext;
@@ -47,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.within;
import static org.springframework.expression.spel.SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST;
import static org.springframework.expression.spel.SpelParserConfiguration.SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME;
/**
* Tests the evaluation of real expressions in a real context.
@@ -97,6 +99,96 @@ class EvaluationTests extends AbstractExpressionTests {
evaluateAndCheckError(parser, expression, String.class, SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED);
}
@Test
void maxOperationsIsConfigurableViaSpelParserConfiguration() {
int maxLength = 100;
int maxOperations = 10;
ExpressionParser parser = new SpelExpressionParser();
// The following expression contains 10 tracked operations:
String expression = "('foo' + 'bar').length >= 6 && {1, 1 + 1, 3}.contains(3)";
Expression expr1 = parser.parseExpression(expression);
assertThat(expr1.getValue(Boolean.class)).isTrue();
SpelParserConfiguration configuration =
new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false, 0, maxLength, maxOperations);
parser = new SpelExpressionParser(configuration);
Expression expr2 = parser.parseExpression(expression);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expr2.getValue(Boolean.class))
.satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_OPERATIONS_EXCEEDED);
assertThat(ex.getInserts()).as("inserts").containsExactly(maxOperations);
});
}
@Test
void maxOperationsIsConfigurableViaSpringProperty() {
int maxOperations = 10;
ExpressionParser parser = new SpelExpressionParser();
// The following expression contains 10 tracked operations:
String expression = "('foo' + 'bar').length >= 6 && {1, 1 + 1, 3}.contains(3)";
Expression expr1 = parser.parseExpression(expression);
assertThat(expr1.getValue(Boolean.class)).isTrue();
try {
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, "" + maxOperations);
parser = new SpelExpressionParser();
Expression expr2 = parser.parseExpression(expression);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expr2.getValue(Boolean.class))
.satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_OPERATIONS_EXCEEDED);
assertThat(ex.getInserts()).as("inserts").containsExactly(maxOperations);
});
}
finally {
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, null);
}
}
@Test
void maxOperationsConfiguredViaSpelParserConfigurationOverridesSpringProperty() {
int maxLength = 100;
int maxOperations = 10;
ExpressionParser parser = new SpelExpressionParser();
// The following expression contains 10 tracked operations:
String expression = "('foo' + 'bar').length >= 6 && {1, 1 + 1, 3}.contains(3)";
Expression expr1 = parser.parseExpression(expression);
assertThat(expr1.getValue(Boolean.class)).isTrue();
try {
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, "" + maxOperations / 2);
// maxOperations + 1 should override maxOperations / 2
SpelParserConfiguration configuration =
new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false, 0, maxLength, maxOperations + 1);
parser = new SpelExpressionParser(configuration);
expr1 = parser.parseExpression(expression);
assertThat(expr1.getValue(Boolean.class)).isTrue();
configuration = new SpelParserConfiguration(SpelCompilerMode.OFF, null, false, false, 0, maxLength, maxOperations);
parser = new SpelExpressionParser(configuration);
Expression expr2 = parser.parseExpression(expression);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expr2.getValue(Boolean.class))
.satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.MAX_OPERATIONS_EXCEEDED);
// Should fail due to maxOperations, not maxOperations / 2
assertThat(ex.getInserts()).as("inserts").containsExactly(maxOperations);
});
}
finally {
SpringProperties.setProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME, null);
}
}
@Test
void createListsOnAttemptToIndexNull01() throws EvaluationException, ParseException {
ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
@@ -574,21 +574,26 @@ class OperatorTests extends AbstractExpressionTests {
@Test
void stringRepeat() {
evaluate("'abc' * 0", "", String.class);
evaluate("'abc' * 1", "abc", String.class);
evaluate("'abc' * 2", "abcabc", String.class);
String EMPTY = "";
evaluate("'' * 0", EMPTY, String.class);
evaluate("'' * 2", EMPTY, String.class);
evaluate("'abc' * 0", EMPTY, String.class);
evaluate("'Abc' * 1", "Abc", String.class);
evaluate("'Abc' * 2", "AbcAbc", String.class);
evaluate("'Abc' * 3", "AbcAbcAbc", String.class);
Expression expr = parser.parseExpression("'a' * 256");
assertThat(expr.getValue(context, String.class)).hasSize(256);
// 4 is the position of the '*' (repeat operator)
evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 4);
// 6 is the position of the repeatCount
evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 6);
// Integer overflow: 2 * ((Integer.MAX_VALUE / 2) + 1) --> integer overflow
int repeatCount = (Integer.MAX_VALUE / 2) + 1;
assertThat(2 * repeatCount).isNegative();
// 5 is the position of the '*' (repeat operator)
evaluateAndCheckError("'ab' * " + repeatCount, String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 5);
// 7 is the position of the repeatCount
evaluateAndCheckError("'ab' * " + repeatCount, String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 7);
}
@Test
@@ -623,12 +628,19 @@ class OperatorTests extends AbstractExpressionTests {
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
// Text is too big
context.setVariable("text1", createString(maxSize));
context.setVariable("text2", createString(maxSize));
evaluateAndCheckError("#text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
context.setVariable("text1", createString(maxSize + 1));
evaluateAndCheckError("#text1 + ''", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("'' + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 3);
evaluateAndCheckError("true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 5);
context.setVariable("text1", createString(maxSize - 1));
evaluateAndCheckError("#text1 + 'YZ'", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
context.setVariable("text1", createString(maxSize / 2));
context.setVariable("text2", createString((maxSize / 2) + 1));
evaluateAndCheckError("#text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
@@ -16,6 +16,7 @@
package org.springframework.expression.spel;
import java.lang.reflect.Method;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
@@ -39,6 +40,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.testresources.Inventor;
import org.springframework.expression.spel.testresources.Person;
import org.springframework.expression.spel.testresources.RecordPerson;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -202,6 +204,16 @@ class PropertyAccessTests extends AbstractExpressionTests {
target.setName("p2");
assertThat(expr.getValue(context, target)).isEqualTo("p2");
assertThatSpelEvaluationException()
.isThrownBy(() -> parser.parseExpression("nonexistent").getValue(context, target))
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE);
Method getInvalid = ClassUtils.getMethod(Person.class, "getInvalid");
assertThat(getInvalid.getReturnType()).isEqualTo(void.class);
assertThatSpelEvaluationException()
.isThrownBy(() -> parser.parseExpression("invalid").getValue(context, target))
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE);
assertThatSpelEvaluationException()
.isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target))
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE);
@@ -30,6 +30,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link InlineList} and {@link InlineMap}.
@@ -47,17 +48,35 @@ class InlineCollectionTests {
class InlineListTests {
@Test
void listIsCached() {
InlineList list = parseList("{1, -2, 3, 4}");
assertThat(list.isConstant()).isTrue();
assertThat(list.getConstantValue()).isEqualTo(List.of(1, -2, 3, 4));
@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
void getConstantValue() {
InlineList inlineList = parseList("{1, -2, 3, 4}");
assertThat(inlineList.isConstant()).isTrue();
List list1 = inlineList.getConstantValue();
List list2 = inlineList.getValue(expressionState(), List.class);
assertThat(list1).containsExactly(1, -2, 3, 4).isSameAs(list2);
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void constantListIsCached() {
ExpressionState expressionState = expressionState();
InlineList inlineList = parseList("{1, -2, 3, 4}");
assertThat(inlineList.isConstant()).isTrue();
List list1 = inlineList.getValue(expressionState, List.class);
List list2 = inlineList.getValue(expressionState, List.class);
assertThat(list1).containsExactly(1, -2, 3, 4).isSameAs(list2);
// Constant inline lists are immutable.
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> list1.add(999));
}
@Test
void dynamicListIsNotCached() {
InlineList list = parseList("{1, (5 - 3), 3, 4}");
assertThat(list.isConstant()).isFalse();
assertThat(list.getValue(null)).isEqualTo(List.of(1, 2, 3, 4));
assertThat(list.getValue(expressionState())).isEqualTo(List.of(1, 2, 3, 4));
}
@Test
@@ -110,11 +129,28 @@ class InlineCollectionTests {
class InlineMapTests {
@Test
void mapIsCached() {
InlineMap map = parseMap("{1 : 2, 3 : 4}");
assertThat(map.isConstant()).isTrue();
Map<Integer, Integer> expected = Map.of(1, 2, 3, 4);
assertThat(map.getValue(null)).isEqualTo(expected);
@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
void getConstantValue() {
InlineMap inlineMap = parseMap("{1 : 2, 3 : 4}");
assertThat(inlineMap.isConstant()).isTrue();
Map map1 = inlineMap.getConstantValue();
Map map2 = inlineMap.getValue(expressionState(), Map.class);
assertThat(map1).isEqualTo(Map.of(1, 2, 3, 4)).isSameAs(map2);
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void constantMapIsCached() {
ExpressionState expressionState = expressionState();
InlineMap inlineMap = parseMap("{1 : 2, 3 : 4}");
assertThat(inlineMap.isConstant()).isTrue();
Map map1 = inlineMap.getValue(expressionState, Map.class);
Map map2 = inlineMap.getValue(expressionState, Map.class);
assertThat(map1).isEqualTo(Map.of(1, 2, 3, 4)).isSameAs(map2);
// Constant inline maps are immutable.
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> map1.put(99, "X"));
}
@Test
@@ -122,7 +158,7 @@ class InlineCollectionTests {
InlineMap map = parseMap("{-1 : 2, (-2 - 1) : -4}");
assertThat(map.isConstant()).isFalse();
Map<Integer, Integer> expected = Map.of(-1, 2, -3, -4);
assertThat(map.getValue(null)).isEqualTo(expected);
assertThat(map.getValue(expressionState())).isEqualTo(expected);
}
@Test
@@ -155,7 +191,7 @@ class InlineCollectionTests {
InlineMap map = parseMap("{-1 : 2, -3 : 4}");
assertThat(map.isConstant()).isTrue();
Map<Integer, Integer> expected = Map.of(-1, 2, -3, 4);
assertThat(map.getValue(null)).isEqualTo(expected);
assertThat(map.getValue(expressionState())).isEqualTo(expected);
}
@Test
@@ -163,7 +199,7 @@ class InlineCollectionTests {
InlineMap map = parseMap("{1 : -2, 3 : -4}");
assertThat(map.isConstant()).isTrue();
Map<Integer, Integer> expected = Map.of(1, -2, 3, -4);
assertThat(map.getValue(null)).isEqualTo(expected);
assertThat(map.getValue(expressionState())).isEqualTo(expected);
}
@Test
@@ -171,7 +207,7 @@ class InlineCollectionTests {
InlineMap map = parseMap("{1L : -2L, 3L : -4L}");
assertThat(map.isConstant()).isTrue();
Map<Long, Long> expected = Map.of(1L, -2L, 3L, -4L);
assertThat(map.getValue(null)).isEqualTo(expected);
assertThat(map.getValue(expressionState())).isEqualTo(expected);
}
@Test
@@ -179,7 +215,7 @@ class InlineCollectionTests {
InlineMap map = parseMap("{-1.0f : -2.0f, -3.0f : -4.0f}");
assertThat(map.isConstant()).isTrue();
Map<Float, Float> expected = Map.of(-1.0f, -2.0f, -3.0f, -4.0f);
assertThat(map.getValue(null)).isEqualTo(expected);
assertThat(map.getValue(expressionState())).isEqualTo(expected);
}
@Test
@@ -187,7 +223,7 @@ class InlineCollectionTests {
InlineMap map = parseMap("{-1.0 : -2.0, -3.0 : -4.0}");
assertThat(map.isConstant()).isTrue();
Map<Double, Double> expected = Map.of(-1.0, -2.0, -3.0, -4.0);
assertThat(map.getValue(null)).isEqualTo(expected);
assertThat(map.getValue(expressionState())).isEqualTo(expected);
}
@Test
@@ -195,7 +231,7 @@ class InlineCollectionTests {
InlineMap map = parseMap("{-1 : -2, -3 : -4}");
assertThat(map.isConstant()).isTrue();
Map<Integer, Integer> expected = Map.of(-1, -2, -3, -4);
assertThat(map.getValue(null)).isEqualTo(expected);
assertThat(map.getValue(expressionState())).isEqualTo(expected);
}
private InlineMap parseMap(String s) {
@@ -210,6 +246,10 @@ class InlineCollectionTests {
return (SpelExpression) parser.parseExpression(s);
}
private static ExpressionState expressionState() {
return new ExpressionState(new StandardEvaluationContext());
}
private static class NumberHolder {
@SuppressWarnings("unused")
@@ -59,4 +59,8 @@ public class Person {
return company;
}
public void getInvalid() {
// no-op
}
}