Skip to content

Commit 9aab4a6

Browse files
committed
Support safe navigation operator with void methods in SpEL
Prior to this commit the Spring Expression Language (SpEL) was able to properly parse an expression that uses the safe navigation operator (?.) with a method that has a `void` return type (for example, "myObject?.doSomething()"); however, SpEL was not able to evaluate or compile such expressions. This commit addresses the evaluation issue by selectively not boxing the exit type descriptor (for inclusion in the generated bytecode) when the method's return type is `void`. This commit addresses the compilation issue by pushing a null object reference onto the stack in the generated byte code when the method's return type is `void`. Closes gh-27421
1 parent 1fe2216 commit 9aab4a6

File tree

2 files changed

+85
-11
lines changed

2 files changed

+85
-11
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ private void updateExitTypeDescriptor() {
259259
if (executorToCheck != null && executorToCheck.get() instanceof ReflectiveMethodExecutor reflectiveMethodExecutor) {
260260
Method method = reflectiveMethodExecutor.getMethod();
261261
String descriptor = CodeFlow.toDescriptor(method.getReturnType());
262-
if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) {
262+
if (this.nullSafe && CodeFlow.isPrimitive(descriptor) && (descriptor.charAt(0) != 'V')) {
263263
this.originalPrimitiveExitTypeDescriptor = descriptor.charAt(0);
264264
this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor);
265265
}
@@ -359,10 +359,16 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) {
359359

360360
if (this.originalPrimitiveExitTypeDescriptor != null) {
361361
// The output of the accessor will be a primitive but from the block above it might be null,
362-
// so to have a 'common stack' element at skipIfNull target we need to box the primitive
362+
// so to have a 'common stack' element at the skipIfNull target we need to box the primitive.
363363
CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor);
364364
}
365+
365366
if (skipIfNull != null) {
367+
if ("V".equals(this.exitTypeDescriptor)) {
368+
// If the method return type is 'void', we need to push a null object
369+
// reference onto the stack to satisfy the needs of the skipIfNull target.
370+
mv.visitInsn(ACONST_NULL);
371+
}
366372
mv.visitLabel(skipIfNull);
367373
}
368374
}

spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,34 @@ public void nullsafeFieldPropertyDereferencing_SPR16489() throws Exception {
771771
assertThat(expression.getValue(context)).isNull();
772772
}
773773

