Move and reuse existing nullability validator.
Original Pull Request: #3244
This commit is contained in:
@@ -24,13 +24,10 @@ import java.util.List;
|
||||
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;
|
||||
@@ -90,14 +87,6 @@ class ProjectingMethodInterceptor implements MethodInterceptor {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.data.convert.Jsr310Converters;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.data.util.NullabilityMethodInvocationValidator;
|
||||
import org.springframework.data.util.NullableWrapperConverters;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -66,6 +67,9 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware
|
||||
private final Lazy<DefaultMethodInvokingMethodInterceptor> defaultMethodInvokingMethodInterceptor = Lazy
|
||||
.of(DefaultMethodInvokingMethodInterceptor::new);
|
||||
|
||||
private final Lazy<NullabilityMethodInvocationValidator> nullabilityValidator = Lazy
|
||||
.of(NullabilityMethodInvocationValidator::new);
|
||||
|
||||
/**
|
||||
* Creates a new {@link ProxyProjectionFactory}.
|
||||
*/
|
||||
@@ -119,6 +123,11 @@ class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware
|
||||
}
|
||||
|
||||
factory.addAdvice(new TargetAwareMethodInterceptor(source.getClass()));
|
||||
|
||||
if(NullabilityMethodInvocationValidator.supports(projectionType)) {
|
||||
factory.addAdvice(nullabilityValidator.get());
|
||||
}
|
||||
|
||||
factory.addAdvice(getMethodInterceptor(source, projectionType));
|
||||
|
||||
return (T) factory.getProxy(classLoader == null ? ClassUtils.getDefaultClassLoader() : classLoader);
|
||||
|
||||
@@ -15,26 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.repository.core.support;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.data.util.KotlinReflectionUtils;
|
||||
import org.springframework.data.util.NullableUtils;
|
||||
import org.springframework.data.util.ReflectionUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.data.util.NullabilityMethodInvocationValidator;
|
||||
|
||||
/**
|
||||
* Interceptor enforcing required return value and method parameter constraints declared on repository query methods.
|
||||
@@ -42,169 +24,17 @@ import org.springframework.util.ObjectUtils;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Johannes Englmeier
|
||||
* @author Christoph Strobl
|
||||
* @since 2.0
|
||||
* @see org.springframework.lang.NonNull
|
||||
* @see ReflectionUtils#isNullable(MethodParameter)
|
||||
* @see NullableUtils
|
||||
* @see org.springframework.data.util.ReflectionUtils#isNullable(org.springframework.core.MethodParameter)
|
||||
* @see org.springframework.data.util.NullableUtils
|
||||
* @deprecated use {@link NullabilityMethodInvocationValidator} instead.
|
||||
*/
|
||||
public class MethodInvocationValidator implements MethodInterceptor {
|
||||
@Deprecated // TODO: do we want to remove this with next major
|
||||
public class MethodInvocationValidator extends NullabilityMethodInvocationValidator {
|
||||
|
||||
private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
|
||||
private final Map<Method, Nullability> nullabilityCache = new ConcurrentHashMap<>(16);
|
||||
|
||||
/**
|
||||
* Returns {@literal true} if the {@code repositoryInterface} is supported by this interceptor.
|
||||
*
|
||||
* @param repositoryInterface the interface class.
|
||||
* @return {@literal true} if the {@code repositoryInterface} is supported by this interceptor.
|
||||
*/
|
||||
public static boolean supports(Class<?> repositoryInterface) {
|
||||
|
||||
return KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(repositoryInterface)
|
||||
|| NullableUtils.isNonNull(repositoryInterface, ElementType.METHOD)
|
||||
|| NullableUtils.isNonNull(repositoryInterface, ElementType.PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
|
||||
|
||||
Method method = invocation.getMethod();
|
||||
Nullability nullability = nullabilityCache.get(method);
|
||||
|
||||
if (nullability == null) {
|
||||
|
||||
nullability = Nullability.of(method, discoverer);
|
||||
nullabilityCache.put(method, nullability);
|
||||
}
|
||||
|
||||
Object[] arguments = invocation.getArguments();
|
||||
|
||||
for (int i = 0; i < method.getParameterCount(); i++) {
|
||||
|
||||
if (nullability.isNullableParameter(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((arguments.length < i) || (arguments[i] == null)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Parameter %s in %s.%s must not be null", nullability.getMethodParameterName(i),
|
||||
ClassUtils.getShortName(method.getDeclaringClass()), method.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
Object result = invocation.proceed();
|
||||
|
||||
if ((result == null) && !nullability.isNullableReturn()) {
|
||||
throw new EmptyResultDataAccessException("Result must not be null", 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static final class Nullability {
|
||||
|
||||
private final boolean nullableReturn;
|
||||
private final boolean[] nullableParameters;
|
||||
private final MethodParameter[] methodParameters;
|
||||
|
||||
private Nullability(boolean nullableReturn, boolean[] nullableParameters, MethodParameter[] methodParameters) {
|
||||
this.nullableReturn = nullableReturn;
|
||||
this.nullableParameters = nullableParameters;
|
||||
this.methodParameters = methodParameters;
|
||||
}
|
||||
|
||||
static Nullability of(Method method, ParameterNameDiscoverer discoverer) {
|
||||
|
||||
boolean nullableReturn = isNullableParameter(new MethodParameter(method, -1));
|
||||
boolean[] nullableParameters = new boolean[method.getParameterCount()];
|
||||
MethodParameter[] methodParameters = new MethodParameter[method.getParameterCount()];
|
||||
|
||||
for (int i = 0; i < method.getParameterCount(); i++) {
|
||||
|
||||
MethodParameter parameter = new MethodParameter(method, i);
|
||||
parameter.initParameterNameDiscovery(discoverer);
|
||||
nullableParameters[i] = isNullableParameter(parameter);
|
||||
methodParameters[i] = parameter;
|
||||
}
|
||||
|
||||
return new Nullability(nullableReturn, nullableParameters, methodParameters);
|
||||
}
|
||||
|
||||
String getMethodParameterName(int index) {
|
||||
|
||||
String parameterName = methodParameters[index].getParameterName();
|
||||
|
||||
if (parameterName == null) {
|
||||
parameterName = String.format("of type %s at index %d",
|
||||
ClassUtils.getShortName(methodParameters[index].getParameterType()), index);
|
||||
}
|
||||
|
||||
return parameterName;
|
||||
}
|
||||
|
||||
boolean isNullableReturn() {
|
||||
return nullableReturn;
|
||||
}
|
||||
|
||||
boolean isNullableParameter(int index) {
|
||||
return nullableParameters[index];
|
||||
}
|
||||
|
||||
private static boolean isNullableParameter(MethodParameter parameter) {
|
||||
|
||||
return requiresNoValue(parameter) || NullableUtils.isExplicitNullable(parameter)
|
||||
|| (KotlinReflectionUtils.isSupportedKotlinClass(parameter.getDeclaringClass())
|
||||
&& ReflectionUtils.isNullable(parameter));
|
||||
}
|
||||
|
||||
private static boolean requiresNoValue(MethodParameter parameter) {
|
||||
return ReflectionUtils.isVoid(parameter.getParameterType());
|
||||
}
|
||||
|
||||
public boolean[] getNullableParameters() {
|
||||
return this.nullableParameters;
|
||||
}
|
||||
|
||||
public MethodParameter[] getMethodParameters() {
|
||||
return this.methodParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof Nullability that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nullableReturn != that.nullableReturn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(nullableParameters, that.nullableParameters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ObjectUtils.nullSafeEquals(methodParameters, that.methodParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (nullableReturn ? 1 : 0);
|
||||
result = (31 * result) + ObjectUtils.nullSafeHashCode(nullableParameters);
|
||||
result = (31 * result) + ObjectUtils.nullSafeHashCode(methodParameters);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodInvocationValidator.Nullability(nullableReturn=" + this.isNullableReturn() + ", nullableParameters="
|
||||
+ java.util.Arrays.toString(this.getNullableParameters()) + ", methodParameters="
|
||||
+ java.util.Arrays.deepToString(this.getMethodParameters()) + ")";
|
||||
}
|
||||
}
|
||||
public MethodInvocationValidator() {
|
||||
super((invocation) -> new EmptyResultDataAccessException("Result must not be null", 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
|
||||
import org.springframework.data.repository.util.QueryExecutionConverters;
|
||||
import org.springframework.data.spel.EvaluationContextProvider;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.data.util.NullabilityMethodInvocationValidator;
|
||||
import org.springframework.data.util.ReflectionUtils;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
@@ -334,7 +335,7 @@ public abstract class RepositoryFactorySupport
|
||||
* @return the implemented repository interface.
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
@SuppressWarnings({ "unchecked", "deprecation" })
|
||||
public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
@@ -399,7 +400,7 @@ public abstract class RepositoryFactorySupport
|
||||
result.setTarget(target);
|
||||
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
|
||||
|
||||
if (MethodInvocationValidator.supports(repositoryInterface)) {
|
||||
if (NullabilityMethodInvocationValidator.supports(repositoryInterface)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(LogMessage.format("Register MethodInvocationValidator for %s…", repositoryInterface.getName()));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2017-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.util;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import kotlin.reflect.KFunction;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Interceptor enforcing required return value and method parameter constraints declared on repository query methods.
|
||||
* Supports Kotlin nullability markers and JSR-305 Non-null annotations.
|
||||
* Originally implemented via {@link org.springframework.data.repository.core.support.MethodInvocationValidator}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Johannes Englmeier
|
||||
* @author Christoph Strobl
|
||||
* @since 3.5
|
||||
* @see org.springframework.lang.NonNull
|
||||
* @see ReflectionUtils#isNullable(MethodParameter)
|
||||
* @see NullableUtils
|
||||
*/
|
||||
public class NullabilityMethodInvocationValidator implements MethodInterceptor {
|
||||
|
||||
private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
|
||||
private final Map<Method, Nullability> nullabilityCache = new ConcurrentHashMap<>(16);
|
||||
private final Function<MethodInvocation, RuntimeException> errorFunction;
|
||||
|
||||
public NullabilityMethodInvocationValidator() {
|
||||
this((invocation) -> new NullPointerException("Method marked non nullable used with null value. If this is by design consider providing additional metadata using @Nullable annotations."));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorFunction custom function creating the error in case of failure.
|
||||
*/
|
||||
protected NullabilityMethodInvocationValidator(Function<MethodInvocation, RuntimeException> errorFunction) {
|
||||
this.errorFunction = errorFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@literal true} if the {@code type} is supported by this interceptor.
|
||||
*
|
||||
* @param type the interface class.
|
||||
* @return {@literal true} if the {@code type} is supported by this interceptor.
|
||||
*/
|
||||
public static boolean supports(Class<?> type) {
|
||||
|
||||
return KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(type)
|
||||
|| NullableUtils.isNonNull(type, ElementType.METHOD)
|
||||
|| NullableUtils.isNonNull(type, ElementType.PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
|
||||
|
||||
Method method = invocation.getMethod();
|
||||
Nullability nullability = nullabilityCache.get(method);
|
||||
|
||||
if (nullability == null) {
|
||||
|
||||
nullability = Nullability.of(method, discoverer);
|
||||
nullabilityCache.put(method, nullability);
|
||||
}
|
||||
|
||||
Object[] arguments = invocation.getArguments();
|
||||
|
||||
for (int i = 0; i < method.getParameterCount(); i++) {
|
||||
|
||||
if (nullability.isNullableParameter(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((arguments.length < i) || (arguments[i] == null)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Parameter %s in %s.%s must not be null", nullability.getMethodParameterName(i),
|
||||
ClassUtils.getShortName(method.getDeclaringClass()), method.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
Object result = invocation.proceed();
|
||||
|
||||
if ((result == null) && !nullability.isNullableReturn()) {
|
||||
throw errorFunction.apply(invocation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static final class Nullability {
|
||||
|
||||
private final boolean nullableReturn;
|
||||
private final boolean[] nullableParameters;
|
||||
private final MethodParameter[] methodParameters;
|
||||
|
||||
private Nullability(boolean nullableReturn, boolean[] nullableParameters, MethodParameter[] methodParameters) {
|
||||
this.nullableReturn = nullableReturn;
|
||||
this.nullableParameters = nullableParameters;
|
||||
this.methodParameters = methodParameters;
|
||||
}
|
||||
|
||||
static Nullability of(Method method, ParameterNameDiscoverer discoverer) {
|
||||
|
||||
boolean nullableReturn = isNullableParameter(new MethodParameter(method, -1));
|
||||
boolean[] nullableParameters = new boolean[method.getParameterCount()];
|
||||
MethodParameter[] methodParameters = new MethodParameter[method.getParameterCount()];
|
||||
|
||||
for (int i = 0; i < method.getParameterCount(); i++) {
|
||||
|
||||
MethodParameter parameter = new MethodParameter(method, i);
|
||||
parameter.initParameterNameDiscovery(discoverer);
|
||||
nullableParameters[i] = isNullableParameter(parameter);
|
||||
methodParameters[i] = parameter;
|
||||
}
|
||||
|
||||
return new Nullability(nullableReturn, nullableParameters, methodParameters);
|
||||
}
|
||||
|
||||
String getMethodParameterName(int index) {
|
||||
|
||||
String parameterName = methodParameters[index].getParameterName();
|
||||
|
||||
if (parameterName == null) {
|
||||
parameterName = String.format("of type %s at index %d",
|
||||
ClassUtils.getShortName(methodParameters[index].getParameterType()), index);
|
||||
}
|
||||
|
||||
return parameterName;
|
||||
}
|
||||
|
||||
boolean isNullableReturn() {
|
||||
return nullableReturn;
|
||||
}
|
||||
|
||||
boolean isNullableParameter(int index) {
|
||||
return nullableParameters[index];
|
||||
}
|
||||
|
||||
private static boolean isNullableParameter(MethodParameter parameter) {
|
||||
|
||||
return requiresNoValue(parameter) || NullableUtils.isExplicitNullable(parameter)
|
||||
|| (KotlinReflectionUtils.isSupportedKotlinClass(parameter.getDeclaringClass())
|
||||
&& (ReflectionUtils.isNullable(parameter) || allowNullableReturn(parameter.getMethod())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check method return nullability
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
private static boolean allowNullableReturn(@Nullable Method method) {
|
||||
|
||||
if(method == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KFunction<?> function = KotlinDetector.isKotlinType(method.getDeclaringClass()) ?
|
||||
KotlinReflectionUtils.findKotlinFunction(method) : null;
|
||||
return function != null && function.getReturnType().isMarkedNullable();
|
||||
}
|
||||
|
||||
private static boolean requiresNoValue(MethodParameter parameter) {
|
||||
return ReflectionUtils.isVoid(parameter.getParameterType());
|
||||
}
|
||||
|
||||
public boolean[] getNullableParameters() {
|
||||
return this.nullableParameters;
|
||||
}
|
||||
|
||||
public MethodParameter[] getMethodParameters() {
|
||||
return this.methodParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof Nullability that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nullableReturn != that.nullableReturn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(nullableParameters, that.nullableParameters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ObjectUtils.nullSafeEquals(methodParameters, that.methodParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (nullableReturn ? 1 : 0);
|
||||
result = (31 * result) + ObjectUtils.nullSafeHashCode(nullableParameters);
|
||||
result = (31 * result) + ObjectUtils.nullSafeHashCode(methodParameters);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodInvocationValidator.Nullability(nullableReturn=" + this.isNullableReturn() + ", nullableParameters="
|
||||
+ java.util.Arrays.toString(this.getNullableParameters()) + ", methodParameters="
|
||||
+ java.util.Arrays.deepToString(this.getMethodParameters()) + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/test/java/example/NoNullableMarkedInterface.java
Normal file
25
src/test/java/example/NoNullableMarkedInterface.java
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 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 example;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface NoNullableMarkedInterface {
|
||||
|
||||
String getFirstname();
|
||||
String getLastname();
|
||||
}
|
||||
@@ -43,7 +43,6 @@ import org.springframework.core.convert.support.DefaultConversionService;
|
||||
* @author Saulo Medeiros de Araujo
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
* @author Yanming Zhou
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProjectingMethodInterceptorUnitTests {
|
||||
@@ -205,30 +204,6 @@ class ProjectingMethodInterceptorUnitTests {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.Configuration.ConfigurationBuilder;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.jayway.jsonpath.Option;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Integration tests for projections.
|
||||
@@ -44,6 +45,6 @@ class ProjectionIntegrationTests {
|
||||
}
|
||||
|
||||
interface SampleProjection {
|
||||
String getName();
|
||||
@Nullable String getName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.data.projection;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import example.NoNullableMarkedInterface;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Calendar;
|
||||
@@ -32,6 +34,7 @@ import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.TargetClassAware;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
/**
|
||||
@@ -342,6 +345,44 @@ class ProxyProjectionFactoryUnitTests {
|
||||
assertThat(excerpt.getBirthdate()).contains(LocalDateTime.of(1967, 1, 9, 0, 0));
|
||||
}
|
||||
|
||||
@Test // GH-3242
|
||||
void projectionFactoryConsidersKotlinNullabilityConstraints() {
|
||||
|
||||
var source = new HashMap<String, Object>(2);
|
||||
source.put("name", null);
|
||||
source.put("age", null);
|
||||
|
||||
Person projection = factory.createProjection(Person.class, source);
|
||||
|
||||
assertThatNoException().isThrownBy(projection::getAge);
|
||||
assertThatExceptionOfType(NullPointerException.class).isThrownBy(projection::getName);
|
||||
}
|
||||
|
||||
@Test // GH-3242
|
||||
void projectionFactoryConsidersNullabilityAnnotations() {
|
||||
|
||||
var source = new HashMap<String, Object>(2);
|
||||
source.put("firstname", null);
|
||||
source.put("lastname", null);
|
||||
|
||||
CustomerProjectionWithNullables projection = factory.createProjection(CustomerProjectionWithNullables.class, source);
|
||||
|
||||
assertThatNoException().isThrownBy(projection::getFirstname);
|
||||
assertThatExceptionOfType(NullPointerException.class).isThrownBy(projection::getLastname);
|
||||
}
|
||||
|
||||
@Test // GH-3242
|
||||
void projectionFactoryIgnoresNullabilityAnnotationsOnUnmanagedPackage() {
|
||||
|
||||
var source = new HashMap<String, Object>(2);
|
||||
source.put("firstname", null);
|
||||
source.put("lastname", null);
|
||||
|
||||
NoNullableMarkedInterface projection = factory.createProjection(NoNullableMarkedInterface.class, source);
|
||||
|
||||
assertThatNoException().isThrownBy(projection::getFirstname);
|
||||
assertThatNoException().isThrownBy(projection::getLastname);
|
||||
}
|
||||
|
||||
interface Contact {}
|
||||
|
||||
@@ -352,6 +393,12 @@ class ProxyProjectionFactoryUnitTests {
|
||||
LocalDateTime getBirthdate();
|
||||
}
|
||||
|
||||
interface CustomerProjectionWithNullables {
|
||||
|
||||
@Nullable String getFirstname();
|
||||
String getLastname();
|
||||
}
|
||||
|
||||
static class Address {
|
||||
|
||||
String zipCode, city;
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -223,7 +224,7 @@ class JsonProjectingMethodInterceptorFactoryUnitTests {
|
||||
|
||||
// Not available in the payload
|
||||
@JsonPath("$.lastname")
|
||||
String getLastname();
|
||||
@Nullable String getLastname();
|
||||
|
||||
// First one not available in the payload
|
||||
@JsonPath({ "$.lastname", "$.firstname" })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024-2025 the original author or authors.
|
||||
* Copyright 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.
|
||||
|
||||
Reference in New Issue
Block a user