Skip to content

Commit 8867c4d

Browse files
fix: Preserve NULL Parameters in case of AOP Proxy
Signed-off-by: Shah Nisarg Pankaj <[email protected]>
1 parent de7d50d commit 8867c4d

File tree

4 files changed

+97
-21
lines changed

4 files changed

+97
-21
lines changed

spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ public static Object invokeSuspendingFunction(Method method, @Nullable Object ta
381381
Continuation<?> continuation = (Continuation<?>) args[args.length -1];
382382
Assert.state(continuation != null, "No Continuation available");
383383
CoroutineContext context = continuation.getContext().minusKey(Job.Key);
384-
return CoroutinesUtils.invokeSuspendingFunction(context, method, target, args);
384+
return CoroutinesUtils.invokeSuspendingFunctionPreserveNulls(context, method, target, args);
385385
}
386386
}
387387

spring-aop/src/test/kotlin/org/springframework/aop/support/AopUtilsKotlinTests.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ class AopUtilsKotlinTests {
4343
}
4444
}
4545

46+
@Test
47+
fun `Invoking suspending function with null argument should not return default value`() {
48+
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handleWithDefaultParam",
49+
String::class. java, Continuation::class.java)!!
50+
val continuation = Continuation<Any>(CoroutineName("test")) { }
51+
val result = AopUtils.invokeJoinpointUsingReflection(WithoutInterface(), method, arrayOf(null, continuation))
52+
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
53+
assertThat(it.block()).isEqualTo(null)
54+
}
55+
}
56+
4657
@Test
4758
fun `Invoking suspending function on bridged method should return Mono`() {
4859
val value = "foo"
@@ -65,6 +76,11 @@ class AopUtilsKotlinTests {
6576
delay(1)
6677
return value
6778
}
79+
80+
suspend fun handleWithDefaultParam(value: String? = "defaultVal") : String? {
81+
delay(1)
82+
return value
83+
}
6884
}
6985

7086
interface ProxyInterface<T> {

spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,35 @@ public static Publisher<?> invokeSuspendingFunction(Method method, Object target
112112
@SuppressWarnings({"DataFlowIssue", "NullAway"})
113113
public static Publisher<?> invokeSuspendingFunction(
114114
CoroutineContext context, Method method, @Nullable Object target, @Nullable Object... args) {
115+
return invokeSuspendingFunctionCore(context, method, target, args, false);
116+
}
117+
118+
/**
119+
* Invoke a suspending function and convert it to {@link Mono} or
120+
* {@link Flux}.
121+
* @param context the coroutine context to use
122+
* @param method the suspending function to invoke
123+
* @param target the target to invoke {@code method} on
124+
* @param args the function arguments. If the {@code Continuation} argument is specified as the last argument
125+
* (typically {@code null}), it is ignored.
126+
* @return the method invocation result as reactive stream
127+
* @throws IllegalArgumentException if {@code method} is not a suspending function
128+
* @since 6.0
129+
* This function preservers the null parameter passed in argument
130+
*/
131+
@SuppressWarnings({"DataFlowIssue", "NullAway"})
132+
public static Publisher<?> invokeSuspendingFunctionPreserveNulls(
133+
CoroutineContext context, Method method, @Nullable Object target, @Nullable Object... args) {
134+
return invokeSuspendingFunctionCore(context, method, target, args, true);
135+
}
136+
137+
private static Publisher<?> invokeSuspendingFunctionCore(
138+
CoroutineContext context,
139+
Method method,
140+
@Nullable Object target,
141+
@Nullable Object[] args,
142+
boolean preserveNulls)
143+
{
115144

116145
Assert.isTrue(KotlinDetector.isSuspendingFunction(method), "Method must be a suspending function");
117146
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
@@ -120,26 +149,7 @@ public static Publisher<?> invokeSuspendingFunction(
120149
KCallablesJvm.setAccessible(function, true);
121150
}
122151
Mono<Object> mono = MonoKt.mono(context, (scope, continuation) -> {
123-
Map<KParameter, Object> argMap = CollectionUtils.newHashMap(args.length + 1);
124-
int index = 0;
125-
for (KParameter parameter : function.getParameters()) {
126-
switch (parameter.getKind()) {
127-
case INSTANCE -> argMap.put(parameter, target);
128-
case VALUE, EXTENSION_RECEIVER -> {
129-
Object arg = args[index];
130-
if (!(parameter.isOptional() && arg == null)) {
131-
KType type = parameter.getType();
132-
if (!(type.isMarkedNullable() && arg == null) &&
133-
type.getClassifier() instanceof KClass<?> kClass &&
134-
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
135-
arg = box(kClass, arg);
136-
}
137-
argMap.put(parameter, arg);
138-
}
139-
index++;
140-
}
141-
}
142-
}
152+
Map<KParameter, Object> argMap = buildArgMap(function, target, args, preserveNulls);
143153
return KCallables.callSuspendBy(function, argMap, continuation);
144154
})
145155
.filter(result -> result != Unit.INSTANCE)
@@ -158,6 +168,40 @@ public static Publisher<?> invokeSuspendingFunction(
158168
return mono;
159169
}
160170

171+
private static Map<KParameter, Object> buildArgMap(
172+
KFunction<?> function,
173+
@Nullable Object target,
174+
@Nullable Object[] args,
175+
boolean preserveNulls) {
176+
177+
Map<KParameter, Object> argMap = CollectionUtils.newHashMap(args.length + 1);
178+
int index = 0;
179+
180+
for (KParameter parameter : function.getParameters()) {
181+
switch (parameter.getKind()) {
182+
case INSTANCE -> argMap.put(parameter, target);
183+
case VALUE, EXTENSION_RECEIVER -> {
184+
Object arg = args[index];
185+
186+
if (!(parameter.isOptional() && arg == null)) {
187+
KType type = parameter.getType();
188+
if (!(type.isMarkedNullable() && arg == null) &&
189+
type.getClassifier() instanceof KClass<?> kClass &&
190+
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
191+
arg = box(kClass, arg);
192+
}
193+
argMap.put(parameter, arg);
194+
} else if(preserveNulls) {
195+
argMap.put(parameter, arg);
196+
}
197+
index++;
198+
}
199+
}
200+
}
201+
return argMap;
202+
}
203+
204+
161205
private static Object box(KClass<?> kClass, @Nullable Object arg) {
162206
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
163207
KType type = constructor.getParameters().get(0).getType();

spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ class CoroutinesUtilsTests {
9393
}
9494
}
9595

96+
@Test
97+
fun invokeSuspendingFunctionWithNullableParameterPreservesNull() {
98+
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingFunctionWithOptionalParameterAndDefaultValue", String::class.java, Continuation::class.java)
99+
val mono = CoroutinesUtils.invokeSuspendingFunctionPreserveNulls(Dispatchers.Unconfined, method, this, null, null) as Mono
100+
runBlocking {
101+
Assertions.assertThat(mono.awaitSingleOrNull()).isNull()
102+
}
103+
}
104+
105+
96106
@Test
97107
fun invokePrivateSuspendingFunction() {
98108
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("privateSuspendingFunction", String::class.java, Continuation::class.java)
@@ -300,6 +310,12 @@ class CoroutinesUtilsTests {
300310
return value
301311
}
302312

313+
suspend fun suspendingFunctionWithOptionalParameterAndDefaultValue(value: String? = "foo"): String? {
314+
delay(1)
315+
return value
316+
}
317+
318+
303319
suspend fun suspendingFunctionWithMono(): Mono<String> {
304320
delay(1)
305321
return Mono.just("foo")

0 commit comments

Comments
 (0)