Browse Source

Move and reuse existing nullability validator.

Original Pull Request: #3244
pull/3260/head
Christoph Strobl 11 months ago
parent
commit
2894ff3f04
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 11
      src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java
  2. 9
      src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java
  3. 190
      src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java
  4. 5
      src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
  5. 236
      src/main/java/org/springframework/data/util/NullabilityMethodInvocationValidator.java
  6. 25
      src/test/java/example/NoNullableMarkedInterface.java
  7. 25
      src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java
  8. 3
      src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java
  9. 47
      src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java
  10. 3
      src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java
  11. 2
      src/test/kotlin/org/springframework/data/projection/Person.kt

11
src/main/java/org/springframework/data/projection/ProjectingMethodInterceptor.java

@ -24,13 +24,10 @@ import java.util.List; @@ -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 { @@ -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;
}

9
src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java

@ -31,6 +31,7 @@ import org.springframework.core.convert.support.DefaultConversionService; @@ -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 @@ -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 @@ -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);

190
src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java

@ -15,26 +15,8 @@ @@ -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; @@ -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 {
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;
}
@Deprecated // TODO: do we want to remove this with next major
public class MethodInvocationValidator extends NullabilityMethodInvocationValidator {
@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));
}
}

5
src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java

@ -73,6 +73,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -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 @@ -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 @@ -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()));
}

236
src/main/java/org/springframework/data/util/NullabilityMethodInvocationValidator.java

@ -0,0 +1,236 @@ @@ -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

@ -0,0 +1,25 @@ @@ -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();
}

25
src/test/java/org/springframework/data/projection/ProjectingMethodInterceptorUnitTests.java

@ -43,7 +43,6 @@ import org.springframework.core.convert.support.DefaultConversionService; @@ -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 { @@ -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.
*

3
src/test/java/org/springframework/data/projection/ProjectionIntegrationTests.java

@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; @@ -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 { @@ -44,6 +45,6 @@ class ProjectionIntegrationTests {
}
interface SampleProjection {
String getName();
@Nullable String getName();
}
}

47
src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java

@ -17,6 +17,8 @@ package org.springframework.data.projection; @@ -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; @@ -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 { @@ -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 { @@ -352,6 +393,12 @@ class ProxyProjectionFactoryUnitTests {
LocalDateTime getBirthdate();
}
interface CustomerProjectionWithNullables {
@Nullable String getFirstname();
String getLastname();
}
static class Address {
String zipCode, city;

3
src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java

@ -26,6 +26,7 @@ import org.junit.jupiter.api.BeforeEach; @@ -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 { @@ -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" })

2
src/test/kotlin/org/springframework/data/projection/Person.kt

@ -1,5 +1,5 @@ @@ -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.

Loading…
Cancel
Save