diff --git a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc index e7bee3b66b6..f2ba820702d 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc @@ -286,6 +286,46 @@ registering property accessors, resolvers, variables, or functions – to ensure of the objects reachable via the context expose operations that would be dangerous if invoked by an expression from an untrusted source. +[[expressions-evaluation-context-lifecycle]] +=== Lifecycle and Reuse + +For performance, the AST nodes that make up a parsed `Expression` may cache the specific +`PropertyAccessor`, `IndexAccessor`, `MethodExecutor`, or `ConstructorExecutor` that +satisfied a previous evaluation, so that later evaluations of the same node can avoid +asking every registered accessor or resolver in turn. Understanding this caching behavior +is essential to using `Expression` and `EvaluationContext` correctly, in addition to the +<> discussed previously. + +A parsed `Expression` is designed to be created once and evaluated repeatedly, and doing +so is both supported and encouraged. In particular: + +* A parsed `Expression` may be evaluated against different root objects, and against + different `EvaluationContext` instances of the *same type and with equivalent + configuration* – for example, several `StandardEvaluationContext` instances each + registering the same kind of custom `PropertyAccessor`. Changing the accessors or + resolvers registered with a context between evaluations of the same expression is + atypical and generally not advised, but is expected to work correctly: the registered + state of the *current* context is what is consulted, not a snapshot taken during an + earlier evaluation. +* A parsed `Expression` must *not* be evaluated first against a context with one set of + security implications and later against a context with different, typically more + restrictive, security implications – for example, first against a + `StandardEvaluationContext` and later against a `SimpleEvaluationContext`. Doing so is + analogous to executing a database query on behalf of an administrator, caching the + resulting administrator-privileged execution plan, and then reusing that cached plan for + a lower-privileged user while expecting the lower-privileged user's restrictions to + apply: cached state from the first, more permissive evaluation may be reused during the + second, and the second context's restrictions cannot be reliably enforced as a result. + If the same expression string must be evaluated under contexts with different security + implications, parse it into *distinct* `Expression` instances, one per context. + +[WARNING] +==== +Reusing a single parsed `Expression` across `EvaluationContext` instances with different +security implications is not a supported usage pattern and must be avoided, regardless of +which `EvaluationContext` implementations are involved. +==== + [[expressions-type-conversion]] === Type Conversion diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java index ec5a8c7d71b..07959d51c4a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java @@ -33,6 +33,12 @@ import org.jspecify.annotations.Nullable; * {@link AccessException} which signals to the infrastructure to go back to the * resolvers to ask for a new one. * + *

A cached {@code ConstructorExecutor} is valid only for the + * {@link EvaluationContext} configuration — in particular, the registered + * {@link ConstructorResolver ConstructorResolvers} — under which it was resolved. + * See {@link Expression} for the resulting contract on reusing a parsed expression + * across different {@code EvaluationContext} instances. + * * @author Andy Clement * @author Sam Brannen * @since 3.0 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 3e807bdac53..e6406a111be 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -56,6 +56,18 @@ import org.jspecify.annotations.Nullable; * those reachable objects expose operations that would be dangerous if invoked * by an expression from an untrusted source. * + *

An {@code EvaluationContext} is designed to be built once and reused across + * many evaluations, potentially of many different {@link Expression} instances. A + * single {@link Expression}, in turn, may cache accessor and executor state resolved + * against a particular {@code EvaluationContext} configuration. Reusing an + * {@code Expression} across {@code EvaluationContext} instances of the same type and + * with equivalent configuration is supported; reusing an {@code Expression} across + * contexts with different security implications — for example, first against a + * {@code StandardEvaluationContext} and later against a + * {@code SimpleEvaluationContext} — is not, since cached state from the more + * permissive evaluation may be reused during the more restrictive one. See + * {@link Expression} for details. + * * @author Andy Clement * @author Juergen Hoeller * @author Sam Brannen diff --git a/spring-expression/src/main/java/org/springframework/expression/Expression.java b/spring-expression/src/main/java/org/springframework/expression/Expression.java index c6cb98baa9d..b9a8ce10971 100644 --- a/spring-expression/src/main/java/org/springframework/expression/Expression.java +++ b/spring-expression/src/main/java/org/springframework/expression/Expression.java @@ -27,6 +27,23 @@ import org.springframework.core.convert.TypeDescriptor; * *

Provides a common abstraction for expression evaluation. * + *

