Revise SpEL internals and documentation

This is a prerequisite for null-safe Optional support.

See gh-20433
This commit is contained in:
Sam Brannen 2025-03-12 13:59:26 +01:00
parent d3d951e44b
commit 2c05e991b5
5 changed files with 66 additions and 81 deletions

View File

@ -252,7 +252,6 @@ Kotlin::
<1> Use "null-safe select first" operator on potentially null `members` list <1> Use "null-safe select first" operator on potentially null `members` list
====== ======
The following example shows how to use the "null-safe select last" operator for The following example shows how to use the "null-safe select last" operator for
collections (`?.$`). collections (`?.$`).

View File

@ -47,7 +47,8 @@ import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
* Expression language AST node that represents a method reference. * Expression language AST node that represents a method reference (i.e., a
* method invocation other than a simple property reference).
* *
* @author Andy Clement * @author Andy Clement
* @author Juergen Hoeller * @author Juergen Hoeller
@ -101,27 +102,28 @@ public class MethodReference extends SpelNodeImpl {
@Override @Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
EvaluationContext evaluationContext = state.getEvaluationContext(); EvaluationContext evaluationContext = state.getEvaluationContext();
Object value = state.getActiveContextObject().getValue(); TypedValue contextObject = state.getActiveContextObject();
TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor(); Object target = contextObject.getValue();
TypeDescriptor targetType = contextObject.getTypeDescriptor();
@Nullable Object[] arguments = getArguments(state); @Nullable Object[] arguments = getArguments(state);
TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments); TypedValue result = getValueInternal(evaluationContext, target, targetType, arguments);
updateExitTypeDescriptor(); updateExitTypeDescriptor();
return result; return result;
} }
private TypedValue getValueInternal(EvaluationContext evaluationContext, private TypedValue getValueInternal(EvaluationContext evaluationContext, @Nullable Object target,
@Nullable Object value, @Nullable TypeDescriptor targetType, @Nullable Object[] arguments) { @Nullable TypeDescriptor targetType, @Nullable Object[] arguments) {
List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments); List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
if (value == null) { if (target == null) {
throwIfNotNullSafe(argumentTypes); throwIfNotNullSafe(argumentTypes);
return TypedValue.NULL; return TypedValue.NULL;
} }
MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes); MethodExecutor executorToUse = getCachedExecutor(evaluationContext, target, targetType, argumentTypes);
if (executorToUse != null) { if (executorToUse != null) {
try { try {
return executorToUse.execute(evaluationContext, value, arguments); return executorToUse.execute(evaluationContext, target, arguments);
} }
catch (AccessException ex) { catch (AccessException ex) {
// Two reasons this can occur: // Two reasons this can occur:
@ -135,7 +137,7 @@ public class MethodReference extends SpelNodeImpl {
// To determine the situation, the AccessException will contain a cause. // To determine the situation, the AccessException will contain a cause.
// If the cause is an InvocationTargetException, a user exception was // If the cause is an InvocationTargetException, a user exception was
// thrown inside the method. Otherwise the method could not be invoked. // thrown inside the method. Otherwise the method could not be invoked.
throwSimpleExceptionIfPossible(value, ex); throwSimpleExceptionIfPossible(target, ex);
// At this point we know it wasn't a user problem so worth a retry if a // At this point we know it wasn't a user problem so worth a retry if a
// better candidate can be found. // better candidate can be found.
@ -144,18 +146,18 @@ public class MethodReference extends SpelNodeImpl {
} }
// either there was no accessor or it no longer existed // either there was no accessor or it no longer existed
executorToUse = findAccessorForMethod(argumentTypes, value, evaluationContext); executorToUse = findMethodExecutor(argumentTypes, target, evaluationContext);
this.cachedExecutor = new CachedMethodExecutor( this.cachedExecutor = new CachedMethodExecutor(
executorToUse, (value instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes); executorToUse, (target instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes);
try { try {
return executorToUse.execute(evaluationContext, value, arguments); return executorToUse.execute(evaluationContext, target, arguments);
} }
catch (AccessException ex) { catch (AccessException ex) {
// Same unwrapping exception handling as above in above catch block // Same unwrapping exception handling as in above catch block
throwSimpleExceptionIfPossible(value, ex); throwSimpleExceptionIfPossible(target, ex);
throw new SpelEvaluationException(getStartPosition(), ex, throw new SpelEvaluationException(getStartPosition(), ex,
SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name, SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name,
value.getClass().getName(), ex.getMessage()); target.getClass().getName(), ex.getMessage());
} }
} }
@ -190,8 +192,8 @@ public class MethodReference extends SpelNodeImpl {
return Collections.unmodifiableList(descriptors); return Collections.unmodifiableList(descriptors);
} }
private @Nullable MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value, private @Nullable MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object target,
@Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) { @Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers(); List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers();
if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) { if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) {
@ -200,21 +202,21 @@ public class MethodReference extends SpelNodeImpl {
} }
CachedMethodExecutor executorToCheck = this.cachedExecutor; CachedMethodExecutor executorToCheck = this.cachedExecutor;
if (executorToCheck != null && executorToCheck.isSuitable(value, target, argumentTypes)) { if (executorToCheck != null && executorToCheck.isSuitable(target, targetType, argumentTypes)) {
return executorToCheck.get(); return executorToCheck.get();
} }
this.cachedExecutor = null; this.cachedExecutor = null;
return null; return null;
} }
private MethodExecutor findAccessorForMethod(List<TypeDescriptor> argumentTypes, Object targetObject, private MethodExecutor findMethodExecutor(List<TypeDescriptor> argumentTypes, Object target,
EvaluationContext evaluationContext) throws SpelEvaluationException { EvaluationContext evaluationContext) throws SpelEvaluationException {
AccessException accessException = null; AccessException accessException = null;
for (MethodResolver methodResolver : evaluationContext.getMethodResolvers()) { for (MethodResolver methodResolver : evaluationContext.getMethodResolvers()) {
try { try {
MethodExecutor methodExecutor = methodResolver.resolve( MethodExecutor methodExecutor = methodResolver.resolve(
evaluationContext, targetObject, this.name, argumentTypes); evaluationContext, target, this.name, argumentTypes);
if (methodExecutor != null) { if (methodExecutor != null) {
return methodExecutor; return methodExecutor;
} }
@ -227,7 +229,7 @@ public class MethodReference extends SpelNodeImpl {
String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes); String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes);
String className = FormatHelper.formatClassNameForMessage( String className = FormatHelper.formatClassNameForMessage(
targetObject instanceof Class<?> clazz ? clazz : targetObject.getClass()); target instanceof Class<?> clazz ? clazz : target.getClass());
if (accessException != null) { if (accessException != null) {
throw new SpelEvaluationException( throw new SpelEvaluationException(
getStartPosition(), accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className); getStartPosition(), accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className);
@ -241,7 +243,7 @@ public class MethodReference extends SpelNodeImpl {
* Decode the AccessException, throwing a lightweight evaluation exception or, * Decode the AccessException, throwing a lightweight evaluation exception or,
* if the cause was a RuntimeException, throw the RuntimeException directly. * if the cause was a RuntimeException, throw the RuntimeException directly.
*/ */
private void throwSimpleExceptionIfPossible(Object value, AccessException ex) { private void throwSimpleExceptionIfPossible(Object target, AccessException ex) {
if (ex.getCause() instanceof InvocationTargetException cause) { if (ex.getCause() instanceof InvocationTargetException cause) {
Throwable rootCause = cause.getCause(); Throwable rootCause = cause.getCause();
if (rootCause instanceof RuntimeException runtimeException) { if (rootCause instanceof RuntimeException runtimeException) {
@ -249,7 +251,7 @@ public class MethodReference extends SpelNodeImpl {
} }
throw new ExpressionInvocationTargetException(getStartPosition(), throw new ExpressionInvocationTargetException(getStartPosition(),
"A problem occurred when trying to execute method '" + this.name + "A problem occurred when trying to execute method '" + this.name +
"' on object of type [" + value.getClass().getName() + "]", rootCause); "' on object of type [" + target.getClass().getName() + "]", rootCause);
} }
} }
@ -376,7 +378,7 @@ public class MethodReference extends SpelNodeImpl {
private final EvaluationContext evaluationContext; private final EvaluationContext evaluationContext;
private final @Nullable Object value; private final @Nullable Object target;
private final @Nullable TypeDescriptor targetType; private final @Nullable TypeDescriptor targetType;
@ -384,7 +386,7 @@ public class MethodReference extends SpelNodeImpl {
public MethodValueRef(ExpressionState state, @Nullable Object[] arguments) { public MethodValueRef(ExpressionState state, @Nullable Object[] arguments) {
this.evaluationContext = state.getEvaluationContext(); this.evaluationContext = state.getEvaluationContext();
this.value = state.getActiveContextObject().getValue(); this.target = state.getActiveContextObject().getValue();
this.targetType = state.getActiveContextObject().getTypeDescriptor(); this.targetType = state.getActiveContextObject().getTypeDescriptor();
this.arguments = arguments; this.arguments = arguments;
} }
@ -392,7 +394,7 @@ public class MethodReference extends SpelNodeImpl {
@Override @Override
public TypedValue getValue() { public TypedValue getValue() {
TypedValue result = MethodReference.this.getValueInternal( TypedValue result = MethodReference.this.getValueInternal(
this.evaluationContext, this.value, this.targetType, this.arguments); this.evaluationContext, this.target, this.targetType, this.arguments);
updateExitTypeDescriptor(); updateExitTypeDescriptor();
return result; return result;
} }
@ -409,32 +411,16 @@ public class MethodReference extends SpelNodeImpl {
} }
private static class CachedMethodExecutor { private record CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class<?> staticClass,
@Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
private final MethodExecutor methodExecutor; public boolean isSuitable(Object target, @Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
return ((this.staticClass == null || this.staticClass == target) &&
private final @Nullable Class<?> staticClass; ObjectUtils.nullSafeEquals(this.targetType, targetType) && this.argumentTypes.equals(argumentTypes));
private final @Nullable TypeDescriptor target;
private final List<TypeDescriptor> argumentTypes;
public CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class<?> staticClass,
@Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
this.methodExecutor = methodExecutor;
this.staticClass = staticClass;
this.target = target;
this.argumentTypes = argumentTypes;
}
public boolean isSuitable(Object value, @Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
return ((this.staticClass == null || this.staticClass == value) &&
ObjectUtils.nullSafeEquals(this.target, target) && this.argumentTypes.equals(argumentTypes));
} }
public boolean hasProxyTarget() { public boolean hasProxyTarget() {
return (this.target != null && Proxy.isProxyClass(this.target.getType())); return (this.targetType != null && Proxy.isProxyClass(this.targetType.getType()));
} }
public MethodExecutor get() { public MethodExecutor get() {

View File

@ -33,8 +33,8 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
* Represents projection, where a given operation is performed on all elements in some * Represents projection, where a given operation is performed on all elements in
* input sequence, returning a new sequence of the same size. * some input sequence, returning a new sequence of the same size.
* *
* <p>For example: <code>{1,2,3,4,5,6,7,8,9,10}.![#isEven(#this)]</code> evaluates * <p>For example: <code>{1,2,3,4,5,6,7,8,9,10}.![#isEven(#this)]</code> evaluates
* to {@code [n, y, n, y, n, y, n, y, n, y]}. * to {@code [n, y, n, y, n, y, n, y, n, y]}.
@ -72,8 +72,8 @@ public class Projection extends SpelNodeImpl {
@Override @Override
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
TypedValue op = state.getActiveContextObject(); TypedValue contextObject = state.getActiveContextObject();
Object operand = op.getValue(); Object operand = contextObject.getValue();
// When the input is a map, we push a Map.Entry on the stack before calling // When the input is a map, we push a Map.Entry on the stack before calling
// the specified operation. Map.Entry has two properties 'key' and 'value' // the specified operation. Map.Entry has two properties 'key' and 'value'

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -179,8 +179,8 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name) private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
throws EvaluationException { throws EvaluationException {
Object targetObject = contextObject.getValue(); Object target = contextObject.getValue();
if (targetObject == null && isNullSafe()) { if (target == null && isNullSafe()) {
return TypedValue.NULL; return TypedValue.NULL;
} }
@ -188,7 +188,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) { if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try { try {
return accessorToUse.read(evalContext, targetObject, name); return accessorToUse.read(evalContext, target, name);
} }
catch (Exception ex) { catch (Exception ex) {
// This is OK - it may have gone stale due to a class change, // This is OK - it may have gone stale due to a class change,
@ -199,19 +199,19 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
} }
List<PropertyAccessor> accessorsToTry = List<PropertyAccessor> accessorsToTry =
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
// Go through the accessors that may be able to resolve it. If they are a cacheable accessor then // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then
// get the accessor and use it. If they are not cacheable but report they can read the property // get the accessor and use it. If they are not cacheable but report they can read the property
// then ask them to read it // then ask them to read it.
try { try {
for (PropertyAccessor accessor : accessorsToTry) { for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canRead(evalContext, targetObject, name)) { if (accessor.canRead(evalContext, target, name)) {
if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) { if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) {
accessor = reflectivePropertyAccessor.createOptimalAccessor( accessor = reflectivePropertyAccessor.createOptimalAccessor(
evalContext, targetObject, name); evalContext, target, name);
} }
this.cachedReadAccessor = accessor; this.cachedReadAccessor = accessor;
return accessor.read(evalContext, targetObject, name); return accessor.read(evalContext, target, name);
} }
} }
} }
@ -232,8 +232,8 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue) TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue)
throws EvaluationException { throws EvaluationException {
Object targetObject = contextObject.getValue(); Object target = contextObject.getValue();
if (targetObject == null) { if (target == null) {
if (isNullSafe()) { if (isNullSafe()) {
return; return;
} }
@ -245,7 +245,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) { if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try { try {
accessorToUse.write(evalContext, targetObject, name, newValue); accessorToUse.write(evalContext, target, name, newValue);
return; return;
} }
catch (Exception ex) { catch (Exception ex) {
@ -257,12 +257,12 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
} }
List<PropertyAccessor> accessorsToTry = List<PropertyAccessor> accessorsToTry =
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
try { try {
for (PropertyAccessor accessor : accessorsToTry) { for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canWrite(evalContext, targetObject, name)) { if (accessor.canWrite(evalContext, target, name)) {
this.cachedWriteAccessor = accessor; this.cachedWriteAccessor = accessor;
accessor.write(evalContext, targetObject, name, newValue); accessor.write(evalContext, target, name, newValue);
return; return;
} }
} }
@ -273,19 +273,19 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
} }
throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name, throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name,
FormatHelper.formatClassNameForMessage(getObjectClass(targetObject))); FormatHelper.formatClassNameForMessage(getObjectClass(target)));
} }
public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext) public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext)
throws EvaluationException { throws EvaluationException {
Object targetObject = contextObject.getValue(); Object target = contextObject.getValue();
if (targetObject != null) { if (target != null) {
List<PropertyAccessor> accessorsToTry = List<PropertyAccessor> accessorsToTry =
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
for (PropertyAccessor accessor : accessorsToTry) { for (PropertyAccessor accessor : accessorsToTry) {
try { try {
if (accessor.canWrite(evalContext, targetObject, name)) { if (accessor.canWrite(evalContext, target, name)) {
return true; return true;
} }
} }

View File

@ -35,7 +35,7 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
* Represents selection over a map or collection. * Represents selection over a {@link Map}, {@link Iterable}, or array.
* *
* <p>For example, <code>{1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this)]</code> evaluates * <p>For example, <code>{1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this)]</code> evaluates
* to {@code [2, 4, 6, 8, 10]}. * to {@code [2, 4, 6, 8, 10]}.
@ -94,8 +94,8 @@ public class Selection extends SpelNodeImpl {
@Override @Override
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
TypedValue op = state.getActiveContextObject(); TypedValue contextObject = state.getActiveContextObject();
Object operand = op.getValue(); Object operand = contextObject.getValue();
SpelNodeImpl selectionCriteria = this.children[0]; SpelNodeImpl selectionCriteria = this.children[0];
if (operand instanceof Map<?, ?> mapdata) { if (operand instanceof Map<?, ?> mapdata) {
@ -151,9 +151,9 @@ public class Selection extends SpelNodeImpl {
try { try {
state.pushActiveContextObject(new TypedValue(element)); state.pushActiveContextObject(new TypedValue(element));
state.enterScope(); state.enterScope();
Object val = selectionCriteria.getValueInternal(state).getValue(); Object criteria = selectionCriteria.getValueInternal(state).getValue();
if (val instanceof Boolean b) { if (criteria instanceof Boolean match) {
if (b) { if (match) {
if (this.variant == FIRST) { if (this.variant == FIRST) {
return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this); return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this);
} }
@ -184,7 +184,7 @@ public class Selection extends SpelNodeImpl {
} }
Class<?> elementType = null; Class<?> elementType = null;
TypeDescriptor typeDesc = op.getTypeDescriptor(); TypeDescriptor typeDesc = contextObject.getTypeDescriptor();
if (typeDesc != null) { if (typeDesc != null) {
TypeDescriptor elementTypeDesc = typeDesc.getElementTypeDescriptor(); TypeDescriptor elementTypeDesc = typeDesc.getElementTypeDescriptor();
if (elementTypeDesc != null) { if (elementTypeDesc != null) {