diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java
index 0b4d2eab971b..0c393a86dbe6 100644
--- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java
+++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.expression;
import java.util.List;
+import java.util.function.Supplier;
import org.springframework.lang.Nullable;
@@ -24,12 +25,21 @@
* Expressions are executed in an evaluation context. It is in this context that
* references are resolved when encountered during expression evaluation.
*
- *
There is a default implementation of this EvaluationContext interface:
- * {@link org.springframework.expression.spel.support.StandardEvaluationContext}
- * which can be extended, rather than having to implement everything manually.
+ *
There are two default implementations of this interface.
+ *
+ * - {@link org.springframework.expression.spel.support.SimpleEvaluationContext
+ * SimpleEvaluationContext}: a simpler builder-style {@code EvaluationContext}
+ * variant for data-binding purposes, which allows for opting into several SpEL
+ * features as needed.
+ * - {@link org.springframework.expression.spel.support.StandardEvaluationContext
+ * StandardEvaluationContext}: a powerful and highly configurable {@code EvaluationContext}
+ * implementation, which can be extended, rather than having to implement everything
+ * manually.
+ *
*
* @author Andy Clement
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.0
*/
public interface EvaluationContext {
@@ -85,7 +95,30 @@ public interface EvaluationContext {
OperatorOverloader getOperatorOverloader();
/**
- * Set a named variable within this evaluation context to a specified value.
+ * Assign the value created by the specified {@link Supplier} to a named variable
+ * within this evaluation context.
+ * In contrast to {@link #setVariable(String, Object)}, this method should only
+ * be invoked to support the assignment operator ({@code =}) within an expression.
+ *
By default, this method delegates to {@code setVariable(String, Object)},
+ * providing the value created by the {@code valueSupplier}. Concrete implementations
+ * may override this default method to provide different semantics.
+ * @param name the name of the variable to assign
+ * @param valueSupplier the supplier of the value to be assigned to the variable
+ * @return a {@link TypedValue} wrapping the assigned value
+ * @since 5.2.24
+ */
+ default TypedValue assignVariable(String name, Supplier valueSupplier) {
+ TypedValue typedValue = valueSupplier.get();
+ setVariable(name, typedValue.getValue());
+ return typedValue;
+ }
+
+ /**
+ * Set a named variable in this evaluation context to a specified value.
+ * In contrast to {@link #assignVariable(String, Supplier)}, this method
+ * should only be invoked programmatically when interacting directly with the
+ * {@code EvaluationContext} — for example, to provide initial
+ * configuration for the context.
* @param name the name of the variable to set
* @param value the value to be placed in the variable
*/
@@ -93,7 +126,7 @@ public interface EvaluationContext {
/**
* Look up a named variable within this evaluation context.
- * @param name variable to lookup
+ * @param name the name of the variable to look up
* @return the value of the variable, or {@code null} if not found
*/
@Nullable
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java
index 252af447af31..8dadae596732 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.function.Supplier;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
@@ -38,18 +39,19 @@
import org.springframework.util.CollectionUtils;
/**
- * An ExpressionState is for maintaining per-expression-evaluation state, any changes to
- * it are not seen by other expressions but it gives a place to hold local variables and
+ * ExpressionState is for maintaining per-expression-evaluation state: any changes to
+ * it are not seen by other expressions, but it gives a place to hold local variables and
* for component expressions in a compound expression to communicate state. This is in
* contrast to the EvaluationContext, which is shared amongst expression evaluations, and
* any changes to it will be seen by other expressions or any code that chooses to ask
* questions of the context.
*
- *
It also acts as a place for to define common utility routines that the various AST
+ *
It also acts as a place to define common utility routines that the various AST
* nodes might need.
*
* @author Andy Clement
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.0
*/
public class ExpressionState {
@@ -138,6 +140,29 @@ public TypedValue getScopeRootContextObject() {
return this.scopeRootObjects.element();
}
+ /**
+ * Assign the value created by the specified {@link Supplier} to a named variable
+ * within the evaluation context.
+ *
In contrast to {@link #setVariable(String, Object)}, this method should
+ * only be invoked to support assignment within an expression.
+ * @param name the name of the variable to assign
+ * @param valueSupplier the supplier of the value to be assigned to the variable
+ * @return a {@link TypedValue} wrapping the assigned value
+ * @since 5.2.24
+ * @see EvaluationContext#assignVariable(String, Supplier)
+ */
+ public TypedValue assignVariable(String name, Supplier valueSupplier) {
+ return this.relatedContext.assignVariable(name, valueSupplier);
+ }
+
+ /**
+ * Set a named variable in the evaluation context to a specified value.
+ * In contrast to {@link #assignVariable(String, Supplier)}, this method
+ * should only be invoked programmatically.
+ * @param name the name of the variable to set
+ * @param value the value to be placed in the variable
+ * @see EvaluationContext#setVariable(String, Object)
+ */
public void setVariable(String name, @Nullable Object value) {
this.relatedContext.setVariable(name, value);
}
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java
index 9bac48e2b34d..c5fe3bcec256 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java
@@ -280,7 +280,11 @@ public enum SpelMessage {
/** @since 5.2.24 */
MAX_EXPRESSION_LENGTH_EXCEEDED(Kind.ERROR, 1079,
- "SpEL expression is too long, exceeding the threshold of ''{0}'' characters");
+ "SpEL expression is too long, exceeding the threshold of ''{0}'' characters"),
+
+ /** @since 5.2.24 */
+ VARIABLE_ASSIGNMENT_NOT_SUPPORTED(Kind.ERROR, 1080,
+ "Assignment to variable ''{0}'' is not supported");
private final Kind kind;
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java
index a009a07db512..55e5d2e4ff08 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
*
Example: 'someNumberProperty=42'
*
* @author Andy Clement
+ * @author Sam Brannen
* @since 3.0
*/
public class Assign extends SpelNodeImpl {
@@ -38,9 +39,7 @@ public Assign(int startPos, int endPos, SpelNodeImpl... operands) {
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
- TypedValue newValue = this.children[1].getValueInternal(state);
- getChild(0).setValue(state, newValue.getValue());
- return newValue;
+ return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
}
@Override
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java
index 0e47facfa7bf..616a503a4ec1 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.expression.spel.ast;
import java.util.StringJoiner;
+import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.expression.EvaluationException;
@@ -24,13 +25,13 @@
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
-import org.springframework.lang.Nullable;
/**
* Represents a DOT separated expression sequence, such as
* {@code 'property1.property2.methodOne()'}.
*
* @author Andy Clement
+ * @author Sam Brannen
* @since 3.0
*/
public class CompoundExpression extends SpelNodeImpl {
@@ -95,8 +96,12 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
}
@Override
- public void setValue(ExpressionState state, @Nullable Object value) throws EvaluationException {
- getValueRef(state).setValue(value);
+ public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier)
+ throws EvaluationException {
+
+ TypedValue typedValue = valueSupplier.get();
+ getValueRef(state).setValue(typedValue.getValue());
+ return typedValue;
}
@Override
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java
index 04b502816169..724f50d0790b 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
+import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.core.convert.TypeDescriptor;
@@ -45,11 +46,12 @@
/**
* An Indexer can index into some proceeding structure to access a particular piece of it.
- * Supported structures are: strings / collections (lists/sets) / arrays.
+ * Supported structures are: strings / collections (lists/sets) / arrays.
*
* @author Andy Clement
* @author Phillip Webb
* @author Stephane Nicoll
+ * @author Sam Brannen
* @since 3.0
*/
// TODO support multidimensional arrays
@@ -102,8 +104,12 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
}
@Override
- public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
- getValueRef(state).setValue(newValue);
+ public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier)
+ throws EvaluationException {
+
+ TypedValue typedValue = valueSupplier.get();
+ getValueRef(state).setValue(typedValue.getValue());
+ return typedValue;
}
@Override
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java
index ec509199eb89..69389e4b4a14 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
@@ -46,6 +47,7 @@
* @author Andy Clement
* @author Juergen Hoeller
* @author Clark Duplichien
+ * @author Sam Brannen
* @since 3.0
*/
public class PropertyOrFieldReference extends SpelNodeImpl {
@@ -147,8 +149,12 @@ else if (Map.class == resultDescriptor.getType()) {
}
@Override
- public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
- writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue);
+ public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier)
+ throws EvaluationException {
+
+ TypedValue typedValue = valueSupplier.get();
+ writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
+ return typedValue;
}
@Override
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java
index 3976fbf3425d..daff26757839 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
+import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
@@ -40,6 +41,7 @@
*
* @author Andy Clement
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.0
*/
public abstract class SpelNodeImpl implements SpelNode, Opcodes {
@@ -64,7 +66,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
* The descriptor is like the bytecode form but is slightly easier to work with.
* It does not include the trailing semicolon (for non array reference types).
* Some examples: Ljava/lang/String, I, [I
- */
+ */
@Nullable
protected volatile String exitTypeDescriptor;
@@ -83,8 +85,8 @@ public SpelNodeImpl(int startPos, int endPos, SpelNodeImpl... operands) {
/**
- * Return {@code true} if the next child is one of the specified classes.
- */
+ * Return {@code true} if the next child is one of the specified classes.
+ */
protected boolean nextChildIs(Class>... classes) {
if (this.parent != null) {
SpelNodeImpl[] peers = this.parent.children;
@@ -125,6 +127,28 @@ public boolean isWritable(ExpressionState expressionState) throws EvaluationExce
@Override
public void setValue(ExpressionState expressionState, @Nullable Object newValue) throws EvaluationException {
+ setValueInternal(expressionState, () -> new TypedValue(newValue));
+ }
+
+ /**
+ * Evaluate the expression to a node and then set the new value created by the
+ * specified {@link Supplier} on that node.
+ *
For example, if the expression evaluates to a property reference, then the
+ * property will be set to the new value.
+ *
Favor this method over {@link #setValue(ExpressionState, Object)} when
+ * the value should be lazily computed.
+ *
By default, this method throws a {@link SpelEvaluationException},
+ * effectively disabling this feature. Subclasses may override this method to
+ * provide an actual implementation.
+ * @param expressionState the current expression state (includes the context)
+ * @param valueSupplier a supplier of the new value
+ * @throws EvaluationException if any problem occurs evaluating the expression or
+ * setting the new value
+ * @since 5.2.24
+ */
+ public TypedValue setValueInternal(ExpressionState expressionState, Supplier valueSupplier)
+ throws EvaluationException {
+
throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass());
}
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java
index 769e4efedbea..97dae78e902e 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,11 @@
package org.springframework.expression.spel.ast;
import java.lang.reflect.Modifier;
+import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
@@ -27,10 +29,11 @@
import org.springframework.lang.Nullable;
/**
- * Represents a variable reference, eg. #someVar. Note this is different to a *local*
- * variable like $someVar
+ * Represents a variable reference — for example, {@code #someVar}. Note
+ * that this is different than a local variable like {@code $someVar}.
*
* @author Andy Clement
+ * @author Sam Brannen
* @since 3.0
*/
public class VariableReference extends SpelNodeImpl {
@@ -53,14 +56,14 @@ public VariableReference(String variableName, int startPos, int endPos) {
@Override
public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException {
if (this.name.equals(THIS)) {
- return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(),this);
+ return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(), this);
}
if (this.name.equals(ROOT)) {
- return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(),this);
+ return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(), this);
}
TypedValue result = state.lookupVariable(this.name);
// a null value will mean either the value was null or the variable was not found
- return new VariableRef(this.name,result,state.getEvaluationContext());
+ return new VariableRef(this.name, result, state.getEvaluationContext());
}
@Override
@@ -90,8 +93,10 @@ public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationE
}
@Override
- public void setValue(ExpressionState state, @Nullable Object value) throws SpelEvaluationException {
- state.setVariable(this.name, value);
+ public TypedValue setValueInternal(ExpressionState state, Supplier valueSupplier)
+ throws EvaluationException {
+
+ return state.assignVariable(this.name, valueSupplier);
}
@Override
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java
index d8826e344720..1168c9c91a26 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
@@ -78,6 +79,7 @@
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 4.3.15
* @see #forPropertyAccessors
* @see #forReadOnlyDataBinding()
@@ -200,6 +202,17 @@ public OperatorOverloader getOperatorOverloader() {
return this.operatorOverloader;
}
+ /**
+ * {@code SimpleEvaluationContext} does not support variable assignment within
+ * expressions.
+ * @throws SpelEvaluationException with {@link SpelMessage#VARIABLE_ASSIGNMENT_NOT_SUPPORTED}
+ * @since 5.2.24
+ */
+ @Override
+ public TypedValue assignVariable(String name, Supplier valueSupplier) {
+ throw new SpelEvaluationException(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED, "#" + name);
+ }
+
@Override
public void setVariable(String name, @Nullable Object value) {
this.variables.put(name, value);
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java
index aafaf408ab1f..53912c038fe7 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java
@@ -25,6 +25,8 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.expression.AccessException;
import org.springframework.expression.BeanResolver;
@@ -36,6 +38,7 @@
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.expression.spel.testresources.TestPerson;
@@ -149,8 +152,24 @@ void mixingOperators() {
// assignment
@Test
- void assignmentToVariables() {
- evaluate("#var1='value1'", "value1", String.class);
+ void assignmentToVariableWithStandardEvaluationContext() {
+ evaluate("#var1 = 'value1'", "value1", String.class);
+ }
+
+ @ParameterizedTest
+ @CsvSource(delimiterString = "->", value = {
+ "'#var1 = \"value1\"' -> #var1",
+ "'true ? #myVar = 4 : 0' -> #myVar"
+ })
+ void assignmentToVariableWithSimpleEvaluationContext(String expression, String varName) {
+ EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
+ Expression expr = parser.parseExpression(expression);
+ assertThatExceptionOfType(SpelEvaluationException.class)
+ .isThrownBy(() -> expr.getValue(context))
+ .satisfies(ex -> {
+ assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED);
+ assertThat(ex.getInserts()).as("inserts").containsExactly(varName);
+ });
}
@Test