Track operations during SpEL expression evaluation

This commit introduces support for tracking operations during SpEL
expression evaluation. If the maximum number of operations is exceeded,
a SpelEvaluationException is thrown.

The limit can be configured either on a per-use-case basis via
SpelParserConfiguration supplied to the SpelExpressionParser or
globally as a JVM system property or Spring property named
`spring.expression.maxOperations`.

Closes gh-36801
This commit is contained in:
Sam Brannen
2026-04-21 17:45:47 +02:00
committed by Brian Clozel
parent 3d47da9746
commit 76a36dfd70
42 changed files with 617 additions and 211 deletions
@@ -74,6 +74,11 @@ expressions used in XML bean definitions, `@Value`, etc.
| The mode to use when compiling expressions for the
xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language].
| `spring.expression.maxOperations`
| The default maximum number of operations permitted during
xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language]
expression evaluation.
| `spring.getenv.ignore`
| Instructs Spring to ignore operating system environment variables if a Spring
`Environment` property -- for example, a placeholder in a configuration String -- isn't
@@ -395,6 +395,16 @@ set a JVM system property or Spring property named `spring.context.expression.ma
to the maximum expression length needed by your application (see
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
Similarly, the number of operations performed during the evaluation of a SpEL expression
cannot exceed 10,000 by default; however, the `maxOperations` value is configurable. If
you create a `SpelExpressionParser` programmatically (the recommend approach), you can
specify a custom `maxOperations` value when creating the `SpelParserConfiguration` that
you provide to the `SpelExpressionParser`. If you are not able to configure an explicit
value for `maxOperations` via `SpelParserConfiguration`, you can set a JVM system
property or Spring property named `spring.expression.maxOperations` to the maximum number
of operations required by your application (see
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
[[expressions-spel-compilation]]
== SpEL Compilation
@@ -44,6 +44,7 @@ import org.springframework.lang.Nullable;
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
* @see org.springframework.jndi.JndiLocatorDelegate#IGNORE_JNDI_PROPERTY_NAME
* @see org.springframework.objenesis.SpringObjenesis#IGNORE_OBJENESIS_PROPERTY_NAME
@@ -67,6 +67,8 @@ public class ExpressionState {
@Nullable
private Deque<VariableScope> variableScopes;
private int operationsCount;
// When entering a new scope there is a new base object which should be used
// for '#this' references (or to act as a target for unqualified references).
// This ArrayDeque captures those objects at each nested scope level.
@@ -146,6 +148,7 @@ public class ExpressionState {
* @see EvaluationContext#assignVariable(String, Supplier)
*/
public TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
trackOperation();
return this.relatedContext.assignVariable(name, valueSupplier);
}
@@ -170,6 +173,7 @@ public class ExpressionState {
* @see #setVariable(String, Object)
*/
public TypedValue lookupVariable(String name) {
trackOperation();
Object value = this.relatedContext.lookupVariable(name);
return (value != null ? new TypedValue(value) : TypedValue.NULL);
}
@@ -301,6 +305,7 @@ public class ExpressionState {
public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException {
OperatorOverloader overloader = this.relatedContext.getOperatorOverloader();
if (overloader.overridesOperation(op, left, right)) {
trackOperation();
Object returnValue = overloader.operate(op, left, right);
return new TypedValue(returnValue);
}
@@ -323,6 +328,18 @@ public class ExpressionState {
return this.configuration;
}
/**
* Track an operation during expression evaluation.
* @since 6.2.19
* @see SpelParserConfiguration#getMaximumOperations()
*/
public void trackOperation() {
int maxOperations = this.configuration.getMaximumOperations();
if (++this.operationsCount >= maxOperations) {
throw new SpelEvaluationException(SpelMessage.MAX_OPERATIONS_EXCEEDED, maxOperations);
}
}
/**
* A new local variable scope is entered when a new expression scope is
@@ -303,8 +303,11 @@ public enum SpelMessage {
/** @since 6.2 */
EXCEPTION_DURING_INDEX_WRITE(Kind.ERROR, 1084,
"A problem occurred while attempting to write index ''{0}'' in ''{1}''");
"A problem occurred while attempting to write index ''{0}'' in ''{1}''"),
/** @since 6.2.19 */
MAX_OPERATIONS_EXCEEDED(Kind.ERROR, 1085,
"SpEL expression evaluation exceeded the threshold of ''{0}'' operations");
private final Kind kind;
@@ -20,6 +20,8 @@ import java.util.Locale;
import org.springframework.core.SpringProperties;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configuration object for the SpEL expression parser.
@@ -39,9 +41,38 @@ public class SpelParserConfiguration {
*/
public static final int DEFAULT_MAX_EXPRESSION_LENGTH = 10_000;
/** System property to configure the default compiler mode for SpEL expression parsers: {@value}. */
/**
* Default maximum number of operations permitted during SpEL expression evaluation: {@value}.
* @since 6.2.19
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public static final int DEFAULT_MAX_OPERATIONS = 10_000;
/**
* System property to configure the default compiler mode for SpEL expression parsers: {@value}.
* <p><strong>NOTE</strong>: Instead of relying on a global default, applications
* and frameworks should ideally set an explicit custom value via the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* <p>Can also be configured via the {@link SpringProperties} mechanism.
*/
public static final String SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME = "spring.expression.compiler.mode";
/**
* System property to configure the default maximum number of operations permitted
* during SpEL expression evaluation: {@value}.
* <p><strong>NOTE</strong>: Instead of relying on a global default, applications
* and frameworks should ideally set an explicit custom value via the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor which provides complete configuration control and the ability
* to override global defaults per use case.
* <p>Can also be configured via the {@link SpringProperties} mechanism.
* @since 6.2.19
* @see #DEFAULT_MAX_OPERATIONS
*/
public static final String SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME = "spring.expression.maxOperations";
private static final SpelCompilerMode defaultCompilerMode;
@@ -65,9 +96,17 @@ public class SpelParserConfiguration {
private final int maximumExpressionLength;
private final int maximumOperations;
/**
* Create a new {@code SpelParserConfiguration} instance with default settings.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration() {
this(null, null, false, false, Integer.MAX_VALUE);
@@ -75,8 +114,16 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode for the parser
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; or {@code null} to use the default mode
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader) {
this(compilerMode, compilerClassLoader, false, false, Integer.MAX_VALUE);
@@ -84,9 +131,14 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @see #SpelParserConfiguration(boolean, boolean, int)
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections) {
this(null, null, autoGrowNullReferences, autoGrowCollections, Integer.MAX_VALUE);
@@ -94,9 +146,15 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size that the collection can auto grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
this(null, null, autoGrowNullReferences, autoGrowCollections, maximumAutoGrowSize);
@@ -104,11 +162,19 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration object should use
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; or {@code null} to use the default mode
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size that the collection can auto grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) {
@@ -119,24 +185,60 @@ public class SpelParserConfiguration {
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration object should use
* @param compilerClassLoader the ClassLoader to use as the basis for expression compilation
* <p><strong>NOTE</strong>: Favor the
* {@link #SpelParserConfiguration(SpelCompilerMode, ClassLoader, boolean, boolean, int, int, int)}
* constructor for complete configuration control and the ability to override
* global defaults per use case.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; or {@code null} to use the default mode
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size that a collection can auto grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @param maximumExpressionLength the maximum length of a SpEL expression;
* must be a positive number
* @since 5.2.25
* @see #SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see #SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
*/
public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength) {
this.compilerMode = (compilerMode != null ? compilerMode : defaultCompilerMode);
this((compilerMode != null ? compilerMode : defaultCompilerMode), compilerClassLoader, autoGrowNullReferences,
autoGrowCollections, maximumAutoGrowSize, maximumExpressionLength, retrieveMaxOperations());
}
/**
* Create a new {@code SpelParserConfiguration} instance.
* @param compilerMode the compiler mode that parsers using this configuration
* should use; must not be {@code null}
* @param compilerClassLoader the {@code ClassLoader} to use as the basis for
* expression compilation; or {@code null} to use the default {@code ClassLoader}
* @param autoGrowNullReferences if null references should automatically grow
* @param autoGrowCollections if collections should automatically grow
* @param maximumAutoGrowSize the maximum size to which a collection can auto grow
* @param maximumExpressionLength the maximum length of a SpEL expression;
* must be a positive number
* @param maximumOperations the maximum number of operations permitted during
* SpEL expression evaluation; must be a positive number
* @since 6.2.19
*/
public SpelParserConfiguration(SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader,
boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize, int maximumExpressionLength,
int maximumOperations) {
Assert.notNull(compilerMode, "'compilerMode' must not be null");
Assert.isTrue(maximumExpressionLength > 0, "'maximumExpressionLength' must be a positive number");
Assert.isTrue(maximumOperations > 0, "'maximumOperations' must be a positive number");
this.compilerMode = compilerMode;
this.compilerClassLoader = compilerClassLoader;
this.autoGrowNullReferences = autoGrowNullReferences;
this.autoGrowCollections = autoGrowCollections;
this.maximumAutoGrowSize = maximumAutoGrowSize;
this.maximumExpressionLength = maximumExpressionLength;
this.maximumOperations = maximumOperations;
}
@@ -148,7 +250,7 @@ public class SpelParserConfiguration {
}
/**
* Return the ClassLoader to use as the basis for expression compilation.
* Return the {@code ClassLoader} to use as the basis for expression compilation.
*/
@Nullable
public ClassLoader getCompilerClassLoader() {
@@ -170,7 +272,7 @@ public class SpelParserConfiguration {
}
/**
* Return the maximum size that a collection can auto grow.
* Return the maximum size to which a collection can auto grow.
*/
public int getMaximumAutoGrowSize() {
return this.maximumAutoGrowSize;
@@ -184,4 +286,32 @@ public class SpelParserConfiguration {
return this.maximumExpressionLength;
}
/**
* Return the maximum number of operations permitted during SpEL expression
* evaluation.
* @since 6.2.19
*/
public int getMaximumOperations() {
return this.maximumOperations;
}
private static int retrieveMaxOperations() {
String value = SpringProperties.getProperty(SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME);
if (!StringUtils.hasText(value)) {
return DEFAULT_MAX_OPERATIONS;
}
try {
int maxOperations = Integer.parseInt(value.trim());
Assert.isTrue(maxOperations > 0, () -> "Value [" + maxOperations + "] for system property [" +
SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME + "] must be positive");
return maxOperations;
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException("Failed to parse value for system property [" +
SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME + "]: " + ex.getMessage(), ex);
}
}
}
@@ -44,6 +44,7 @@ public class Assign extends SpelNodeImpl {
if (!state.getEvaluationContext().isAssignmentEnabled()) {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST());
}
state.trackOperation();
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
}
@@ -65,6 +65,7 @@ public class BeanReference extends SpelNodeImpl {
getStartPosition(), SpelMessage.NO_BEAN_RESOLVER_REGISTERED, this.beanName);
}
state.trackOperation();
try {
return new TypedValue(beanResolver.resolve(state.getEvaluationContext(), this.beanName));
}
@@ -110,6 +110,7 @@ public class ConstructorReference extends SpelNodeImpl {
*/
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
if (this.isArrayConstructor) {
return createArray(state);
}
@@ -51,6 +51,7 @@ public class Elvis extends SpelNodeImpl {
*/
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
TypedValue value = this.children[0].getValueInternal(state);
// If this check is changed, the generateCode method will need changing too
if (value.getValue() != null && !"".equals(value.getValue())) {
@@ -84,6 +84,7 @@ public class FunctionReference extends SpelNodeImpl {
// Note: "javaMethod" cannot be named "method" due to a bug in Checkstyle.
if (function instanceof Method javaMethod) {
try {
state.trackOperation();
return executeFunctionViaMethod(state, javaMethod);
}
catch (SpelEvaluationException ex) {
@@ -95,6 +96,7 @@ public class FunctionReference extends SpelNodeImpl {
// Function registered via a MethodHandle.
if (function instanceof MethodHandle methodHandle) {
try {
state.trackOperation();
return executeFunctionViaMethodHandle(state, methodHandle);
}
catch (SpelEvaluationException ex) {
@@ -220,14 +220,14 @@ public class Indexer extends SpelNodeImpl {
if (target.getClass().isArray()) {
int intIndex = convertIndexToInt(state, index);
this.indexedType = IndexedType.ARRAY;
return new ArrayIndexingValueRef(state.getTypeConverter(), target, intIndex, targetDescriptor);
return new ArrayIndexingValueRef(state, state.getTypeConverter(), target, intIndex, targetDescriptor);
}
// Indexing into a List
if (target instanceof List<?> list) {
int intIndex = convertIndexToInt(state, index);
this.indexedType = IndexedType.LIST;
return new CollectionIndexingValueRef(list, intIndex, targetDescriptor,
return new CollectionIndexingValueRef(state, list, intIndex, targetDescriptor,
state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
state.getConfiguration().getMaximumAutoGrowSize());
}
@@ -240,14 +240,14 @@ public class Indexer extends SpelNodeImpl {
key = state.convertValue(key, mapKeyTypeDescriptor);
}
this.indexedType = IndexedType.MAP;
return new MapIndexingValueRef(state.getTypeConverter(), map, key, targetDescriptor);
return new MapIndexingValueRef(state, state.getTypeConverter(), map, key, targetDescriptor);
}
// Indexing into a String
if (target instanceof String string) {
int intIndex = convertIndexToInt(state, index);
this.indexedType = IndexedType.STRING;
return new StringIndexingValueRef(string, intIndex, targetDescriptor);
return new StringIndexingValueRef(state, string, intIndex, targetDescriptor);
}
// Check for a custom IndexAccessor.
@@ -259,7 +259,7 @@ public class Indexer extends SpelNodeImpl {
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canRead(evalContext, target, index)) {
this.indexedType = IndexedType.CUSTOM;
return new IndexAccessorValueRef(target, index, evalContext, targetDescriptor);
return new IndexAccessorValueRef(state, target, index, evalContext, targetDescriptor);
}
}
}
@@ -274,7 +274,7 @@ public class Indexer extends SpelNodeImpl {
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canWrite(evalContext, target, index)) {
this.indexedType = IndexedType.CUSTOM;
return new IndexAccessorValueRef(target, index, evalContext, targetDescriptor);
return new IndexAccessorValueRef(state, target, index, evalContext, targetDescriptor);
}
}
}
@@ -288,7 +288,7 @@ public class Indexer extends SpelNodeImpl {
// Fallback indexing support for collections
if (target instanceof Collection<?> collection) {
int intIndex = convertIndexToInt(state, index);
return new CollectionIndexingValueRef(collection, intIndex, targetDescriptor,
return new CollectionIndexingValueRef(state, collection, intIndex, targetDescriptor,
state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
state.getConfiguration().getMaximumAutoGrowSize());
}
@@ -298,7 +298,7 @@ public class Indexer extends SpelNodeImpl {
if (valueType != null && String.class == valueType.getType()) {
this.indexedType = IndexedType.OBJECT;
return new PropertyAccessorValueRef(
target, (String) index, state.getEvaluationContext(), targetDescriptor);
state, target, (String) index, state.getEvaluationContext(), targetDescriptor);
}
throw new SpelEvaluationException(
@@ -502,6 +502,8 @@ public class Indexer extends SpelNodeImpl {
private class ArrayIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final TypeConverter typeConverter;
private final Object array;
@@ -510,7 +512,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor typeDescriptor;
ArrayIndexingValueRef(TypeConverter typeConverter, Object array, int index, TypeDescriptor typeDescriptor) {
ArrayIndexingValueRef(ExpressionState expressionState, TypeConverter typeConverter,
Object array, int index, TypeDescriptor typeDescriptor) {
this.expressionState = expressionState;
this.typeConverter = typeConverter;
this.array = array;
this.index = index;
@@ -536,6 +541,7 @@ public class Indexer extends SpelNodeImpl {
}
private Object getArrayElement(Object ctx, int idx) throws SpelEvaluationException {
this.expressionState.trackOperation();
Class<?> arrayComponentType = ctx.getClass().componentType();
if (arrayComponentType == boolean.class) {
boolean[] array = (boolean[]) ctx;
@@ -606,6 +612,7 @@ public class Indexer extends SpelNodeImpl {
private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue,
Class<?> arrayComponentType) throws EvaluationException {
this.expressionState.trackOperation();
if (arrayComponentType == boolean.class) {
boolean[] array = (boolean[]) ctx;
checkAccess(array.length, idx);
@@ -669,13 +676,14 @@ public class Indexer extends SpelNodeImpl {
}
return result;
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
private class MapIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final TypeConverter typeConverter;
private final Map map;
@@ -685,9 +693,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor mapEntryDescriptor;
public MapIndexingValueRef(
TypeConverter typeConverter, Map map, @Nullable Object key, TypeDescriptor mapEntryDescriptor) {
public MapIndexingValueRef(ExpressionState expressionState, TypeConverter typeConverter,
Map map, @Nullable Object key, TypeDescriptor mapEntryDescriptor) {
this.expressionState = expressionState;
this.typeConverter = typeConverter;
this.map = map;
this.key = key;
@@ -696,6 +705,7 @@ public class Indexer extends SpelNodeImpl {
@Override
public TypedValue getValue() {
this.expressionState.trackOperation();
Object value = this.map.get(this.key);
exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
return new TypedValue(value, this.mapEntryDescriptor.getMapValueTypeDescriptor(value));
@@ -707,6 +717,7 @@ public class Indexer extends SpelNodeImpl {
newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue),
this.mapEntryDescriptor.getMapValueTypeDescriptor());
}
this.expressionState.trackOperation();
this.map.put(this.key, newValue);
}
@@ -719,6 +730,8 @@ public class Indexer extends SpelNodeImpl {
private class PropertyAccessorValueRef implements ValueRef {
private final ExpressionState expressionState;
private final Object targetObject;
private final String name;
@@ -727,9 +740,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor targetObjectTypeDescriptor;
public PropertyAccessorValueRef(Object targetObject, String name,
public PropertyAccessorValueRef(ExpressionState expressionState, Object targetObject, String name,
EvaluationContext evaluationContext, TypeDescriptor targetObjectTypeDescriptor) {
this.expressionState = expressionState;
this.targetObject = targetObject;
this.name = name;
this.evaluationContext = evaluationContext;
@@ -747,6 +761,7 @@ public class Indexer extends SpelNodeImpl {
// Is it OK to use the cached accessor?
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType)) {
PropertyAccessor accessor = cachedPropertyReadState.accessor;
this.expressionState.trackOperation();
return accessor.read(this.evaluationContext, this.targetObject, this.name);
}
// If the above code block did not use a cached accessor and return a value,
@@ -761,6 +776,7 @@ public class Indexer extends SpelNodeImpl {
accessor = reflectivePropertyAccessor.createOptimalAccessor(
this.evaluationContext, this.targetObject, this.name);
}
this.expressionState.trackOperation();
TypedValue result = accessor.read(this.evaluationContext, this.targetObject, this.name);
Indexer.this.cachedPropertyReadState = new CachedPropertyState(accessor, targetType, this.name);
if (accessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
@@ -789,6 +805,7 @@ public class Indexer extends SpelNodeImpl {
// Is it OK to use the cached accessor?
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType)) {
PropertyAccessor accessor = cachedPropertyWriteState.accessor;
this.expressionState.trackOperation();
accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
return;
}
@@ -800,6 +817,7 @@ public class Indexer extends SpelNodeImpl {
AccessorUtils.getAccessorsToTry(targetType, this.evaluationContext.getPropertyAccessors());
for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canWrite(this.evaluationContext, this.targetObject, this.name)) {
this.expressionState.trackOperation();
accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
Indexer.this.cachedPropertyWriteState = new CachedPropertyState(accessor, targetType, this.name);
return;
@@ -824,6 +842,8 @@ public class Indexer extends SpelNodeImpl {
@SuppressWarnings({"rawtypes", "unchecked"})
private class CollectionIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final Collection collection;
private final int index;
@@ -836,9 +856,11 @@ public class Indexer extends SpelNodeImpl {
private final int maximumSize;
public CollectionIndexingValueRef(Collection collection, int index, TypeDescriptor collectionEntryDescriptor,
TypeConverter typeConverter, boolean growCollection, int maximumSize) {
public CollectionIndexingValueRef(ExpressionState expressionState, Collection collection, int index,
TypeDescriptor collectionEntryDescriptor, TypeConverter typeConverter,
boolean growCollection, int maximumSize) {
this.expressionState = expressionState;
this.collection = collection;
this.index = index;
this.collectionEntryDescriptor = collectionEntryDescriptor;
@@ -851,6 +873,7 @@ public class Indexer extends SpelNodeImpl {
public TypedValue getValue() {
growCollectionIfNecessary();
if (this.collection instanceof List list) {
this.expressionState.trackOperation();
Object o = list.get(this.index);
exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o));
@@ -858,6 +881,7 @@ public class Indexer extends SpelNodeImpl {
int pos = 0;
for (Object o : this.collection) {
this.expressionState.trackOperation();
if (pos == this.index) {
return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o));
}
@@ -879,6 +903,7 @@ public class Indexer extends SpelNodeImpl {
newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue),
this.collectionEntryDescriptor.getElementTypeDescriptor());
}
this.expressionState.trackOperation();
list.set(this.index, newValue);
}
@@ -900,6 +925,7 @@ public class Indexer extends SpelNodeImpl {
Constructor<?> ctor = getDefaultConstructor(elementType.getType());
int newElements = this.index - this.collection.size();
while (newElements >= 0) {
this.expressionState.trackOperation();
// Insert a null value if the element type does not have a default constructor.
this.collection.add(ctor != null ? ctor.newInstance() : null);
newElements--;
@@ -930,13 +956,18 @@ public class Indexer extends SpelNodeImpl {
private class StringIndexingValueRef implements ValueRef {
private final ExpressionState expressionState;
private final String target;
private final int index;
private final TypeDescriptor typeDescriptor;
public StringIndexingValueRef(String target, int index, TypeDescriptor typeDescriptor) {
public StringIndexingValueRef(
ExpressionState expressionState, String target, int index, TypeDescriptor typeDescriptor) {
this.expressionState = expressionState;
this.target = target;
this.index = index;
this.typeDescriptor = typeDescriptor;
@@ -948,6 +979,7 @@ public class Indexer extends SpelNodeImpl {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.STRING_INDEX_OUT_OF_BOUNDS,
this.target.length(), this.index);
}
this.expressionState.trackOperation();
return new TypedValue(String.valueOf(this.target.charAt(this.index)));
}
@@ -966,6 +998,8 @@ public class Indexer extends SpelNodeImpl {
private class IndexAccessorValueRef implements ValueRef {
private final ExpressionState expressionState;
private final Object target;
private final Object index;
@@ -975,9 +1009,10 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor typeDescriptor;
IndexAccessorValueRef(Object target, Object index, EvaluationContext evaluationContext,
TypeDescriptor typeDescriptor) {
IndexAccessorValueRef(ExpressionState expressionState, Object target, Object index,
EvaluationContext evaluationContext, TypeDescriptor typeDescriptor) {
this.expressionState = expressionState;
this.target = target;
this.index = index;
this.evaluationContext = evaluationContext;
@@ -999,6 +1034,7 @@ public class Indexer extends SpelNodeImpl {
IndexAccessor accessor = cachedIndexReadState.accessor;
if (this.evaluationContext.getIndexAccessors().contains(accessor)) {
try {
this.expressionState.trackOperation();
return accessor.read(this.evaluationContext, this.target, this.index);
}
catch (Exception ex) {
@@ -1017,6 +1053,7 @@ public class Indexer extends SpelNodeImpl {
AccessorUtils.getAccessorsToTry(this.target, this.evaluationContext.getIndexAccessors());
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canRead(this.evaluationContext, this.target, this.index)) {
this.expressionState.trackOperation();
TypedValue result = indexAccessor.read(this.evaluationContext, this.target, this.index);
Indexer.this.cachedIndexReadState = new CachedIndexState(indexAccessor, targetType, this.index);
if (indexAccessor instanceof CompilableIndexAccessor compilableIndexAccessor) {
@@ -1053,6 +1090,7 @@ public class Indexer extends SpelNodeImpl {
IndexAccessor accessor = cachedIndexWriteState.accessor;
if (this.evaluationContext.getIndexAccessors().contains(accessor)) {
try {
this.expressionState.trackOperation();
accessor.write(this.evaluationContext, this.target, this.index, newValue);
return;
}
@@ -1072,6 +1110,7 @@ public class Indexer extends SpelNodeImpl {
AccessorUtils.getAccessorsToTry(this.target, this.evaluationContext.getIndexAccessors());
for (IndexAccessor indexAccessor : accessorsToTry) {
if (indexAccessor.canWrite(this.evaluationContext, this.target, this.index)) {
this.expressionState.trackOperation();
indexAccessor.write(this.evaluationContext, this.target, this.index, newValue);
Indexer.this.cachedIndexWriteState = new CachedIndexState(indexAccessor, targetType, this.index);
return;
@@ -27,7 +27,7 @@ import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelNode;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -42,69 +42,64 @@ import org.springframework.util.Assert;
*/
public class InlineList extends SpelNodeImpl {
private final boolean isConstant;
@Nullable
private final TypedValue constant;
private volatile TypedValue constant;
public InlineList(int startPos, int endPos, SpelNodeImpl... args) {
super(startPos, endPos, args);
this.constant = computeConstantValue();
this.isConstant = determineIfConstant();
}
/**
* If all the components of the list are constants, or lists that themselves
* contain constants, then a constant list can be built to represent this node.
* <p>This will speed up later getValue calls and reduce the amount of garbage
* created.
* Determine whether this list is structurally eligible to be a constant
* value: whether all of its components are themselves constants or lists
* that contain only constants.
* <p>The actual constant value is created lazily on the first call to
* {@link #getValueInternal(ExpressionState)}.
*/
@Nullable
private TypedValue computeConstantValue() {
private boolean determineIfConstant() {
for (int c = 0, max = getChildCount(); c < max; c++) {
SpelNode child = getChild(c);
if (!(child instanceof Literal)) {
if (child instanceof InlineList inlineList) {
if (!inlineList.isConstant()) {
return null;
}
}
else if (!(child instanceof OpMinus opMinus) || !opMinus.isNegativeNumberLiteral()) {
return null;
}
if (child instanceof Literal) {
continue;
}
if (child instanceof InlineList inlineList && inlineList.isConstant()) {
continue;
}
if (child instanceof OpMinus opMinus && opMinus.isNegativeNumberLiteral()) {
continue;
}
return false;
}
List<Object> constantList = new ArrayList<>();
int childcount = getChildCount();
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
for (int c = 0; c < childcount; c++) {
SpelNode child = getChild(c);
if (child instanceof Literal literal) {
constantList.add(literal.getLiteralValue().getValue());
}
else if (child instanceof InlineList inlineList) {
constantList.add(inlineList.getConstantValue());
}
else if (child instanceof OpMinus) {
constantList.add(child.getValue(expressionState));
}
}
return new TypedValue(Collections.unmodifiableList(constantList));
return true;
}
@Override
public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException {
if (this.constant != null) {
return this.constant;
TypedValue result = this.constant;
if (result != null) {
return result;
}
else {
int childCount = getChildCount();
List<Object> returnValue = new ArrayList<>(childCount);
for (int c = 0; c < childCount; c++) {
returnValue.add(getChild(c).getValue(expressionState));
}
return new TypedValue(returnValue);
result = createList(expressionState);
if (this.isConstant) {
this.constant = result;
}
return result;
}
private TypedValue createList(ExpressionState expressionState) throws EvaluationException {
int childCount = getChildCount();
expressionState.trackOperation();
List<Object> list = new ArrayList<>(childCount);
for (int c = 0; c < childCount; c++) {
expressionState.trackOperation();
list.add(getChild(c).getValue(expressionState));
}
return new TypedValue(this.isConstant ? Collections.unmodifiableList(list) : list);
}
@Override
@@ -118,22 +113,38 @@ public class InlineList extends SpelNodeImpl {
}
/**
* Return whether this list is a constant value.
* Return whether this list is structurally a constant value.
* <p>Note that the resulting constant value is created lazily on the
* first call to {@link #getValueInternal(ExpressionState)} or
* {@link #getConstantValue()}.
*/
public boolean isConstant() {
return (this.constant != null);
return this.isConstant;
}
/**
* Return the cached constant {@link List} value for this inline list,
* lazily creating it on first access.
* @see #isConstant()
* @deprecated as of Spring Framework 6.2.19; this method was only intended for
* testing purposes and will be removed in a future version of the framework
*/
@SuppressWarnings("unchecked")
@Deprecated(since = "6.2.19")
@Nullable
public List<Object> getConstantValue() {
Assert.state(this.constant != null, "No constant");
return (List<Object>) this.constant.getValue();
Assert.state(this.isConstant, "Not a constant");
TypedValue result = this.constant;
if (result == null) {
result = createList(new ExpressionState(SimpleEvaluationContext.forReadOnlyDataBinding().build()));
this.constant = result;
}
return (List<Object>) result.getValue();
}
@Override
public boolean isCompilable() {
return isConstant();
return this.isConstant;
}
@Override
@@ -24,7 +24,7 @@ import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelNode;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -39,105 +39,80 @@ import org.springframework.util.Assert;
*/
public class InlineMap extends SpelNodeImpl {
private final boolean isConstant;
@Nullable
private final TypedValue constant;
private volatile TypedValue constant;
public InlineMap(int startPos, int endPos, SpelNodeImpl... args) {
super(startPos, endPos, args);
this.constant = computeConstantValue();
this.isConstant = determineIfConstant();
}
/**
* If all the components of the map are constants, or lists/maps that themselves
* contain constants, then a constant map can be built to represent this node.
* <p>This will speed up later getValue calls and reduce the amount of garbage
* created.
* Determine whether this map is structurally eligible to be a constant
* value: whether all of its components are themselves constants or lists/maps
* that contain only constants.
* <p>The actual constant value is created lazily on the first call to
* {@link #getValueInternal(ExpressionState)}.
*/
@Nullable
private TypedValue computeConstantValue() {
for (int c = 0, max = getChildCount(); c < max; c++) {
SpelNode child = getChild(c);
if (!(child instanceof Literal)) {
if (child instanceof InlineList inlineList) {
if (!inlineList.isConstant()) {
return null;
}
}
else if (child instanceof InlineMap inlineMap) {
if (!inlineMap.isConstant()) {
return null;
}
}
else if (!(c % 2 == 0 && child instanceof PropertyOrFieldReference)) {
if (!(child instanceof OpMinus opMinus) || !opMinus.isNegativeNumberLiteral()) {
return null;
}
}
}
}
Map<Object, Object> constantMap = new LinkedHashMap<>();
private boolean determineIfConstant() {
int childCount = getChildCount();
ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext());
for (int c = 0; c < childCount; c++) {
SpelNode keyChild = getChild(c++);
Object key;
if (keyChild instanceof Literal literal) {
key = literal.getLiteralValue().getValue();
SpelNode child = getChild(c);
if (child instanceof Literal) {
continue;
}
else if (keyChild instanceof PropertyOrFieldReference propertyOrFieldReference) {
key = propertyOrFieldReference.getName();
if (child instanceof InlineList inlineList && inlineList.isConstant()) {
continue;
}
else if (keyChild instanceof OpMinus) {
key = keyChild.getValue(expressionState);
if (child instanceof InlineMap inlineMap && inlineMap.isConstant()) {
continue;
}
else {
return null;
if (c % 2 == 0 && child instanceof PropertyOrFieldReference) {
continue;
}
SpelNode valueChild = getChild(c);
Object value = null;
if (valueChild instanceof Literal literal) {
value = literal.getLiteralValue().getValue();
if (child instanceof OpMinus opMinus && opMinus.isNegativeNumberLiteral()) {
continue;
}
else if (valueChild instanceof InlineList inlineList) {
value = inlineList.getConstantValue();
}
else if (valueChild instanceof InlineMap inlineMap) {
value = inlineMap.getConstantValue();
}
else if (valueChild instanceof OpMinus) {
value = valueChild.getValue(expressionState);
}
constantMap.put(key, value);
return false;
}
return new TypedValue(Collections.unmodifiableMap(constantMap));
return true;
}
@Override
public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException {
if (this.constant != null) {
return this.constant;
TypedValue result = this.constant;
if (result != null) {
return result;
}
else {
Map<Object, Object> returnValue = new LinkedHashMap<>();
int childcount = getChildCount();
for (int c = 0; c < childcount; c++) {
SpelNode keyChild = getChild(c++);
Object key = null;
if (keyChild instanceof PropertyOrFieldReference reference) {
key = reference.getName();
}
else {
key = keyChild.getValue(expressionState);
}
Object value = getChild(c).getValue(expressionState);
returnValue.put(key, value);
result = createMap(expressionState);
if (this.isConstant) {
this.constant = result;
}
return result;
}
private TypedValue createMap(ExpressionState expressionState) throws EvaluationException {
expressionState.trackOperation();
Map<Object, Object> map = new LinkedHashMap<>();
int childCount = getChildCount();
for (int c = 0; c < childCount; c++) {
SpelNode keyChild = getChild(c++);
Object key;
if (keyChild instanceof PropertyOrFieldReference reference) {
key = reference.getName();
}
return new TypedValue(returnValue);
else {
key = keyChild.getValue(expressionState);
}
Object value = getChild(c).getValue(expressionState);
expressionState.trackOperation();
map.put(key, value);
}
return new TypedValue(this.isConstant ? Collections.unmodifiableMap(map) : map);
}
@Override
@@ -156,17 +131,33 @@ public class InlineMap extends SpelNodeImpl {
}
/**
* Return whether this map is a constant value.
* Return whether this map is structurally a constant value.
* <p>Note that the resulting constant value is created lazily on the
* first call to {@link #getValueInternal(ExpressionState)} or
* {@link #getConstantValue()}.
*/
public boolean isConstant() {
return this.constant != null;
return this.isConstant;
}
/**
* Return the cached constant {@link Map} value for this inline map,
* lazily creating it on first access.
* @see #isConstant()
* @deprecated as of Spring Framework 6.2.19; this method was only intended for
* testing purposes and will be removed in a future version of the framework
*/
@SuppressWarnings("unchecked")
@Deprecated(since = "6.2.19")
@Nullable
public Map<Object, Object> getConstantValue() {
Assert.state(this.constant != null, "No constant");
return (Map<Object, Object>) this.constant.getValue();
Assert.state(this.isConstant, "Not a constant");
TypedValue result = this.constant;
if (result == null) {
result = createMap(new ExpressionState(SimpleEvaluationContext.forReadOnlyDataBinding().build()));
this.constant = result;
}
return (Map<Object, Object>) result.getValue();
}
}
@@ -105,12 +105,12 @@ public class MethodReference extends SpelNodeImpl {
Object value = state.getActiveContextObject().getValue();
TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor();
Object[] arguments = getArguments(state);
TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments);
TypedValue result = getValueInternal(state, evaluationContext, value, targetType, arguments);
updateExitTypeDescriptor();
return result;
}
private TypedValue getValueInternal(EvaluationContext evaluationContext,
private TypedValue getValueInternal(ExpressionState state, EvaluationContext evaluationContext,
@Nullable Object value, @Nullable TypeDescriptor targetType, Object[] arguments) {
List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
@@ -122,6 +122,7 @@ public class MethodReference extends SpelNodeImpl {
MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes);
if (executorToUse != null) {
try {
state.trackOperation();
return executorToUse.execute(evaluationContext, value, arguments);
}
catch (AccessException ex) {
@@ -149,6 +150,7 @@ public class MethodReference extends SpelNodeImpl {
this.cachedExecutor = new CachedMethodExecutor(
executorToUse, (value instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes);
try {
state.trackOperation();
return executorToUse.execute(evaluationContext, value, arguments);
}
catch (AccessException ex) {
@@ -376,6 +378,8 @@ public class MethodReference extends SpelNodeImpl {
private class MethodValueRef implements ValueRef {
private final ExpressionState expressionState;
private final EvaluationContext evaluationContext;
@Nullable
@@ -387,6 +391,7 @@ public class MethodReference extends SpelNodeImpl {
private final Object[] arguments;
public MethodValueRef(ExpressionState state, Object[] arguments) {
this.expressionState = state;
this.evaluationContext = state.getEvaluationContext();
this.value = state.getActiveContextObject().getValue();
this.targetType = state.getActiveContextObject().getTypeDescriptor();
@@ -396,7 +401,7 @@ public class MethodReference extends SpelNodeImpl {
@Override
public TypedValue getValue() {
TypedValue result = MethodReference.this.getValueInternal(
this.evaluationContext, this.value, this.targetType, this.arguments);
this.expressionState, this.evaluationContext, this.value, this.targetType, this.arguments);
updateExitTypeDescriptor();
return result;
}
@@ -46,6 +46,7 @@ public class OpAnd extends Operator {
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
if (!getBooleanValue(state, getLeftOperand())) {
// no need to evaluate right operand
return BooleanTypedValue.FALSE;
@@ -68,6 +68,7 @@ public class OpDec extends Operator {
TypedValue newValue = null;
if (operandValue instanceof Number op1) {
state.trackOperation();
if (op1 instanceof BigDecimal bigDecimal) {
newValue = new TypedValue(bigDecimal.subtract(BigDecimal.ONE), operandTypedValue.getTypeDescriptor());
}
@@ -114,8 +115,9 @@ public class OpDec extends Operator {
}
}
// set the new value
state.trackOperation();
try {
// set the new value
lvalue.setValue(newValue.getValue());
}
catch (SpelEvaluationException see) {
@@ -51,6 +51,7 @@ public class OpDivide extends Operator {
Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -43,6 +43,7 @@ public class OpEQ extends Operator {
Object right = getRightOperand().getValueInternal(state).getValue();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
state.trackOperation();
return BooleanTypedValue.forValue(equalityCheck(state.getEvaluationContext(), left, right));
}
@@ -46,6 +46,7 @@ public class OpGE extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -46,6 +46,7 @@ public class OpGT extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -66,6 +66,7 @@ public class OpInc extends Operator {
TypedValue newValue = null;
if (value instanceof Number op1) {
state.trackOperation();
if (op1 instanceof BigDecimal bigDecimal) {
newValue = new TypedValue(bigDecimal.add(BigDecimal.ONE), typedValue.getTypeDescriptor());
}
@@ -110,8 +111,9 @@ public class OpInc extends Operator {
}
}
// set the new value
state.trackOperation();
try {
// set the new value
valueRef.setValue(newValue.getValue());
}
catch (SpelEvaluationException see) {
@@ -46,6 +46,7 @@ public class OpLE extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -46,6 +46,7 @@ public class OpLT extends Operator {
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Object left = getLeftOperand().getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
state.trackOperation();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
@@ -51,6 +51,13 @@ public class OpMinus extends Operator {
public OpMinus(int startPos, int endPos, SpelNodeImpl... operands) {
super("-", startPos, endPos, operands);
// If this is a unary negation of a number literal, the exit type descriptor
// can be derived statically from the literal. Doing so up front lets the
// compiler determine that this node is compilable without first evaluating
// the expression.
if (isNegativeNumberLiteral()) {
this.exitTypeDescriptor = ((Literal) operands[0]).exitTypeDescriptor;
}
}
@@ -72,6 +79,7 @@ public class OpMinus extends Operator {
if (this.children.length < 2) { // if only one operand, then this is unary minus
Object operand = leftOp.getValueInternal(state).getValue();
if (operand instanceof Number number) {
state.trackOperation();
if (number instanceof BigDecimal bigDecimal) {
return new TypedValue(bigDecimal.negate());
}
@@ -112,6 +120,7 @@ public class OpMinus extends Operator {
Object right = getRightOperand().getValueInternal(state).getValue();
if (left instanceof Number leftNumber && right instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -145,6 +154,7 @@ public class OpMinus extends Operator {
}
if (left instanceof String theString && right instanceof Integer theInteger && theString.length() == 1) {
state.trackOperation();
// Implements character - int (ie. b - 1 = a)
return new TypedValue(Character.toString((char) (theString.charAt(0) - theInteger)));
}
@@ -49,6 +49,7 @@ public class OpModulus extends Operator {
Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -82,6 +82,7 @@ public class OpMultiply extends Operator {
Object rightOperand = getRightOperand().getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -116,6 +117,7 @@ public class OpMultiply extends Operator {
if (leftOperand instanceof String text && rightOperand instanceof Integer count) {
checkRepeatedTextSize(text, count);
state.trackOperation();
return new TypedValue(text.repeat(count));
}
@@ -44,6 +44,7 @@ public class OpNE extends Operator {
Object rightValue = getRightOperand().getValueInternal(state).getValue();
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(leftValue);
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(rightValue);
state.trackOperation();
return BooleanTypedValue.forValue(!equalityCheck(state.getEvaluationContext(), leftValue, rightValue));
}
@@ -45,6 +45,7 @@ public class OpOr extends Operator {
@Override
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
state.trackOperation();
if (getBooleanValue(state, getLeftOperand())) {
// no need to evaluate right operand
return BooleanTypedValue.TRUE;
@@ -73,6 +73,7 @@ public class OpPlus extends Operator {
if (this.children.length < 2) { // if only one operand, then this is unary plus
Object operandOne = leftOp.getValueInternal(state).getValue();
if (operandOne instanceof Number) {
state.trackOperation();
if (operandOne instanceof Double) {
this.exitTypeDescriptor = "D";
}
@@ -96,6 +97,7 @@ public class OpPlus extends Operator {
Object rightOperand = operandTwoValue.getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class);
@@ -132,21 +134,21 @@ public class OpPlus extends Operator {
this.exitTypeDescriptor = "Ljava/lang/String";
checkStringLength(leftString);
checkStringLength(rightString);
return concatenate(leftString, rightString);
return concatenate(state, leftString, rightString);
}
if (leftOperand instanceof String leftString) {
checkStringLength(leftString);
String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state));
checkStringLength(rightString);
return concatenate(leftString, rightString);
return concatenate(state, leftString, rightString);
}
if (rightOperand instanceof String rightString) {
checkStringLength(rightString);
String leftString = (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state));
checkStringLength(leftString);
return concatenate(leftString, rightString);
return concatenate(state, leftString, rightString);
}
return state.operate(Operation.ADD, leftOperand, rightOperand);
@@ -163,8 +165,9 @@ public class OpPlus extends Operator {
}
}
private TypedValue concatenate(String leftString, String rightString) {
private TypedValue concatenate(ExpressionState state, String leftString, String rightString) {
checkStringLength(leftString.length() + rightString.length());
state.trackOperation();
return new TypedValue(leftString + rightString);
}
@@ -68,6 +68,7 @@ public class OperatorBetween extends Operator {
Object high = list.get(1);
TypeComparator comp = state.getTypeComparator();
try {
state.trackOperation();
return BooleanTypedValue.forValue(comp.compare(left, low) >= 0 && comp.compare(left, high) <= 0);
}
catch (SpelEvaluationException ex) {
@@ -62,6 +62,7 @@ public class OperatorInstanceof extends Operator {
Object leftValue = left.getValue();
Object rightValue = right.getValue();
BooleanTypedValue result;
state.trackOperation();
if (!(rightValue instanceof Class<?> rightClass)) {
throw new SpelEvaluationException(getRightOperand().getStartPosition(),
SpelMessage.INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND,
@@ -125,6 +125,7 @@ public class OperatorMatches extends Operator {
SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH);
}
state.trackOperation();
try {
Pattern pattern = this.patternCache.get(regex);
Matcher matcher = pattern.matcher(new MatcherInput(input, new AccessCount()));
@@ -48,6 +48,7 @@ public class OperatorNot extends SpelNodeImpl { // Not is a unary operator so d
if (value == null) {
throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean");
}
state.trackOperation();
return BooleanTypedValue.forValue(!value);
}
catch (SpelEvaluationException ex) {
@@ -48,6 +48,7 @@ public class OperatorPower extends Operator {
Object rightOperand = rightOp.getValueInternal(state).getValue();
if (leftOperand instanceof Number leftNumber && rightOperand instanceof Number rightNumber) {
state.trackOperation();
if (leftNumber instanceof BigDecimal) {
BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class);
return new TypedValue(leftBigDecimal.pow(rightNumber.intValue()));
@@ -79,11 +79,13 @@ public class Projection extends SpelNodeImpl {
// that can be referenced in the operation -- for example,
// {'a':'y', 'b':'n'}.![value == 'y' ? key : null] evaluates to ['a', null].
if (operand instanceof Map<?, ?> mapData) {
state.trackOperation();
List<Object> result = new ArrayList<>();
for (Map.Entry<?, ?> entry : mapData.entrySet()) {
try {
state.pushActiveContextObject(new TypedValue(entry));
state.enterScope();
state.trackOperation();
result.add(this.children[0].getValueInternal(state).getValue());
}
finally {
@@ -99,6 +101,7 @@ public class Projection extends SpelNodeImpl {
Iterable<?> data = (operand instanceof Iterable<?> iterable ?
iterable : Arrays.asList(ObjectUtils.toObjectArray(operand)));
state.trackOperation();
List<Object> result = new ArrayList<>();
Class<?> arrayElementType = null;
for (Object element : data) {
@@ -109,6 +112,7 @@ public class Projection extends SpelNodeImpl {
if (value != null && operandIsArray) {
arrayElementType = determineCommonType(arrayElementType, value.getClass());
}
state.trackOperation();
result.add(value);
}
finally {
@@ -91,14 +91,14 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
@Override
public ValueRef getValueRef(ExpressionState state) throws EvaluationException {
return new AccessorValueRef(this, state.getActiveContextObject(), state.getEvaluationContext(),
return new AccessorValueRef(this, state.getActiveContextObject(), state, state.getEvaluationContext(),
state.getConfiguration().isAutoGrowNullReferences());
}
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(),
state.getConfiguration().isAutoGrowNullReferences());
TypedValue tv = getValueInternal(state, state.getActiveContextObject(),
state.getEvaluationContext(), state.getConfiguration().isAutoGrowNullReferences());
PropertyAccessor accessorToUse = this.cachedReadAccessor;
if (accessorToUse instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
setExitTypeDescriptor(CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType()));
@@ -106,10 +106,10 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
return tv;
}
private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext evalContext,
boolean isAutoGrowNullReferences) throws EvaluationException {
private TypedValue getValueInternal(ExpressionState state, TypedValue contextObject,
EvaluationContext evalContext, boolean isAutoGrowNullReferences) throws EvaluationException {
TypedValue result = readProperty(contextObject, evalContext, this.name);
TypedValue result = readProperty(state, contextObject, evalContext, this.name);
// Dynamically create the objects if the user has requested that optional behavior
if (result.getValue() == null && isAutoGrowNullReferences &&
@@ -119,16 +119,18 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
// Create a new collection or map ready for the indexer
if (List.class == resultDescriptor.getType()) {
if (isWritableProperty(this.name, contextObject, evalContext)) {
state.trackOperation();
List<?> newList = new ArrayList<>();
writeProperty(contextObject, evalContext, this.name, newList);
result = readProperty(contextObject, evalContext, this.name);
writeProperty(state, contextObject, evalContext, this.name, newList);
result = readProperty(state, contextObject, evalContext, this.name);
}
}
else if (Map.class == resultDescriptor.getType()) {
if (isWritableProperty(this.name,contextObject, evalContext)) {
state.trackOperation();
Map<?,?> newMap = new HashMap<>();
writeProperty(contextObject, evalContext, this.name, newMap);
result = readProperty(contextObject, evalContext, this.name);
writeProperty(state, contextObject, evalContext, this.name, newMap);
result = readProperty(state, contextObject, evalContext, this.name);
}
}
else {
@@ -136,9 +138,10 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
try {
if (isWritableProperty(this.name,contextObject, evalContext)) {
Class<?> clazz = resultDescriptor.getType();
state.trackOperation();
Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance();
writeProperty(contextObject, evalContext, this.name, newObject);
result = readProperty(contextObject, evalContext, this.name);
writeProperty(state, contextObject, evalContext, this.name, newObject);
result = readProperty(state, contextObject, evalContext, this.name);
}
}
catch (InvocationTargetException ex) {
@@ -159,7 +162,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
writeProperty(state, state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
return typedValue;
}
@@ -178,8 +181,8 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
* @return the value of the property
* @throws EvaluationException if any problem accessing the property, or if it cannot be found
*/
private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
throws EvaluationException {
private TypedValue readProperty(ExpressionState state, TypedValue contextObject,
EvaluationContext evalContext, String name) throws EvaluationException {
Object targetObject = contextObject.getValue();
if (targetObject == null && isNullSafe()) {
@@ -190,6 +193,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try {
state.trackOperation();
return accessorToUse.read(evalContext, targetObject, name);
}
catch (Exception ex) {
@@ -213,11 +217,15 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
evalContext, targetObject, name);
}
this.cachedReadAccessor = accessor;
state.trackOperation();
return accessor.read(evalContext, targetObject, name);
}
}
}
catch (Exception ex) {
if (ex instanceof SpelEvaluationException see) {
throw see;
}
throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage());
}
@@ -230,9 +238,8 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
}
}
private void writeProperty(
TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue)
throws EvaluationException {
private void writeProperty(ExpressionState state, TypedValue contextObject, EvaluationContext evalContext,
String name, @Nullable Object newValue) throws EvaluationException {
Object targetObject = contextObject.getValue();
if (targetObject == null) {
@@ -247,6 +254,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try {
state.trackOperation();
accessorToUse.write(evalContext, targetObject, name, newValue);
return;
}
@@ -264,6 +272,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canWrite(evalContext, targetObject, name)) {
this.cachedWriteAccessor = accessor;
state.trackOperation();
accessor.write(evalContext, targetObject, name, newValue);
return;
}
@@ -357,15 +366,18 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
private final TypedValue contextObject;
private final ExpressionState expressionState;
private final EvaluationContext evalContext;
private final boolean autoGrowNullReferences;
public AccessorValueRef(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject,
EvaluationContext evalContext, boolean autoGrowNullReferences) {
ExpressionState expressionState, EvaluationContext evalContext, boolean autoGrowNullReferences) {
this.ref = propertyOrFieldReference;
this.contextObject = activeContextObject;
this.expressionState = expressionState;
this.evalContext = evalContext;
this.autoGrowNullReferences = autoGrowNullReferences;
}
@@ -373,7 +385,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
@Override
public TypedValue getValue() {
TypedValue value =
this.ref.getValueInternal(this.contextObject, this.evalContext, this.autoGrowNullReferences);
this.ref.getValueInternal(this.expressionState, this.contextObject, this.evalContext, this.autoGrowNullReferences);
if (this.ref.cachedReadAccessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
this.ref.setExitTypeDescriptor(CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType()));
}
@@ -382,7 +394,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
@Override
public void setValue(@Nullable Object newValue) {
this.ref.writeProperty(this.contextObject, this.evalContext, this.ref.name, newValue);
this.ref.writeProperty(this.expressionState, this.contextObject, this.evalContext, this.ref.name, newValue);
}
@Override
@@ -99,6 +99,7 @@ public class Selection extends SpelNodeImpl {
SpelNodeImpl selectionCriteria = this.children[0];
if (operand instanceof Map<?, ?> mapdata) {
state.trackOperation();
Map<Object, Object> result = new HashMap<>();
Object lastKey = null;
@@ -110,6 +111,7 @@ public class Selection extends SpelNodeImpl {
Object val = selectionCriteria.getValueInternal(state).getValue();
if (val instanceof Boolean b) {
if (b) {
state.trackOperation();
result.put(entry.getKey(), entry.getValue());
if (this.variant == FIRST) {
return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this);
@@ -146,6 +148,7 @@ public class Selection extends SpelNodeImpl {
Iterable<?> data = (operand instanceof Iterable<?> iterable ? iterable :
Arrays.asList(ObjectUtils.toObjectArray(operand)));
state.trackOperation();
List<Object> result = new ArrayList<>();
for (Object element : data) {
try {
@@ -157,6 +160,7 @@ public class Selection extends SpelNodeImpl {
if (this.variant == FIRST) {
return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this);
}
state.trackOperation();
result.add(element);
}
}
@@ -51,12 +51,13 @@ public class Ternary extends SpelNodeImpl {
*/
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
Boolean value = this.children[0].getValue(state, Boolean.class);
if (value == null) {
Boolean condition = this.children[0].getValue(state, Boolean.class);
if (condition == null) {
throw new SpelEvaluationException(getChild(0).getStartPosition(),
SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean");
}
TypedValue result = this.children[value ? 1 : 2].getValueInternal(state);
state.trackOperation();
TypedValue result = this.children[condition ? 1 : 2].getValueInternal(state);
computeExitTypeDescriptor();
return result;
}
@@ -58,6 +58,7 @@ public class TypeReference extends SpelNodeImpl {
// TODO Possible optimization: if we cache the discovered type reference, but can we do that?
String typeName = (String) this.children[0].getValueInternal(state).getValue();
Assert.state(typeName != null, "No type name");
state.trackOperation();
if (!typeName.contains(".") && Character.isLowerCase(typeName.charAt(0))) {
TypeCode tc = TypeCode.valueOf(typeName.toUpperCase(Locale.ROOT));
if (tc != TypeCode.OBJECT) {
@@ -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));
@@ -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")