23
23
import java .util .ArrayList ;
24
24
import java .util .Collections ;
25
25
import java .util .List ;
26
+ import java .util .Optional ;
26
27
import java .util .StringJoiner ;
27
28
28
29
import org .jspecify .annotations .Nullable ;
50
51
* Expression language AST node that represents a method reference (i.e., a
51
52
* method invocation other than a simple property reference).
52
53
*
54
+ * <h3>Null-safe Invocation</h3>
55
+ *
56
+ * <p>Null-safe invocation is supported via the {@code '?.'} operator. For example,
57
+ * {@code 'counter?.incrementBy(1)'} will evaluate to {@code null} if {@code counter}
58
+ * is {@code null} and will otherwise evaluate to the value returned from the
59
+ * invocation of {@code counter.incrementBy(1)}. As of Spring Framework 7.0,
60
+ * null-safe invocation also applies when invoking a method on an {@link Optional}
61
+ * target. For example, if {@code counter} is of type {@code Optional<Counter>},
62
+ * the expression {@code 'counter?.incrementBy(1)'} will evaluate to {@code null}
63
+ * if {@code counter} is {@code null} or {@link Optional#isEmpty() empty} and will
64
+ * otherwise evaluate the value returned from the invocation of
65
+ * {@code counter.get().incrementBy(1)}.
66
+ *
53
67
* @author Andy Clement
54
68
* @author Juergen Hoeller
55
69
* @author Sam Brannen
@@ -93,7 +107,9 @@ public final String getName() {
93
107
protected ValueRef getValueRef (ExpressionState state ) throws EvaluationException {
94
108
@ Nullable Object [] arguments = getArguments (state );
95
109
if (state .getActiveContextObject ().getValue () == null ) {
96
- throwIfNotNullSafe (getArgumentTypes (arguments ));
110
+ if (!isNullSafe ()) {
111
+ throw nullTargetException (getArgumentTypes (arguments ));
112
+ }
97
113
return ValueRef .NullValueRef .INSTANCE ;
98
114
}
99
115
return new MethodValueRef (state , arguments );
@@ -115,9 +131,26 @@ private TypedValue getValueInternal(EvaluationContext evaluationContext, @Nullab
115
131
@ Nullable TypeDescriptor targetType , @ Nullable Object [] arguments ) {
116
132
117
133
List <TypeDescriptor > argumentTypes = getArgumentTypes (arguments );
134
+ Optional <?> fallbackOptionalTarget = null ;
135
+ boolean isEmptyOptional = false ;
136
+
137
+ if (isNullSafe ()) {
138
+ if (target == null ) {
139
+ return TypedValue .NULL ;
140
+ }
141
+ if (target instanceof Optional <?> optional ) {
142
+ if (optional .isPresent ()) {
143
+ target = optional .get ();
144
+ fallbackOptionalTarget = optional ;
145
+ }
146
+ else {
147
+ isEmptyOptional = true ;
148
+ }
149
+ }
150
+ }
151
+
118
152
if (target == null ) {
119
- throwIfNotNullSafe (argumentTypes );
120
- return TypedValue .NULL ;
153
+ throw nullTargetException (argumentTypes );
121
154
}
122
155
123
156
MethodExecutor executorToUse = getCachedExecutor (evaluationContext , target , targetType , argumentTypes );
@@ -142,31 +175,64 @@ private TypedValue getValueInternal(EvaluationContext evaluationContext, @Nullab
142
175
// At this point we know it wasn't a user problem so worth a retry if a
143
176
// better candidate can be found.
144
177
this .cachedExecutor = null ;
178
+ executorToUse = null ;
179
+ }
180
+ }
181
+
182
+ // Either there was no cached executor, or it no longer exists.
183
+
184
+ // First, attempt to find the method on the target object.
185
+ Object targetToUse = target ;
186
+ MethodExecutorSearchResult searchResult = findMethodExecutor (argumentTypes , target , evaluationContext );
187
+ if (searchResult .methodExecutor != null ) {
188
+ executorToUse = searchResult .methodExecutor ;
189
+ }
190
+ // Second, attempt to find the method on the original Optional instance.
191
+ else if (fallbackOptionalTarget != null ) {
192
+ searchResult = findMethodExecutor (argumentTypes , fallbackOptionalTarget , evaluationContext );
193
+ if (searchResult .methodExecutor != null ) {
194
+ executorToUse = searchResult .methodExecutor ;
195
+ targetToUse = fallbackOptionalTarget ;
196
+ }
197
+ }
198
+ // If we got this far, that means we failed to find an executor for both the
199
+ // target and the fallback target. So, we return NULL if the original target
200
+ // is a null-safe empty Optional.
201
+ else if (isEmptyOptional ) {
202
+ return TypedValue .NULL ;
203
+ }
204
+
205
+ if (executorToUse == null ) {
206
+ String method = FormatHelper .formatMethodForMessage (this .name , argumentTypes );
207
+ String className = FormatHelper .formatClassNameForMessage (
208
+ target instanceof Class <?> clazz ? clazz : target .getClass ());
209
+ if (searchResult .accessException != null ) {
210
+ throw new SpelEvaluationException (
211
+ getStartPosition (), searchResult .accessException , SpelMessage .PROBLEM_LOCATING_METHOD , method , className );
212
+ }
213
+ else {
214
+ throw new SpelEvaluationException (getStartPosition (), SpelMessage .METHOD_NOT_FOUND , method , className );
145
215
}
146
216
}
147
217
148
- // either there was no accessor or it no longer existed
149
- executorToUse = findMethodExecutor (argumentTypes , target , evaluationContext );
150
218
this .cachedExecutor = new CachedMethodExecutor (
151
- executorToUse , (target instanceof Class <?> clazz ? clazz : null ), targetType , argumentTypes );
219
+ executorToUse , (targetToUse instanceof Class <?> clazz ? clazz : null ), targetType , argumentTypes );
152
220
try {
153
- return executorToUse .execute (evaluationContext , target , arguments );
221
+ return executorToUse .execute (evaluationContext , targetToUse , arguments );
154
222
}
155
223
catch (AccessException ex ) {
156
224
// Same unwrapping exception handling as in above catch block
157
- throwSimpleExceptionIfPossible (target , ex );
225
+ throwSimpleExceptionIfPossible (targetToUse , ex );
158
226
throw new SpelEvaluationException (getStartPosition (), ex ,
159
227
SpelMessage .EXCEPTION_DURING_METHOD_INVOCATION , this .name ,
160
- target .getClass ().getName (), ex .getMessage ());
228
+ targetToUse .getClass ().getName (), ex .getMessage ());
161
229
}
162
230
}
163
231
164
- private void throwIfNotNullSafe (List <TypeDescriptor > argumentTypes ) {
165
- if (!isNullSafe ()) {
166
- throw new SpelEvaluationException (getStartPosition (),
167
- SpelMessage .METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED ,
168
- FormatHelper .formatMethodForMessage (this .name , argumentTypes ));
169
- }
232
+ private SpelEvaluationException nullTargetException (List <TypeDescriptor > argumentTypes ) {
233
+ return new SpelEvaluationException (getStartPosition (),
234
+ SpelMessage .METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED ,
235
+ FormatHelper .formatMethodForMessage (this .name , argumentTypes ));
170
236
}
171
237
172
238
private @ Nullable Object [] getArguments (ExpressionState state ) {
@@ -209,7 +275,7 @@ private List<TypeDescriptor> getArgumentTypes(@Nullable Object... arguments) {
209
275
return null ;
210
276
}
211
277
212
- private MethodExecutor findMethodExecutor (List <TypeDescriptor > argumentTypes , Object target ,
278
+ private MethodExecutorSearchResult findMethodExecutor (List <TypeDescriptor > argumentTypes , Object target ,
213
279
EvaluationContext evaluationContext ) throws SpelEvaluationException {
214
280
215
281
AccessException accessException = null ;
@@ -218,7 +284,7 @@ private MethodExecutor findMethodExecutor(List<TypeDescriptor> argumentTypes, Ob
218
284
MethodExecutor methodExecutor = methodResolver .resolve (
219
285
evaluationContext , target , this .name , argumentTypes );
220
286
if (methodExecutor != null ) {
221
- return methodExecutor ;
287
+ return new MethodExecutorSearchResult ( methodExecutor , null ) ;
222
288
}
223
289
}
224
290
catch (AccessException ex ) {
@@ -227,16 +293,7 @@ private MethodExecutor findMethodExecutor(List<TypeDescriptor> argumentTypes, Ob
227
293
}
228
294
}
229
295
230
- String method = FormatHelper .formatMethodForMessage (this .name , argumentTypes );
231
- String className = FormatHelper .formatClassNameForMessage (
232
- target instanceof Class <?> clazz ? clazz : target .getClass ());
233
- if (accessException != null ) {
234
- throw new SpelEvaluationException (
235
- getStartPosition (), accessException , SpelMessage .PROBLEM_LOCATING_METHOD , method , className );
236
- }
237
- else {
238
- throw new SpelEvaluationException (getStartPosition (), SpelMessage .METHOD_NOT_FOUND , method , className );
239
- }
296
+ return new MethodExecutorSearchResult (null , accessException );
240
297
}
241
298
242
299
/**
@@ -411,6 +468,9 @@ public boolean isWritable() {
411
468
}
412
469
413
470
471
+ private record MethodExecutorSearchResult (@ Nullable MethodExecutor methodExecutor , @ Nullable AccessException accessException ) {
472
+ }
473
+
414
474
private record CachedMethodExecutor (MethodExecutor methodExecutor , @ Nullable Class <?> staticClass ,
415
475
@ Nullable TypeDescriptor targetType , List <TypeDescriptor > argumentTypes ) {
416
476
0 commit comments