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 ;
47
48
import org .springframework .util .ObjectUtils ;
48
49
49
50
/**
50
- * Expression language AST node that represents a method reference.
51
+ * Expression language AST node that represents a method reference (i.e., a
52
+ * method invocation other than a simple property reference).
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)}.
51
66
*
52
67
* @author Andy Clement
53
68
* @author Juergen Hoeller
@@ -92,7 +107,9 @@ public final String getName() {
92
107
protected ValueRef getValueRef (ExpressionState state ) throws EvaluationException {
93
108
@ Nullable Object [] arguments = getArguments (state );
94
109
if (state .getActiveContextObject ().getValue () == null ) {
95
- throwIfNotNullSafe (getArgumentTypes (arguments ));
110
+ if (!isNullSafe ()) {
111
+ throw nullTargetObjectException (getArgumentTypes (arguments ));
112
+ }
96
113
return ValueRef .NullValueRef .INSTANCE ;
97
114
}
98
115
return new MethodValueRef (state , arguments );
@@ -109,19 +126,32 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep
109
126
return result ;
110
127
}
111
128
112
- private TypedValue getValueInternal (EvaluationContext evaluationContext ,
113
- @ Nullable Object value , @ Nullable TypeDescriptor targetType , @ Nullable Object [] arguments ) {
129
+ private TypedValue getValueInternal (EvaluationContext evaluationContext , final @ Nullable Object value ,
130
+ @ Nullable TypeDescriptor targetType , @ Nullable Object [] arguments ) {
114
131
132
+ Object target = value ;
115
133
List <TypeDescriptor > argumentTypes = getArgumentTypes (arguments );
116
- if (value == null ) {
117
- throwIfNotNullSafe (argumentTypes );
118
- return TypedValue .NULL ;
134
+
135
+ if (isNullSafe ()) {
136
+ if (target == null ) {
137
+ return TypedValue .NULL ;
138
+ }
139
+ if (target instanceof Optional <?> optional ) {
140
+ if (optional .isEmpty ()) {
141
+ return TypedValue .NULL ;
142
+ }
143
+ target = optional .get ();
144
+ }
119
145
}
120
146
121
- MethodExecutor executorToUse = getCachedExecutor (evaluationContext , value , targetType , argumentTypes );
147
+ if (target == null ) {
148
+ throw nullTargetObjectException (argumentTypes );
149
+ }
150
+
151
+ MethodExecutor executorToUse = getCachedExecutor (evaluationContext , target , targetType , argumentTypes );
122
152
if (executorToUse != null ) {
123
153
try {
124
- return executorToUse .execute (evaluationContext , value , arguments );
154
+ return executorToUse .execute (evaluationContext , target , arguments );
125
155
}
126
156
catch (AccessException ex ) {
127
157
// Two reasons this can occur:
@@ -135,7 +165,7 @@ private TypedValue getValueInternal(EvaluationContext evaluationContext,
135
165
// To determine the situation, the AccessException will contain a cause.
136
166
// If the cause is an InvocationTargetException, a user exception was
137
167
// thrown inside the method. Otherwise the method could not be invoked.
138
- throwSimpleExceptionIfPossible (value , ex );
168
+ throwSimpleExceptionIfPossible (target , ex );
139
169
140
170
// At this point we know it wasn't a user problem so worth a retry if a
141
171
// better candidate can be found.
@@ -144,27 +174,25 @@ private TypedValue getValueInternal(EvaluationContext evaluationContext,
144
174
}
145
175
146
176
// either there was no accessor or it no longer existed
147
- executorToUse = findAccessorForMethod (argumentTypes , value , evaluationContext );
177
+ executorToUse = findAccessorForMethod (argumentTypes , target , evaluationContext );
148
178
this .cachedExecutor = new CachedMethodExecutor (
149
- executorToUse , (value instanceof Class <?> clazz ? clazz : null ), targetType , argumentTypes );
179
+ executorToUse , (target instanceof Class <?> clazz ? clazz : null ), targetType , argumentTypes );
150
180
try {
151
- return executorToUse .execute (evaluationContext , value , arguments );
181
+ return executorToUse .execute (evaluationContext , target , arguments );
152
182
}
153
183
catch (AccessException ex ) {
154
184
// Same unwrapping exception handling as above in above catch block
155
- throwSimpleExceptionIfPossible (value , ex );
185
+ throwSimpleExceptionIfPossible (target , ex );
156
186
throw new SpelEvaluationException (getStartPosition (), ex ,
157
187
SpelMessage .EXCEPTION_DURING_METHOD_INVOCATION , this .name ,
158
- value .getClass ().getName (), ex .getMessage ());
188
+ ( value != null ? value .getClass ().getName () : "null" ), ex .getMessage ());
159
189
}
160
190
}
161
191
162
- private void throwIfNotNullSafe (List <TypeDescriptor > argumentTypes ) {
163
- if (!isNullSafe ()) {
164
- throw new SpelEvaluationException (getStartPosition (),
165
- SpelMessage .METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED ,
166
- FormatHelper .formatMethodForMessage (this .name , argumentTypes ));
167
- }
192
+ private SpelEvaluationException nullTargetObjectException (List <TypeDescriptor > argumentTypes ) {
193
+ return new SpelEvaluationException (getStartPosition (),
194
+ SpelMessage .METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED ,
195
+ FormatHelper .formatMethodForMessage (this .name , argumentTypes ));
168
196
}
169
197
170
198
private @ Nullable Object [] getArguments (ExpressionState state ) {
0 commit comments