22
22
import java .util .Map ;
23
23
import java .util .function .Supplier ;
24
24
25
+ import org .springframework .asm .Label ;
25
26
import org .springframework .asm .MethodVisitor ;
26
27
import org .springframework .core .convert .TypeDescriptor ;
27
28
import org .springframework .expression .AccessException ;
57
58
* <li>Objects: the property with the specified name</li>
58
59
* </ul>
59
60
*
61
+ * <h3>Null-safe Indexing</h3>
62
+ *
63
+ * <p>As of Spring Framework 6.2, null-safe indexing is supported via the {@code '?.'}
64
+ * operator. For example, {@code 'colors?.[0]'} will evaluate to {@code null} if
65
+ * {@code colors} is {@code null} and will otherwise evaluate to the 0<sup>th</sup>
66
+ * color.
67
+ *
60
68
* @author Andy Clement
61
69
* @author Phillip Webb
62
70
* @author Stephane Nicoll
@@ -68,9 +76,14 @@ public class Indexer extends SpelNodeImpl {
68
76
private enum IndexedType {ARRAY , LIST , MAP , STRING , OBJECT }
69
77
70
78
79
+ private final boolean nullSafe ;
80
+
71
81
@ Nullable
72
82
private IndexedType indexedType ;
73
83
84
+ @ Nullable
85
+ private String originalPrimitiveExitTypeDescriptor ;
86
+
74
87
@ Nullable
75
88
private volatile String arrayTypeDescriptor ;
76
89
@@ -106,12 +119,34 @@ private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT}
106
119
/**
107
120
* Create an {@code Indexer} with the given start position, end position, and
108
121
* index expression.
122
+ * @see #Indexer(boolean, int, int, SpelNodeImpl)
123
+ * @deprecated as of Spring Framework 6.2, in favor of {@link #Indexer(boolean, int, int, SpelNodeImpl)}
109
124
*/
125
+ @ Deprecated (since = "6.2" , forRemoval = true )
110
126
public Indexer (int startPos , int endPos , SpelNodeImpl indexExpression ) {
127
+ this (false , startPos , endPos , indexExpression );
128
+ }
129
+
130
+ /**
131
+ * Create an {@code Indexer} with the given null-safe flag, start position,
132
+ * end position, and index expression.
133
+ * @since 6.2
134
+ */
135
+ public Indexer (boolean nullSafe , int startPos , int endPos , SpelNodeImpl indexExpression ) {
111
136
super (startPos , endPos , indexExpression );
137
+ this .nullSafe = nullSafe ;
112
138
}
113
139
114
140
141
+ /**
142
+ * Does this node represent a null-safe index operation?
143
+ * @since 6.2
144
+ */
145
+ @ Override
146
+ public final boolean isNullSafe () {
147
+ return this .nullSafe ;
148
+ }
149
+
115
150
@ Override
116
151
public TypedValue getValueInternal (ExpressionState state ) throws EvaluationException {
117
152
return getValueRef (state ).getValue ();
@@ -136,6 +171,15 @@ public boolean isWritable(ExpressionState expressionState) throws SpelEvaluation
136
171
protected ValueRef getValueRef (ExpressionState state ) throws EvaluationException {
137
172
TypedValue context = state .getActiveContextObject ();
138
173
Object target = context .getValue ();
174
+
175
+ if (target == null ) {
176
+ if (this .nullSafe ) {
177
+ return ValueRef .NullValueRef .INSTANCE ;
178
+ }
179
+ // Raise a proper exception in case of a null target
180
+ throw new SpelEvaluationException (getStartPosition (), SpelMessage .CANNOT_INDEX_INTO_NULL_VALUE );
181
+ }
182
+
139
183
TypeDescriptor targetDescriptor = context .getTypeDescriptor ();
140
184
TypedValue indexValue ;
141
185
Object index ;
@@ -159,11 +203,6 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException
159
203
}
160
204
}
161
205
162
- // Raise a proper exception in case of a null target
163
- if (target == null ) {
164
- throw new SpelEvaluationException (getStartPosition (), SpelMessage .CANNOT_INDEX_INTO_NULL_VALUE );
165
- }
166
-
167
206
// At this point, we need a TypeDescriptor for a non-null target object
168
207
Assert .state (targetDescriptor != null , "No type descriptor" );
169
208
@@ -243,6 +282,17 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) {
243
282
cf .loadTarget (mv );
244
283
}
245
284
285
+ Label skipIfNull = null ;
286
+ if (this .nullSafe ) {
287
+ mv .visitInsn (DUP );
288
+ skipIfNull = new Label ();
289
+ Label continueLabel = new Label ();
290
+ mv .visitJumpInsn (IFNONNULL , continueLabel );
291
+ CodeFlow .insertCheckCast (mv , exitTypeDescriptor );
292
+ mv .visitJumpInsn (GOTO , skipIfNull );
293
+ mv .visitLabel (continueLabel );
294
+ }
295
+
246
296
SpelNodeImpl index = this .children [0 ];
247
297
248
298
if (this .indexedType == IndexedType .ARRAY ) {
@@ -305,6 +355,16 @@ else if (this.indexedType == IndexedType.OBJECT) {
305
355
}
306
356
307
357
cf .pushDescriptor (exitTypeDescriptor );
358
+
359
+ if (skipIfNull != null ) {
360
+ if (this .originalPrimitiveExitTypeDescriptor != null ) {
361
+ // The output of the indexer is a primitive, but from the logic above it
362
+ // might be null. So, to have a common stack element type at the skipIfNull
363
+ // target, it is necessary to box the primitive.
364
+ CodeFlow .insertBoxIfNecessary (mv , this .originalPrimitiveExitTypeDescriptor );
365
+ }
366
+ mv .visitLabel (skipIfNull );
367
+ }
308
368
}
309
369
310
370
@ Override
@@ -368,64 +428,64 @@ private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationExce
368
428
if (arrayComponentType == boolean .class ) {
369
429
boolean [] array = (boolean []) ctx ;
370
430
checkAccess (array .length , idx );
371
- this . exitTypeDescriptor = "Z" ;
431
+ setExitTypeDescriptor ( "Z" ) ;
372
432
this .arrayTypeDescriptor = "[Z" ;
373
433
return array [idx ];
374
434
}
375
435
else if (arrayComponentType == byte .class ) {
376
436
byte [] array = (byte []) ctx ;
377
437
checkAccess (array .length , idx );
378
- this . exitTypeDescriptor = "B" ;
438
+ setExitTypeDescriptor ( "B" ) ;
379
439
this .arrayTypeDescriptor = "[B" ;
380
440
return array [idx ];
381
441
}
382
442
else if (arrayComponentType == char .class ) {
383
443
char [] array = (char []) ctx ;
384
444
checkAccess (array .length , idx );
385
- this . exitTypeDescriptor = "C" ;
445
+ setExitTypeDescriptor ( "C" ) ;
386
446
this .arrayTypeDescriptor = "[C" ;
387
447
return array [idx ];
388
448
}
389
449
else if (arrayComponentType == double .class ) {
390
450
double [] array = (double []) ctx ;
391
451
checkAccess (array .length , idx );
392
- this . exitTypeDescriptor = "D" ;
452
+ setExitTypeDescriptor ( "D" ) ;
393
453
this .arrayTypeDescriptor = "[D" ;
394
454
return array [idx ];
395
455
}
396
456
else if (arrayComponentType == float .class ) {
397
457
float [] array = (float []) ctx ;
398
458
checkAccess (array .length , idx );
399
- this . exitTypeDescriptor = "F" ;
459
+ setExitTypeDescriptor ( "F" ) ;
400
460
this .arrayTypeDescriptor = "[F" ;
401
461
return array [idx ];
402
462
}
403
463
else if (arrayComponentType == int .class ) {
404
464
int [] array = (int []) ctx ;
405
465
checkAccess (array .length , idx );
406
- this . exitTypeDescriptor = "I" ;
466
+ setExitTypeDescriptor ( "I" ) ;
407
467
this .arrayTypeDescriptor = "[I" ;
408
468
return array [idx ];
409
469
}
410
470
else if (arrayComponentType == long .class ) {
411
471
long [] array = (long []) ctx ;
412
472
checkAccess (array .length , idx );
413
- this . exitTypeDescriptor = "J" ;
473
+ setExitTypeDescriptor ( "J" ) ;
414
474
this .arrayTypeDescriptor = "[J" ;
415
475
return array [idx ];
416
476
}
417
477
else if (arrayComponentType == short .class ) {
418
478
short [] array = (short []) ctx ;
419
479
checkAccess (array .length , idx );
420
- this . exitTypeDescriptor = "S" ;
480
+ setExitTypeDescriptor ( "S" ) ;
421
481
this .arrayTypeDescriptor = "[S" ;
422
482
return array [idx ];
423
483
}
424
484
else {
425
485
Object [] array = (Object []) ctx ;
426
486
checkAccess (array .length , idx );
427
487
Object retValue = array [idx ];
428
- this . exitTypeDescriptor = CodeFlow .toDescriptor (arrayComponentType );
488
+ setExitTypeDescriptor ( CodeFlow .toDescriptor (arrayComponentType ) );
429
489
this .arrayTypeDescriptor = CodeFlow .toDescriptor (array .getClass ());
430
490
return retValue ;
431
491
}
@@ -438,6 +498,19 @@ private void checkAccess(int arrayLength, int index) throws SpelEvaluationExcept
438
498
}
439
499
}
440
500
501
+ private void setExitTypeDescriptor (String descriptor ) {
502
+ // If this indexer would return a primitive - and yet it is also marked
503
+ // null-safe - then the exit type descriptor must be promoted to the box
504
+ // type to allow a null value to be passed on.
505
+ if (this .nullSafe && CodeFlow .isPrimitive (descriptor )) {
506
+ this .originalPrimitiveExitTypeDescriptor = descriptor ;
507
+ this .exitTypeDescriptor = CodeFlow .toBoxedDescriptor (descriptor );
508
+ }
509
+ else {
510
+ this .exitTypeDescriptor = descriptor ;
511
+ }
512
+ }
513
+
441
514
@ SuppressWarnings ("unchecked" )
442
515
private <T > T convertValue (TypeConverter converter , @ Nullable Object value , Class <T > targetType ) {
443
516
T result = (T ) converter .convertValue (
@@ -574,7 +647,7 @@ public TypedValue getValue() {
574
647
Indexer .this .cachedReadName = this .name ;
575
648
Indexer .this .cachedReadTargetType = targetObjectRuntimeClass ;
576
649
if (accessor instanceof CompilablePropertyAccessor compilablePropertyAccessor ) {
577
- Indexer . this . exitTypeDescriptor = CodeFlow .toDescriptor (compilablePropertyAccessor .getPropertyType ());
650
+ setExitTypeDescriptor ( CodeFlow .toDescriptor (compilablePropertyAccessor .getPropertyType () ));
578
651
}
579
652
return accessor .read (this .evaluationContext , this .targetObject , this .name );
580
653
}
0 commit comments