774+
@Test // gh-27421
775+
public void nullSafeMethodChainingWithNonStaticVoidMethod() throws Exception {
776+
FooObjectHolder foh = new FooObjectHolder();
777+
StandardEvaluationContext context = new StandardEvaluationContext(foh);
778+
SpelExpression expression = (SpelExpression) parser.parseExpression("getFoo()?.doFoo()");
779+
780+
FooObject.doFooInvoked = false;
781+
assertThat(expression.getValue(context)).isNull();
782+
assertThat(FooObject.doFooInvoked).isTrue();
783+
784+
FooObject.doFooInvoked = false;
785+
foh.foo = null;
786+
assertThat(expression.getValue(context)).isNull();
787+
assertThat(FooObject.doFooInvoked).isFalse();
788+
789+
assertCanCompile(expression);
790+
791+
FooObject.doFooInvoked = false;
792+
foh.foo = new FooObject();
793+
assertThat(expression.getValue(context)).isNull();
794+
assertThat(FooObject.doFooInvoked).isTrue();
795+
796+
FooObject.doFooInvoked = false;
797+
foh.foo = null;
798+
assertThat(expression.getValue(context)).isNull();
799+
assertThat(FooObject.doFooInvoked).isFalse();
800+
}
801+
774802
@Test
775803
public void nullsafeMethodChaining_SPR16489() throws Exception {
776804
FooObjectHolder foh = new FooObjectHolder();
@@ -3898,37 +3926,71 @@ void methodReferenceVarargs() {
38983926
tc.reset();
38993927
}
39003928

3901-
@Test
3902-
void nullSafeInvocationOfNonStaticVoidWrapperMethod() {
3929+
@Test // gh-27421
3930+
public void nullSafeInvocationOfNonStaticVoidMethod() {
3931+
// non-static method, no args, void return
3932+
expression = parser.parseExpression("new %s()?.one()".formatted(TestClass5.class.getName()));
3933+
3934+
assertCantCompile(expression);
3935+
3936+
TestClass5._i = 0;
3937+
assertThat(expression.getValue()).isNull();
3938+
assertThat(TestClass5._i).isEqualTo(1);
3939+
3940+
TestClass5._i = 0;
3941+
assertCanCompile(expression);
3942+
assertThat(expression.getValue()).isNull();
3943+
assertThat(TestClass5._i).isEqualTo(1);
3944+
}
3945+
3946+
@Test // gh-27421
3947+
public void nullSafeInvocationOfStaticVoidMethod() {
3948+
// static method, no args, void return
3949+
expression = parser.parseExpression("T(%s)?.two()".formatted(TestClass5.class.getName()));
3950+
3951+
assertCantCompile(expression);
3952+
3953+
TestClass5._i = 0;
3954+
assertThat(expression.getValue()).isNull();
3955+
assertThat(TestClass5._i).isEqualTo(1);
3956+
3957+
TestClass5._i = 0;
3958+
assertCanCompile(expression);
3959+
assertThat(expression.getValue()).isNull();
3960+
assertThat(TestClass5._i).isEqualTo(1);
3961+
}
3962+
3963+
@Test // gh-27421
3964+
public void nullSafeInvocationOfNonStaticVoidWrapperMethod() {
39033965
// non-static method, no args, Void return
39043966
expression = parser.parseExpression("new %s()?.oneVoidWrapper()".formatted(TestClass5.class.getName()));
39053967

39063968
assertCantCompile(expression);
39073969

39083970
TestClass5._i = 0;
3909-
expression.getValue();
3971+
assertThat(expression.getValue()).isNull();
39103972
assertThat(TestClass5._i).isEqualTo(1);
39113973

39123974
TestClass5._i = 0;
39133975
assertCanCompile(expression);
3914-
expression.getValue();
3976+
assertThat(expression.getValue()).isNull();
39153977
assertThat(TestClass5._i).isEqualTo(1);
39163978
}
39173979

3918-
@Test
3919-
void nullSafeInvocationOfStaticVoidWrapperMethod() {
3980+
@Test // gh-27421
3981+
public void nullSafeInvocationOfStaticVoidWrapperMethod() {
39203982
// static method, no args, Void return
39213983
expression = parser.parseExpression("T(%s)?.twoVoidWrapper()".formatted(TestClass5.class.getName()));
39223984

39233985
assertCantCompile(expression);
39243986

39253987
TestClass5._i = 0;
3926-
expression.getValue();
3988+
assertThat(expression.getValue()).isNull();
39273989
assertThat(TestClass5._i).isEqualTo(1);
39283990

39293991
TestClass5._i = 0;
39303992
assertCanCompile(expression);
3931-
expression.getValue();
3993+
assertThat(expression.getValue()).isNull();
39323994
assertThat(TestClass5._i).isEqualTo(1);
39333995
}
39343996

@@ -5496,7 +5558,10 @@ public FooObject getFoo() {
54965558

54975559
public static class FooObject {
54985560

5561+
static boolean doFooInvoked = false;
5562+
54995563
public Object getObject() { return "hello"; }
5564+
public void doFoo() { doFooInvoked = true; }
55005565
}
55015566

55025567

@@ -5744,7 +5809,10 @@ public void reset() {
57445809
field = null;
57455810
}
57465811

5747-
public void one() { i = 1; }
5812+
public void one() {
5813+
_i = 1;
5814+
this.i = 1;
5815+
}
57485816

57495817
public Void oneVoidWrapper() {
57505818
_i = 1;

0 commit comments

Comments
 (0)