Skip to content

Commit 2c05e99

Browse files
committed
Revise SpEL internals and documentation
This is a prerequisite for null-safe Optional support. See gh-20433
1 parent d3d951e commit 2c05e99

File tree

5 files changed

+66
-81
lines changed

5 files changed

+66
-81
lines changed

Diff for: framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc

-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ Kotlin::
252252
<1> Use "null-safe select first" operator on potentially null `members` list
253253
======
254254

255-
256255
The following example shows how to use the "null-safe select last" operator for
257256
collections (`?.$`).
258257

Diff for: spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java

+35-49
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
import org.springframework.util.ObjectUtils;
4848

4949
/**
50-
* Expression language AST node that represents a method reference.
50+
* Expression language AST node that represents a method reference (i.e., a
51+
* method invocation other than a simple property reference).
5152
*
5253
* @author Andy Clement
5354
* @author Juergen Hoeller
@@ -101,27 +102,28 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
101102
@Override
102103
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
103104
EvaluationContext evaluationContext = state.getEvaluationContext();
104-
Object value = state.getActiveContextObject().getValue();
105-
TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor();
105+
TypedValue contextObject = state.getActiveContextObject();
106+
Object target = contextObject.getValue();
107+
TypeDescriptor targetType = contextObject.getTypeDescriptor();
106108
@Nullable Object[] arguments = getArguments(state);
107-
TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments);
109+
TypedValue result = getValueInternal(evaluationContext, target, targetType, arguments);
108110
updateExitTypeDescriptor();
109111
return result;
110112
}
111113

112-
private TypedValue getValueInternal(EvaluationContext evaluationContext,
113-
@Nullable Object value, @Nullable TypeDescriptor targetType, @Nullable Object[] arguments) {
114+
private TypedValue getValueInternal(EvaluationContext evaluationContext, @Nullable Object target,
115+
@Nullable TypeDescriptor targetType, @Nullable Object[] arguments) {
114116

115117
List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
116-
if (value == null) {
118+
if (target == null) {
117119
throwIfNotNullSafe(argumentTypes);
118120
return TypedValue.NULL;
119121
}
120122

121-
MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes);
123+
MethodExecutor executorToUse = getCachedExecutor(evaluationContext, target, targetType, argumentTypes);
122124
if (executorToUse != null) {
123125
try {
124-
return executorToUse.execute(evaluationContext, value, arguments);
126+
return executorToUse.execute(evaluationContext, target, arguments);
125127
}
126128
catch (AccessException ex) {
127129
// Two reasons this can occur:
@@ -135,7 +137,7 @@ private TypedValue getValueInternal(EvaluationContext evaluationContext,
135137
// To determine the situation, the AccessException will contain a cause.
136138
// If the cause is an InvocationTargetException, a user exception was
137139
// thrown inside the method. Otherwise the method could not be invoked.
138-
throwSimpleExceptionIfPossible(value, ex);
140+
throwSimpleExceptionIfPossible(target, ex);
139141

140142
// At this point we know it wasn't a user problem so worth a retry if a
141143
// better candidate can be found.
@@ -144,18 +146,18 @@ private TypedValue getValueInternal(EvaluationContext evaluationContext,
144146
}
145147

146148
// either there was no accessor or it no longer existed
147-
executorToUse = findAccessorForMethod(argumentTypes, value, evaluationContext);
149+
executorToUse = findMethodExecutor(argumentTypes, target, evaluationContext);
148150
this.cachedExecutor = new CachedMethodExecutor(
149-
executorToUse, (value instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes);
151+
executorToUse, (target instanceof Class<?> clazz ? clazz : null), targetType, argumentTypes);
150152
try {
151-
return executorToUse.execute(evaluationContext, value, arguments);
153+
return executorToUse.execute(evaluationContext, target, arguments);
152154
}
153155
catch (AccessException ex) {
154-
// Same unwrapping exception handling as above in above catch block
155-
throwSimpleExceptionIfPossible(value, ex);
156+
// Same unwrapping exception handling as in above catch block
157+
throwSimpleExceptionIfPossible(target, ex);
156158
throw new SpelEvaluationException(getStartPosition(), ex,
157159
SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name,
158-
value.getClass().getName(), ex.getMessage());
160+
target.getClass().getName(), ex.getMessage());
159161
}
160162
}
161163

@@ -190,8 +192,8 @@ private List<TypeDescriptor> getArgumentTypes(@Nullable Object... arguments) {
190192
return Collections.unmodifiableList(descriptors);
191193
}
192194

193-
private @Nullable MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value,
194-
@Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
195+
private @Nullable MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object target,
196+
@Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
195197

196198
List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers();
197199
if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) {
@@ -200,21 +202,21 @@ private List<TypeDescriptor> getArgumentTypes(@Nullable Object... arguments) {
200202
}
201203

202204
CachedMethodExecutor executorToCheck = this.cachedExecutor;
203-
if (executorToCheck != null && executorToCheck.isSuitable(value, target, argumentTypes)) {
205+
if (executorToCheck != null && executorToCheck.isSuitable(target, targetType, argumentTypes)) {
204206
return executorToCheck.get();
205207
}
206208
this.cachedExecutor = null;
207209
return null;
208210
}
209211

210-
private MethodExecutor findAccessorForMethod(List<TypeDescriptor> argumentTypes, Object targetObject,
212+
private MethodExecutor findMethodExecutor(List<TypeDescriptor> argumentTypes, Object target,
211213
EvaluationContext evaluationContext) throws SpelEvaluationException {
212214

213215
AccessException accessException = null;
214216
for (MethodResolver methodResolver : evaluationContext.getMethodResolvers()) {
215217
try {
216218
MethodExecutor methodExecutor = methodResolver.resolve(
217-
evaluationContext, targetObject, this.name, argumentTypes);
219+
evaluationContext, target, this.name, argumentTypes);
218220
if (methodExecutor != null) {
219221
return methodExecutor;
220222
}
@@ -227,7 +229,7 @@ private MethodExecutor findAccessorForMethod(List<TypeDescriptor> argumentTypes,
227229

228230
String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes);
229231
String className = FormatHelper.formatClassNameForMessage(
230-
targetObject instanceof Class<?> clazz ? clazz : targetObject.getClass());
232+
target instanceof Class<?> clazz ? clazz : target.getClass());
231233
if (accessException != null) {
232234
throw new SpelEvaluationException(
233235
getStartPosition(), accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className);
@@ -241,15 +243,15 @@ private MethodExecutor findAccessorForMethod(List<TypeDescriptor> argumentTypes,
241243
* Decode the AccessException, throwing a lightweight evaluation exception or,
242244
* if the cause was a RuntimeException, throw the RuntimeException directly.
243245
*/
244-
private void throwSimpleExceptionIfPossible(Object value, AccessException ex) {
246+
private void throwSimpleExceptionIfPossible(Object target, AccessException ex) {
245247
if (ex.getCause() instanceof InvocationTargetException cause) {
246248
Throwable rootCause = cause.getCause();
247249
if (rootCause instanceof RuntimeException runtimeException) {
248250
throw runtimeException;
249251
}
250252
throw new ExpressionInvocationTargetException(getStartPosition(),
251253
"A problem occurred when trying to execute method '" + this.name +
252-
"' on object of type [" + value.getClass().getName() + "]", rootCause);
254+
"' on object of type [" + target.getClass().getName() + "]", rootCause);
253255
}
254256
}
255257