An {@code Expression} is intended to be parsed once and evaluated repeatedly, + * potentially against different root objects and different {@link EvaluationContext} + * instances. For performance, an {@code Expression} implementation may internally + * cache the specific accessor or executor — for example, a + * {@code PropertyAccessor}, {@code IndexAccessor}, {@code MethodExecutor}, or + * {@code ConstructorExecutor} — that satisfied a previous evaluation, in order + * to avoid repeatedly asking every candidate registered with the context. Reusing a + * parsed {@code Expression} across {@code EvaluationContext} instances of the same + * type and with equivalent configuration is supported; however, reusing a parsed + * {@code Expression} across contexts with different security implications — for + * example, first against a {@code StandardEvaluationContext} and later against a + * {@code SimpleEvaluationContext} — is not supported, since cached state from a + * more permissive evaluation may be reused during a subsequent, more restrictive + * evaluation. If the same expression string must be evaluated under contexts with + * different security implications, parse it into distinct {@code Expression} + * instances, one per context. + * * @author Keith Donald * @author Andy Clement * @author Juergen Hoeller diff --git a/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java b/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java index da6c0252fe3..a0db10d051a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java @@ -30,6 +30,17 @@ import org.jspecify.annotations.Nullable; * {@linkplain #getSpecificTargetClasses() target classes} for which it should be * called. See {@link TargetedAccessor} for details. * + *

SpEL infrastructure may cache an {@code IndexAccessor} instance on a per-node + * basis once it has successfully served a read or write for a given index and target + * type, in order to avoid repeatedly invoking {@link #canRead}/{@link #canWrite} on + * every registered accessor for subsequent evaluations of that node. Before reusing a + * cached instance, SpEL confirms that it is still registered with the current + * {@link EvaluationContext}. Consequently, {@link #canRead}, {@link #canWrite}, + * {@link #read}, and {@link #write} should behave consistently for a given + * {@code (context, target type, index)} combination for as long as the accessor remains + * registered with a context, since a change in behavior may not be observed until the + * accessor is re-selected from scratch. + * * @author Jackmiking Lee * @author Sam Brannen * @since 6.2 diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java index 7c804dfbf8f..2a3919864cb 100644 --- a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java @@ -32,6 +32,12 @@ import org.jspecify.annotations.Nullable; * {@link AccessException} which signals to the infrastructure to go back to the * resolvers to ask for a new one. * + *

A cached {@code MethodExecutor} is valid only for the {@link EvaluationContext} + * configuration — in particular, the registered {@link MethodResolver + * MethodResolvers} — under which it was resolved. See {@link Expression} for the + * resulting contract on reusing a parsed expression across different + * {@code EvaluationContext} instances. + * * @author Andy Clement * @author Sam Brannen * @since 3.0 diff --git a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java index c23252b31b4..aacbcd90f3a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java @@ -30,6 +30,17 @@ import org.jspecify.annotations.Nullable; * {@linkplain #getSpecificTargetClasses() target classes} for which it should be * called. See {@link TargetedAccessor} for details. * + *

SpEL infrastructure may cache a {@code PropertyAccessor} instance on a per-node + * basis once it has successfully served a read or write for a given property name and + * target type, in order to avoid repeatedly invoking {@link #canRead}/{@link #canWrite} + * on every registered accessor for subsequent evaluations of that node. Before reusing + * a cached instance, SpEL confirms that it is still registered with the current + * {@link EvaluationContext}. Consequently, {@link #canRead}, {@link #canWrite}, + * {@link #read}, and {@link #write} should behave consistently for a given + * {@code (context, target type, name)} combination for as long as the accessor remains + * registered with a context, since a change in behavior may not be observed until the + * accessor is re-selected from scratch. + * * @author Andy Clement * @since 3.0 * @see TargetedAccessor diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java index 4506b507b55..87133da0779 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java @@ -43,6 +43,18 @@ import org.springframework.util.Assert; * specified context. During expression evaluation the context may be asked to resolve * references to types, beans, properties, and methods. * + *

The individual nodes of the parsed AST may cache the specific + * {@link org.springframework.expression.PropertyAccessor PropertyAccessor}, + * {@link org.springframework.expression.IndexAccessor IndexAccessor}, + * {@link org.springframework.expression.MethodExecutor MethodExecutor}, or + * {@link org.springframework.expression.ConstructorExecutor ConstructorExecutor} that + * satisfied a previous evaluation of that node. On a subsequent evaluation, the current + * {@code EvaluationContext} is consulted to confirm that the cached accessor or executor + * (or, in some cases, the resolver that produced it) is still applicable before it is + * reused; if it is not, resolution is performed again from scratch. See {@link Expression} + * for the resulting contract on reusing a {@code SpelExpression} across different + * {@code EvaluationContext} instances. + * * @author Andy Clement * @author Juergen Hoeller * @author Sam Brannen 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 1e6931daa4c..e7023eabf0f 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 @@ -105,6 +105,16 @@ import org.springframework.expression.spel.SpelMessage; * untrusted source. For details on what qualifies as a "trusted" source, see * {@link StandardEvaluationContext}. * + *

Because a parsed {@code Expression} may cache accessor and executor state + * resolved against a particular {@code EvaluationContext} configuration, a parsed + * {@code Expression} must never be evaluated first against a + * {@code StandardEvaluationContext} (or any other, less restrictive, + * {@code EvaluationContext}) and later against a {@code SimpleEvaluationContext}. If the + * same expression string must be evaluated under both kinds of contexts, parse it into + * two distinct {@code Expression} instances instead. See + * {@link org.springframework.expression.Expression Expression} for further details on + * this lifecycle contract. + * * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sam Brannen diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 01155813b3e..469ce92df03 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -80,6 +80,15 @@ import org.springframework.util.Assert; * regardless of the {@code EvaluationContext} implementation in use; see * {@link SimpleEvaluationContext} for further details. * + *

Because a parsed {@code Expression} may cache accessor and executor state + * resolved against a particular {@code EvaluationContext} configuration, a parsed + * {@code Expression} must never be evaluated first against a + * {@code StandardEvaluationContext} and later against a {@code SimpleEvaluationContext} + * (or any other, more restrictive {@code EvaluationContext}). If the same expression + * string must be evaluated under both kinds of contexts, parse it into two distinct + * {@code Expression} instances instead. See {@link org.springframework.expression.Expression + * Expression} for further details on this lifecycle contract. + * * @author Andy Clement * @author Juergen Hoeller * @author Sam Brannen