Use Spring's Nullness utility to determine JSpecify nullness.
We now use Nullness.forMethodParameter(…) to introspect method return types and argument types for nullness in addition to Spring's NonNullApi and JSR-305 annotations. Closes #3100
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
[[repositories.nullability]]
|
||||
= Null Handling of Repository Methods
|
||||
|
||||
As of Spring Data 2.0, repository CRUD methods that return an individual aggregate instance use Java 8's `Optional` to indicate the potential absence of a value.
|
||||
Repository CRUD methods that return an individual aggregate instances can use `Optional` to indicate the potential absence of a value.
|
||||
Besides that, Spring Data supports returning the following wrapper types on query methods:
|
||||
|
||||
* `com.google.common.base.Optional`
|
||||
@@ -16,7 +16,74 @@ See "`xref:repositories/query-return-types-reference.adoc[Repository query retur
|
||||
[[repositories.nullability.annotations]]
|
||||
== Nullability Annotations
|
||||
|
||||
=== JSpecify
|
||||
|
||||
As on Spring Framework 7 and Spring Data 4, you can express nullability constraints for repository methods by using https://jspecify.dev/docs/start-here/[JSpecify].
|
||||
JSpecify is well integrated into IntelliJ and Eclipse to provide a tooling-friendly approach and opt-in `null` checks during runtime, as follows:
|
||||
|
||||
* https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`]: Used on the module-, package- and class-level to declare that the default behavior for parameters and return values is, respectively, neither to accept nor to produce `null` values.
|
||||
* https://jspecify.dev/docs/api/org/jspecify/annotations/NonNull.html[`@NonNull`]: Used on a type level for parameter or return values that must not be `null` (not needed value where `@NullMarked` applies).
|
||||
* https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`]: Used on the type level for parameter or return values that can be `null`.
|
||||
* https://jspecify.dev/docs/api/org/jspecify/annotations/NullUnmarked.html[`@NullUnmarked`]: Used on the package-, class-, and method-level to roll back nullness declaration and opt-out from a previous `@NullMarked`.
|
||||
Nullness changes to unspecified in such a case.
|
||||
|
||||
.`@NullMarked` at the package level via a `package-info.java` file
|
||||
[source,java,subs="verbatim,quotes",chomp="-packages",fold="none"]
|
||||
----
|
||||
@NullMarked
|
||||
package org.springframework.core;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
----
|
||||
|
||||
In the various Java files belonging to the package, nullable type usages are defined explicitly with
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`].
|
||||
It is recommended that this annotation is specified just before the related type.
|
||||
|
||||
For example, for a field:
|
||||
|
||||
[source,java,subs="verbatim,quotes"]
|
||||
----
|
||||
private @Nullable String fileEncoding;
|
||||
----
|
||||
|
||||
Or for method parameters and return value:
|
||||
|
||||
[source,java,subs="verbatim,quotes"]
|
||||
----
|
||||
public static @Nullable String buildMessage(@Nullable String message,
|
||||
@Nullable Throwable cause) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
When overriding a method, nullness annotations are not inherited from the superclass method.
|
||||
That means those nullness annotations should be repeated if you just want to override the implementation and keep the same API nullness.
|
||||
|
||||
With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of the array itself.
|
||||
Pay attention to the syntax
|
||||
https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[defined by the Java specification] which may be initially surprising:
|
||||
|
||||
- `@Nullable Object[] array` means individual elements can be null but the array itself can't.
|
||||
- `Object @Nullable [] array` means individual elements can't be null but the array itself can.
|
||||
- `@Nullable Object @Nullable [] array` means both individual elements and the array can be null.
|
||||
|
||||
The Java specifications also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify
|
||||
`@Nullable` should be specified after the last `.` with inner or fully qualified types:
|
||||
|
||||
- `Cache.@Nullable ValueWrapper`
|
||||
- `jakarta.validation.@Nullable Validator`
|
||||
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/NonNull.html[`@NonNull`] and
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/NullUnmarked.html[`@NullUnmarked`] should rarely be needed for typical use cases.
|
||||
|
||||
=== Spring Framework Nullability and JSR-305 Annotations
|
||||
|
||||
You can express nullability constraints for repository methods by using {spring-framework-docs}/core/null-safety.html[Spring Framework's nullability annotations].
|
||||
|
||||
NOTE: As on Spring Framework 7, Spring's nullability annotations are deprecated in favor of JSpecify.
|
||||
Consult the framework documentation on {spring-framework-docs}/core/null-safety.html[Migrating from Spring null-safety annotations to JSpecify] for more information.
|
||||
|
||||
They provide a tooling-friendly approach and opt-in `null` checks during runtime, as follows:
|
||||
|
||||
* {spring-framework-javadoc}/org/springframework/lang/NonNullApi.html[`@NonNullApi`]: Used on the package level to declare that the default behavior for parameters and return values is, respectively, neither to accept nor to produce `null` values.
|
||||
@@ -59,6 +126,7 @@ interface UserRepository extends Repository<User, Long> {
|
||||
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); <4>
|
||||
}
|
||||
----
|
||||
|
||||
<1> The repository resides in a package (or sub-package) for which we have defined non-null behavior.
|
||||
<2> Throws an `EmptyResultDataAccessException` when the query does not produce a result.
|
||||
Throws an `IllegalArgumentException` when the `emailAddress` handed to the method is `null`.
|
||||
@@ -85,6 +153,7 @@ interface UserRepository : Repository<User, String> {
|
||||
fun findByFirstname(firstname: String?): User? <2>
|
||||
}
|
||||
----
|
||||
|
||||
<1> The method defines both the parameter and the result as non-nullable (the Kotlin default).
|
||||
The Kotlin compiler rejects method invocations that pass `null` to the method.
|
||||
If the query yields an empty result, an `EmptyResultDataAccessException` is thrown.
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
@@ -60,6 +61,11 @@ public class NullnessMethodInvocationValidator implements MethodInterceptor {
|
||||
*/
|
||||
public static boolean supports(Class<?> type) {
|
||||
|
||||
if (type.getPackage() != null
|
||||
&& type.getPackage().isAnnotationPresent(NullMarked.class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(type)
|
||||
|| NullableUtils.isNonNull(type, ElementType.METHOD)
|
||||
|| NullableUtils.isNonNull(type, ElementType.PARAMETER);
|
||||
|
||||
@@ -32,6 +32,7 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -121,7 +122,7 @@ class RepositoryFactorySupportUnitTests {
|
||||
factory.getRepository(ObjectRepository.class);
|
||||
|
||||
verify(listener, times(1)).onCreation(any(MyRepositoryQuery.class));
|
||||
verify(otherListener, times(3)).onCreation(any(RepositoryQuery.class));
|
||||
verify(otherListener, times(4)).onCreation(any(RepositoryQuery.class));
|
||||
}
|
||||
|
||||
@Test // DATACMNS-1538
|
||||
@@ -253,7 +254,8 @@ class RepositoryFactorySupportUnitTests {
|
||||
@Test // GH-3090
|
||||
void capturesRepositoryMetadata() {
|
||||
|
||||
record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) {}
|
||||
record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) {
|
||||
}
|
||||
|
||||
when(factory.queryOne.execute(any(Object[].class)))
|
||||
.then(invocation -> new Metadata(RepositoryMethodContextHolder.getContext(),
|
||||
@@ -298,7 +300,7 @@ class RepositoryFactorySupportUnitTests {
|
||||
|
||||
var repository1 = factory.getRepository(ObjectAndQuerydslRepository.class, backingRepo);
|
||||
var repository2 = factory.getRepository(ObjectAndQuerydslRepository.class, backingRepo);
|
||||
repository1.findByFoo();
|
||||
repository1.findByFoo(null);
|
||||
repository2.deleteAll();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
@@ -430,6 +432,20 @@ class RepositoryFactorySupportUnitTests {
|
||||
() -> repository.findByClass(null)) //
|
||||
.isInstanceOf(IllegalArgumentException.class) //
|
||||
.hasMessageContaining("must not be null");
|
||||
|
||||
}
|
||||
|
||||
@Test // GH-3100
|
||||
void considersRequiredParameterThroughJspecify() {
|
||||
|
||||
var repository = factory.getRepository(ObjectRepository.class);
|
||||
|
||||
assertThatNoException().isThrownBy(() -> repository.findByFoo(null));
|
||||
|
||||
assertThatThrownBy( //
|
||||
() -> repository.findByNonNullFoo(null)) //
|
||||
.isInstanceOf(IllegalArgumentException.class) //
|
||||
.hasMessageContaining("must not be null");
|
||||
}
|
||||
|
||||
@Test // DATACMNS-1154
|
||||
@@ -565,8 +581,10 @@ class RepositoryFactorySupportUnitTests {
|
||||
@Nullable
|
||||
Object findByClass(Class<?> clazz);
|
||||
|
||||
@Nullable
|
||||
Object findByFoo();
|
||||
@org.jspecify.annotations.Nullable
|
||||
Object findByFoo(@org.jspecify.annotations.Nullable Object foo);
|
||||
|
||||
Object findByNonNullFoo(@NonNull Object foo);
|
||||
|
||||
@Nullable
|
||||
Object save(Object entity);
|
||||
|
||||
Reference in New Issue
Block a user