Document lifecycle and reuse contract for SpEL expressions and contexts

This commit adds a "Lifecycle and Reuse" section to the SpEL reference
documentation, immediately following the security considerations
introduced for gh-36997, explaining that AST nodes within a parsed
Expression may cache resolved PropertyAccessor, IndexAccessor,
MethodExecutor, and ConstructorExecutor instances for performance.

We also now document that reusing a parsed Expression across
EvaluationContext instances of the same type and with equivalent
configuration is supported (even if atypical), including when accessors
or resolvers registered with a context change between evaluations, but
that reusing a parsed Expression across contexts with different
security implications (for example, first against a
StandardEvaluationContext and later against a SimpleEvaluationContext)
is not supported, since cached state from a more permissive evaluation
may be reused during a more restrictive one.

The Javadoc for Expression, SpelExpression, EvaluationContext,
StandardEvaluationContext, SimpleEvaluationContext, PropertyAccessor,
IndexAccessor, MethodExecutor, and ConstructorExecutor has also been
updated to make these contracts discoverable via the API as well.

Closes gh-36968
This commit is contained in:
Sam Brannen
2026-07-29 22:05:15 +03:00
parent 9b42a40a2a
commit a894818c9e
10 changed files with 134 additions and 0 deletions
@@ -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.
*
* <p>A cached {@code ConstructorExecutor} is valid only for the
* {@link EvaluationContext} configuration &mdash; in particular, the registered
* {@link ConstructorResolver ConstructorResolvers} &mdash; 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
@@ -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.
*
* <p>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 &mdash; for example, first against a
* {@code StandardEvaluationContext} and later against a
* {@code SimpleEvaluationContext} &mdash; 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
@@ -27,6 +27,23 @@ import org.springframework.core.convert.TypeDescriptor;
*
* <p>Provides a common abstraction for expression evaluation.
*
* <p>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 &mdash; for example, a
* {@code PropertyAccessor}, {@code IndexAccessor}, {@code MethodExecutor}, or
* {@code ConstructorExecutor} &mdash; 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 &mdash; for
* example, first against a {@code StandardEvaluationContext} and later against a
* {@code SimpleEvaluationContext} &mdash; 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
@@ -30,6 +30,17 @@ import org.jspecify.annotations.Nullable;
* {@linkplain #getSpecificTargetClasses() target classes} for which it should be
* called. See {@link TargetedAccessor} for details.
*
* <p>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
@@ -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.
*
* <p>A cached {@code MethodExecutor} is valid only for the {@link EvaluationContext}
* configuration &mdash; in particular, the registered {@link MethodResolver
* MethodResolvers} &mdash; 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
@@ -30,6 +30,17 @@ import org.jspecify.annotations.Nullable;
* {@linkplain #getSpecificTargetClasses() target classes} for which it should be
* called. See {@link TargetedAccessor} for details.
*
* <p>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
@@ -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.
*
* <p>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
@@ -105,6 +105,16 @@ import org.springframework.expression.spel.SpelMessage;
* untrusted source. For details on what qualifies as a "trusted" source, see
* {@link StandardEvaluationContext}.
*
* <p>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
@@ -80,6 +80,15 @@ import org.springframework.util.Assert;
* regardless of the {@code EvaluationContext} implementation in use; see
* {@link SimpleEvaluationContext} for further details.
*
* <p>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