@@ -376,23 +378,23 @@ private class MethodValueRef implements ValueRef {
376378

377379
private final EvaluationContext evaluationContext;
378380

379-
private final @Nullable Object value;
381+
private final @Nullable Object target;
380382

381383
private final @Nullable TypeDescriptor targetType;
382384

383385
private final @Nullable Object[] arguments;
384386

385387
public MethodValueRef(ExpressionState state, @Nullable Object[] arguments) {
386388
this.evaluationContext = state.getEvaluationContext();
387-
this.value = state.getActiveContextObject().getValue();
389+
this.target = state.getActiveContextObject().getValue();
388390
this.targetType = state.getActiveContextObject().getTypeDescriptor();
389391
this.arguments = arguments;
390392
}
391393

392394
@Override
393395
public TypedValue getValue() {
394396
TypedValue result = MethodReference.this.getValueInternal(
395-
this.evaluationContext, this.value, this.targetType, this.arguments);
397+
this.evaluationContext, this.target, this.targetType, this.arguments);
396398
updateExitTypeDescriptor();
397399
return result;
398400
}
@@ -409,32 +411,16 @@ public boolean isWritable() {
409411
}
410412

411413

412-
private static class CachedMethodExecutor {
414+
private record CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class<?> staticClass,
415+
@Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
413416

414-
private final MethodExecutor methodExecutor;
415-
416-
private final @Nullable Class<?> staticClass;
417-
418-
private final @Nullable TypeDescriptor target;
419-
420-
private final List<TypeDescriptor> argumentTypes;
421-
422-
public CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class<?> staticClass,
423-
@Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
424-
425-
this.methodExecutor = methodExecutor;
426-
this.staticClass = staticClass;
427-
this.target = target;
428-
this.argumentTypes = argumentTypes;
429-
}
430-
431-
public boolean isSuitable(Object value, @Nullable TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
432-
return ((this.staticClass == null || this.staticClass == value) &&
433-
ObjectUtils.nullSafeEquals(this.target, target) && this.argumentTypes.equals(argumentTypes));
417+
public boolean isSuitable(Object target, @Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
418+
return ((this.staticClass == null || this.staticClass == target) &&
419+
ObjectUtils.nullSafeEquals(this.targetType, targetType) && this.argumentTypes.equals(argumentTypes));
434420
}
435421

436422
public boolean hasProxyTarget() {
437-
return (this.target != null && Proxy.isProxyClass(this.target.getType()));
423+
return (this.targetType != null && Proxy.isProxyClass(this.targetType.getType()));
438424
}
439425

440426
public MethodExecutor get() {

Diff for: spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
import org.springframework.util.ObjectUtils;
3434

3535
/**
36-
* Represents projection, where a given operation is performed on all elements in some
37-
* input sequence, returning a new sequence of the same size.
36+
* Represents projection, where a given operation is performed on all elements in
37+
* some input sequence, returning a new sequence of the same size.
3838
*
3939
* <p>For example: <code>{1,2,3,4,5,6,7,8,9,10}.![#isEven(#this)]</code> evaluates
4040
* to {@code [n, y, n, y, n, y, n, y, n, y]}.
@@ -72,8 +72,8 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
7272

7373
@Override
7474
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
75-
TypedValue op = state.getActiveContextObject();
76-
Object operand = op.getValue();
75+
TypedValue contextObject = state.getActiveContextObject();
76+
Object operand = contextObject.getValue();
7777

7878
// When the input is a map, we push a Map.Entry on the stack before calling
7979
// the specified operation. Map.Entry has two properties 'key' and 'value'

Diff for: spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -179,16 +179,16 @@ public String toStringAST() {
179179
private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
180180
throws EvaluationException {
181181

182-
Object targetObject = contextObject.getValue();
183-
if (targetObject == null && isNullSafe()) {
182+
Object target = contextObject.getValue();
183+
if (target == null && isNullSafe()) {
184184
return TypedValue.NULL;
185185
}
186186

187187
PropertyAccessor accessorToUse = this.cachedReadAccessor;
188188
if (accessorToUse != null) {
189189
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
190190
try {
191-
return accessorToUse.read(evalContext, targetObject, name);
191+
return accessorToUse.read(evalContext, target, name);
192192
}
193193
catch (Exception ex) {
194194
// This is OK - it may have gone stale due to a class change,
@@ -199,19 +199,19 @@ private TypedValue readProperty(TypedValue contextObject, EvaluationContext eval
199199
}
200200

201201
List<PropertyAccessor> accessorsToTry =
202-
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors());
202+
AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
203203
// Go through the accessors that may be able to resolve it. If they are a cacheable accessor then
204204
// get the accessor and use it. If they are not cacheable but report they can read the property
205-
// then ask them to read it
205+
// then ask them to read it.
206206
try {
207207
for (PropertyAccessor accessor : accessorsToTry) {
208-
if (accessor.canRead(evalContext, targetObject, name)) {
208+
if (accessor.canRead(evalContext, target, name)) {
209209
if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) {
210210
accessor = reflectivePropertyAccessor.createOptimalAccessor(
211-
evalContext, targetObject, name);
211+
evalContext, target, name);
212212
}
213213
this.cachedReadAccessor = accessor;
214-
return accessor.read(evalContext, targetObject, name);
214+
return accessor.read(evalContext, target, name);
215215
}
216216
}
217217
}
@@ -232,8 +232,8 @@ private void writeProperty(
232232
TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue)
233233
throws EvaluationException {
234234

235-
Object targetObject = contextObject.getValue();
236-
if (targetObject == null) {
235+
Object target = contextObject.getValue();
236+
if (target == null) {
237237
if (isNullSafe()) {
238238
return;
239239
}
@@ -245,7 +245,7 @@ private void writeProperty(
245245
if (accessorToUse != null) {
246246
if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
247247
try {
248-
accessorToUse.write(evalContext, targetObject, name, newValue);
248+
accessorToUse.write(evalContext, target, name, newValue);
249249
return;
250250
}
251251
catch (Exception ex) {
@@ -257,12 +257,12 @@ private void writeProperty(
257257
}
258258

259259
List<PropertyAccessor> accessorsToTry =
260-
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors());
260+
AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
261261
try {
262262
for (PropertyAccessor accessor : accessorsToTry) {
263-
if (accessor.canWrite(evalContext, targetObject, name)) {
263+
if (accessor.canWrite(evalContext, target, name)) {
264264
this.cachedWriteAccessor = accessor;
265-
accessor.write(evalContext, targetObject, name, newValue);
265+
accessor.write(evalContext, target, name, newValue);
266266
return;
267267
}
268268
}
@@ -273,19 +273,19 @@ private void writeProperty(
273273
}
274274

275275
throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name,
276-
FormatHelper.formatClassNameForMessage(getObjectClass(targetObject)));
276+
FormatHelper.formatClassNameForMessage(getObjectClass(target)));
277277
}
278278

279279
public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext)
280280
throws EvaluationException {
281281

282-
Object targetObject = contextObject.getValue();
283-
if (targetObject != null) {
282+
Object target = contextObject.getValue();
283+
if (target != null) {
284284
List<PropertyAccessor> accessorsToTry =
285-
AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors());
285+
AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors());
286286
for (PropertyAccessor accessor : accessorsToTry) {
287287
try {
288-
if (accessor.canWrite(evalContext, targetObject, name)) {
288+
if (accessor.canWrite(evalContext, target, name)) {
289289
return true;
290290
}
291291
}

Diff for: spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import org.springframework.util.ObjectUtils;
3636

3737
/**
38-
* Represents selection over a map or collection.
38+
* Represents selection over a {@link Map}, {@link Iterable}, or array.
3939
*
4040
* <p>For example, <code>{1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this)]</code> evaluates
4141
* to {@code [2, 4, 6, 8, 10]}.
@@ -94,8 +94,8 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
9494

9595
@Override
9696
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
97-
TypedValue op = state.getActiveContextObject();
98-
Object operand = op.getValue();
97+
TypedValue contextObject = state.getActiveContextObject();
98+
Object operand = contextObject.getValue();
9999
SpelNodeImpl selectionCriteria = this.children[0];
100100

101101
if (operand instanceof Map<?, ?> mapdata) {
@@ -151,9 +151,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
151151
try {
152152
state.pushActiveContextObject(new TypedValue(element));
153153
state.enterScope();
154-
Object val = selectionCriteria.getValueInternal(state).getValue();
155-
if (val instanceof Boolean b) {
156-
if (b) {
154+
Object criteria = selectionCriteria.getValueInternal(state).getValue();
155+
if (criteria instanceof Boolean match) {
156+
if (match) {
157157
if (this.variant == FIRST) {
158158
return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this);
159159
}
@@ -184,7 +184,7 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
184184
}
185185

186186
Class<?> elementType = null;
187-
TypeDescriptor typeDesc = op.getTypeDescriptor();
187+
TypeDescriptor typeDesc = contextObject.getTypeDescriptor();
188188
if (typeDesc != null) {
189189
TypeDescriptor elementTypeDesc = typeDesc.getElementTypeDescriptor();
190190
if (elementTypeDesc != null) {

0 commit comments

Comments
 (0)