From 7331706f097fdd97bf50ab54598e9f8a34ee31e2 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Fri, 14 Feb 2025 11:41:07 +0800 Subject: [PATCH] Throw exception if Kotlin projection requires non-null value but null result present Fix GH-3242 Signed-off-by: Yanming Zhou --- .../ProjectingMethodInterceptor.java | 17 ++++++++++++- .../ProjectingMethodInterceptorUnitTests.java | 25 +++++++++++++++++++ .../springframework/data/projection/Person.kt | 25 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/org/springframework/data/projection/Person.kt diff --git a/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java index 35a16c39e5..662b427437 100644 --- a/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java @@ -16,6 +16,7 @@ package org.springframework.data.projection; import java.lang.reflect.Array; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -23,10 +24,13 @@ import java.util.Map; import java.util.Map.Entry; +import kotlin.reflect.KFunction; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.core.CollectionFactory; +import org.springframework.core.KotlinDetector; import org.springframework.core.convert.ConversionService; +import org.springframework.data.util.KotlinReflectionUtils; import org.springframework.data.util.NullableWrapper; import org.springframework.data.util.NullableWrapperConverters; import org.springframework.data.util.TypeInformation; @@ -44,6 +48,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Johannes Englmeier + * @author Yanming Zhou * @since 1.10 */ class ProjectingMethodInterceptor implements MethodInterceptor { @@ -64,11 +69,13 @@ class ProjectingMethodInterceptor implements MethodInterceptor { @Override public Object invoke(@SuppressWarnings("null") @NonNull MethodInvocation invocation) throws Throwable { - TypeInformation type = TypeInformation.fromReturnTypeOf(invocation.getMethod()); + Method method = invocation.getMethod(); + TypeInformation type = TypeInformation.fromReturnTypeOf(method); TypeInformation resultType = type; TypeInformation typeToReturn = type; Object result = delegate.invoke(invocation); + boolean applyWrapper = false; if (NullableWrapperConverters.supports(type.getType()) @@ -83,6 +90,14 @@ public Object invoke(@SuppressWarnings("null") @NonNull MethodInvocation invocat return conversionService.convert(new NullableWrapper(result), typeToReturn.getType()); } + if (result == null) { + KFunction function = KotlinDetector.isKotlinType(method.getDeclaringClass()) ? + KotlinReflectionUtils.findKotlinFunction(method) : null; + if (function != null && !function.getReturnType().isMarkedNullable()) { + throw new IllegalArgumentException("Kotlin function '%s' requires non-null return value".formatted(method.toString())); + } + } + return result; } diff --git a/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java index 9e242a7a0b..e9e5336030 100755 --- a/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java @@ -43,6 +43,7 @@ * @author Saulo Medeiros de Araujo * @author Mark Paluch * @author Christoph Strobl + * @author Yanming Zhou */ @ExtendWith(MockitoExtension.class) class ProjectingMethodInterceptorUnitTests { @@ -204,6 +205,30 @@ void returnsEnumSet() throws Throwable { assertThat(collection).containsOnly(HelperEnum.Helpful); } + @Test + void throwExceptionIfKotlinProjectionRequiresNonNullWithNullResult() throws Throwable { + + MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor, + conversionService); + + when(invocation.getMethod()).thenReturn(Person.class.getMethod("getName")); + when(interceptor.invoke(invocation)).thenReturn(null); + + assertThatIllegalArgumentException().isThrownBy(() -> methodInterceptor.invoke(invocation)); + } + + @Test + void returnsNullIfKotlinProjectionDoesNotRequiresNonNullWithNullResult() throws Throwable { + + MethodInterceptor methodInterceptor = new ProjectingMethodInterceptor(new ProxyProjectionFactory(), interceptor, + conversionService); + + when(invocation.getMethod()).thenReturn(Person.class.getMethod("getAge")); + when(interceptor.invoke(invocation)).thenReturn(null); + + assertThat(methodInterceptor.invoke(invocation)).isNull(); + } + /** * Mocks the {@link Helper} method of the given name to return the given value. * diff --git a/src/test/kotlin/org/springframework/data/projection/Person.kt b/src/test/kotlin/org/springframework/data/projection/Person.kt new file mode 100644 index 0000000000..3646c7b796 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/projection/Person.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.projection + +/** + * @author Yanming Zhou + */ +interface Person { + val name: String + val age: Int? +}