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
======
The following example shows how to use the "null-safe select last" operator for
collections (`?.$`).

View File

@ -47,7 +47,8 @@ import org.springframework.util.Assert;
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 Juergen Hoeller
@ -101,27 +102,28 @@ public class MethodReference extends SpelNodeImpl {
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
EvaluationContext evaluationContext = state.getEvaluationContext();
Object value = state.getActiveContextObject().getValue();
TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor();
TypedValue contextObject = state.getActiveContextObject();
Object target = contextObject.getValue();
TypeDescriptor targetType = contextObject.getTypeDescriptor();
@Nullable Object[] arguments = getArguments(state);
TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments);
TypedValue result = getValueInternal(evaluationContext, target, targetType, arguments);
updateExitTypeDescriptor();
return result;
}
private TypedValue getValueInternal(EvaluationContext evaluationContext,
@Nullable Object value, @Nullable TypeDescriptor targetType, @Nullable Object[] arguments) {
private TypedValue getValueInternal(EvaluationContext evaluationContext, @Nullable Object target,
@Nullable TypeDescriptor targetType, @Nullable Object[] arguments) {
List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
if (value == null) {
if (target == null) {
throwIfNotNullSafe(argumentTypes);
return TypedValue.NULL;
}
MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes);
MethodExecutor executorToUse = getCachedExecutor(evaluationContext, target, targetType, argumentTypes);
if (executorToUse != null) {
try {
return executorToUse.execute(evaluationContext, value, arguments);
return executorToUse.execute(evaluationContext, target, arguments);
}
catch (AccessException ex) {
// Two reasons this can occur:
@ -135,7 +137,7 @@ public class MethodReference extends SpelNodeImpl {
// To determine the situation, the AccessException will contain a cause.
// If the cause is an InvocationTargetException, a user exception was
// 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
// better candidate can be found.
@ -144,18 +146,18 @@ public class MethodReference extends SpelNodeImpl {
}
// either there was no accessor or it no longer existed
executorToUse = findAccessorForMethod(argumentTypes, value, evaluationContext);
executorToUse = findMethodExecutor(argumentTypes, target, evaluationContext);
this.cachedExecutor = new CachedMethodExecutor(
executorToUse, (value instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes);
executorToUse, (target instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes);
try {
return executorToUse.execute(evaluationContext, value, arguments);
return executorToUse.execute(evaluationContext, target, arguments);
}
catch (AccessException ex) {
// Same unwrapping exception handling as above in above catch block
throwSimpleExceptionIfPossible(value, ex);
// Same unwrapping exception handling as in above catch block
throwSimpleExceptionIfPossible(target, ex);
throw new SpelEvaluationException(getStartPosition(), ex,
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);
}
private @Nullable MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value,
@Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
private @Nullable MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object target,
@Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers();
if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) {
@ -200,21 +202,21 @@ public class MethodReference extends SpelNodeImpl {
}
CachedMethodExecutor executorToCheck = this.cachedExecutor;
if (executorToCheck != null && executorToCheck.isSuitable(value, target, argumentTypes)) {
if (executorToCheck != null && executorToCheck.isSuitable(target, targetType, argumentTypes)) {
return executorToCheck.get();
}
this.cachedExecutor = null;
return null;
}
private MethodExecutor findAccessorForMethod(List<TypeDescriptor> argumentTypes, Object targetObject,
private MethodExecutor findMethodExecutor(List<TypeDescriptor> argumentTypes, Object target,
EvaluationContext evaluationContext) throws SpelEvaluationException {
AccessException accessException = null;
for (MethodResolver methodResolver : evaluationContext.getMethodResolvers()) {
try {
MethodExecutor methodExecutor = methodResolver.resolve(
evaluationContext, targetObject, this.name, argumentTypes);
evaluationContext, target, this.name, argumentTypes);
if (methodExecutor != null) {
return methodExecutor;
}
@ -227,7 +229,7 @@ public class MethodReference extends SpelNodeImpl {
String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes);
String className = FormatHelper.formatClassNameForMessage(
targetObject instanceof Class<?> clazz ? clazz : targetObject.getClass());
target instanceof Class<?> clazz ? clazz : target.getClass());
if (accessException != null) {
throw new SpelEvaluationException(
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,
* 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) {
Throwable rootCause = cause.getCause();
if (rootCause instanceof RuntimeException runtimeException) {
@ -249,7 +251,7 @@ public class MethodReference extends SpelNodeImpl {
}
throw new ExpressionInvocationTargetException(getStartPosition(),
"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 @Nullable Object value;
private final @Nullable Object target;
private final @Nullable TypeDescriptor targetType;
@ -384,7 +386,7 @@ public class MethodReference extends SpelNodeImpl {
public MethodValueRef(ExpressionState state, @Nullable Object[] arguments) {
this.evaluationContext = state.getEvaluationContext();
this.value = state.getActiveContextObject().getValue();
this.target = state.getActiveContextObject().getValue();
this.targetType = state.getActiveContextObject().getTypeDescriptor();
this.arguments = arguments;
}
@ -392,7 +394,7 @@ public class MethodReference extends SpelNodeImpl {
@Override
public TypedValue getValue() {
TypedValue result = MethodReference.this.getValueInternal(
this.evaluationContext, this.value, this.targetType, this.arguments);
this.evaluationContext, this.target, this.targetType, this.arguments);
updateExitTypeDescriptor();
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;
private final @Nullable Class<?> staticClass;
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 isSuitable(Object target, @Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
return ((this.staticClass == null || this.staticClass == target) &&
ObjectUtils.nullSafeEquals(this.targetType, targetType) && this.argumentTypes.equals(argumentTypes));
}
public boolean hasProxyTarget() {
return (this.target != null && Proxy.isProxyClass(this.target.getType()));
return (this.targetType != null && Proxy.isProxyClass(this.targetType.getType()));
}
public MethodExecutor get() {

View File

@ -33,8 +33,8 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Represents projection, where a given operation is performed on all elements in some
* input sequence, returning a new sequence of the same size.
* Represents projection, where a given operation is performed on all elements in
* 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
* to {@code [n, y, n, y, n, y, n, y, n, y]}.
@ -72,8 +72,8 @@ public class Projection extends SpelNodeImpl {
@Override
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
TypedValue op = state.getActiveContextObject();
Object operand = op.getValue();
TypedValue contextObject = state.getActiveContextObject();
Object operand = contextObject.getValue();
// 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'

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");
* 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)
throws EvaluationException {
Object targetObject = contextObject.getValue();
if (targetObject == null && isNullSafe()) {
Object target = contextObject.getValue();
if (target == null && isNullSafe()) {
return TypedValue.NULL;
}
@ -188,7 +188,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try {
return accessorToUse.read(evalContext, targetObject, name);
return accessorToUse.read(evalContext, target, name);
}
catch (Exception ex) {
// 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 =
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
// 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 {
for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canRead(evalContext, targetObject, name)) {
if (accessor.canRead(evalContext, target, name)) {
if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) {
accessor = reflectivePropertyAccessor.createOptimalAccessor(
evalContext, targetObject, name);
evalContext, target, name);
}
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)
throws EvaluationException {
Object targetObject = contextObject.getValue();
if (targetObject == null) {
Object target = contextObject.getValue();
if (target == null) {
if (isNullSafe()) {
return;
}
@ -245,7 +245,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
if (accessorToUse != null) {
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
try {
accessorToUse.write(evalContext, targetObject, name, newValue);
accessorToUse.write(evalContext, target, name, newValue);
return;
}
catch (Exception ex) {
@ -257,12 +257,12 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
}
List<PropertyAccessor> accessorsToTry =
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors());
AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
try {
for (PropertyAccessor accessor : accessorsToTry) {
if (accessor.canWrite(evalContext, targetObject, name)) {
if (accessor.canWrite(evalContext, target, name)) {
this.cachedWriteAccessor = accessor;
accessor.write(evalContext, targetObject, name, newValue);
accessor.write(evalContext, target, name, newValue);
return;
}
}
@ -273,19 +273,19 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
}
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)
throws EvaluationException {
Object targetObject = contextObject.getValue();
if (targetObject != null) {
Object target = contextObject.getValue();
if (target != null) {
List<PropertyAccessor> accessorsToTry =
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors());
AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
for (PropertyAccessor accessor : accessorsToTry) {
try {
if (accessor.canWrite(evalContext, targetObject, name)) {
if (accessor.canWrite(evalContext, target, name)) {
return true;
}
}

View File

@ -35,7 +35,7 @@ import org.springframework.util.CollectionUtils;
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
* to {@code [2, 4, 6, 8, 10]}.
@ -94,8 +94,8 @@ public class Selection extends SpelNodeImpl {
@Override
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
TypedValue op = state.getActiveContextObject();
Object operand = op.getValue();
TypedValue contextObject = state.getActiveContextObject();
Object operand = contextObject.getValue();
SpelNodeImpl selectionCriteria = this.children[0];
if (operand instanceof Map<?, ?> mapdata) {
@ -151,9 +151,9 @@ public class Selection extends SpelNodeImpl {
try {
state.pushActiveContextObject(new TypedValue(element));
state.enterScope();
Object val = selectionCriteria.getValueInternal(state).getValue();
if (val instanceof Boolean b) {
if (b) {
Object criteria = selectionCriteria.getValueInternal(state).getValue();
if (criteria instanceof Boolean match) {
if (match) {
if (this.variant == FIRST) {
return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this);
}
@ -184,7 +184,7 @@ public class Selection extends SpelNodeImpl {
}
Class<?> elementType = null;
TypeDescriptor typeDesc = op.getTypeDescriptor();
TypeDescriptor typeDesc = contextObject.getTypeDescriptor();
if (typeDesc != null) {
TypeDescriptor elementTypeDesc = typeDesc.getElementTypeDescriptor();
if (elementTypeDesc != null) {