Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Nullability API #3115

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>3.4.0-SNAPSHOT</version>
<version>3.4.0-GH-3100-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
Expand Down Expand Up @@ -41,40 +43,48 @@
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
Expand All @@ -92,19 +102,28 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jaxb}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation-api}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>0.3.0</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ They provide a tooling-friendly approach and opt-in `null` checks during runtime

Spring annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=305[JSR 305] annotations (a dormant but widely used JSR).
JSR 305 meta-annotations let tooling vendors (such as https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html[IDEA], https://help.eclipse.org/latest/index.jsp?topic=/org.eclipse.jdt.doc.user/tasks/task-using_external_null_annotations.htm[Eclipse], and link:https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[Kotlin]) provide null-safety support in a generic way, without having to hard-code support for Spring annotations.

NOTE: Spring Data can evaluate link:https://jspecify.dev/[JSpecify] annotations to determine nullability rules.0 JSpecify is still in development and might experience slight changes.

To enable runtime checking of nullability constraints for query methods, you need to activate non-nullability on the package level by using Spring’s `@NonNullApi` in `package-info.java`, as shown in the following example:

.Declaring Non-nullability in `package-info.java`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@

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.Nullability;
import org.springframework.data.util.Nullability.MethodNullability;
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;

/**
Expand All @@ -45,12 +45,12 @@
* @since 2.0
* @see org.springframework.lang.NonNull
* @see ReflectionUtils#isNullable(MethodParameter)
* @see NullableUtils
* @see org.springframework.data.util.Nullability
*/
public class MethodInvocationValidator implements MethodInterceptor {

private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
private final Map<Method, Nullability> nullabilityCache = new ConcurrentHashMap<>(16);
private final Map<Method, CachedNullability> nullabilityCache = new ConcurrentHashMap<>(16);

/**
* Returns {@literal true} if the {@code repositoryInterface} is supported by this interceptor.
Expand All @@ -60,21 +60,26 @@ public class MethodInvocationValidator implements MethodInterceptor {
*/
public static boolean supports(Class<?> repositoryInterface) {

return KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(repositoryInterface)
|| NullableUtils.isNonNull(repositoryInterface, ElementType.METHOD)
|| NullableUtils.isNonNull(repositoryInterface, ElementType.PARAMETER);
if (KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(repositoryInterface)) {
return true;
}

org.springframework.data.util.Nullability.Introspector introspector = org.springframework.data.util.Nullability
.introspect(repositoryInterface);

return introspector.isDeclared(ElementType.METHOD) || introspector.isDeclared(ElementType.PARAMETER);
}

@Nullable
@Override
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {

Method method = invocation.getMethod();
Nullability nullability = nullabilityCache.get(method);
CachedNullability nullability = nullabilityCache.get(method);

if (nullability == null) {

nullability = Nullability.of(method, discoverer);
nullability = CachedNullability.of(method, discoverer);
nullabilityCache.put(method, nullability);
}

Expand Down Expand Up @@ -102,33 +107,36 @@ public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) thro
return result;
}

static final class Nullability {
static final class CachedNullability {

private final boolean nullableReturn;
private final boolean[] nullableParameters;
private final MethodParameter[] methodParameters;

private Nullability(boolean nullableReturn, boolean[] nullableParameters, MethodParameter[] methodParameters) {
private CachedNullability(boolean nullableReturn, boolean[] nullableParameters,
MethodParameter[] methodParameters) {
this.nullableReturn = nullableReturn;
this.nullableParameters = nullableParameters;
this.methodParameters = methodParameters;
}

static Nullability of(Method method, ParameterNameDiscoverer discoverer) {
static CachedNullability of(Method method, ParameterNameDiscoverer discoverer) {

MethodNullability methodNullability = Nullability.forMethod(method);

boolean nullableReturn = isNullableParameter(new MethodParameter(method, -1));
boolean nullableReturn = isNullableParameter(methodNullability, 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);
nullableParameters[i] = isNullableParameter(methodNullability, parameter);
methodParameters[i] = parameter;
}

return new Nullability(nullableReturn, nullableParameters, methodParameters);
return new CachedNullability(nullableReturn, nullableParameters, methodParameters);
}

String getMethodParameterName(int index) {
Expand All @@ -151,9 +159,9 @@ boolean isNullableParameter(int index) {
return nullableParameters[index];
}

private static boolean isNullableParameter(MethodParameter parameter) {
private static boolean isNullableParameter(MethodNullability methodNullability, MethodParameter parameter) {

return requiresNoValue(parameter) || NullableUtils.isExplicitNullable(parameter)
return requiresNoValue(parameter) || methodNullability.forParameter(parameter).isNullable()
|| (KotlinReflectionUtils.isSupportedKotlinClass(parameter.getDeclaringClass())
&& ReflectionUtils.isNullable(parameter));
}
Expand All @@ -177,7 +185,7 @@ public boolean equals(@Nullable Object o) {
return true;
}

if (!(o instanceof Nullability that)) {
if (!(o instanceof CachedNullability that)) {
return false;
}

Expand Down
Loading