Skip to content

Commit ba1485a

Browse files
committed
Add support for JSpecify.
1 parent 4146ba8 commit ba1485a

File tree

6 files changed

+178
-20
lines changed

6 files changed

+178
-20
lines changed

pom.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<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">
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
35

46
<modelVersion>4.0.0</modelVersion>
57

@@ -116,6 +118,13 @@
116118
<optional>true</optional>
117119
</dependency>
118120

121+
<dependency>
122+
<groupId>org.jspecify</groupId>
123+
<artifactId>jspecify</artifactId>
124+
<version>0.3.0</version>
125+
<optional>true</optional>
126+
</dependency>
127+
119128
<dependency>
120129
<groupId>com.google.code.findbugs</groupId>
121130
<artifactId>jsr305</artifactId>

src/main/antora/modules/ROOT/pages/repositories/null-handling.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ They provide a tooling-friendly approach and opt-in `null` checks during runtime
2525

2626
Spring annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=305[JSR 305] annotations (a dormant but widely used JSR).
2727
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.
28+
29+
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.
30+
2831
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:
2932

3033
.Declaring Non-nullability in `package-info.java`

src/main/java/org/springframework/data/util/NullabilityIntrospector.java

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import java.lang.annotation.Annotation;
1919
import java.lang.annotation.ElementType;
2020
import java.lang.reflect.AnnotatedElement;
21+
import java.lang.reflect.AnnotatedType;
22+
import java.lang.reflect.Executable;
2123
import java.lang.reflect.Method;
2224
import java.lang.reflect.Parameter;
2325
import java.util.ArrayList;
@@ -52,8 +54,12 @@ class NullabilityIntrospector implements Nullability.Introspector {
5254
providers.add(new Jsr305Provider());
5355
}
5456

55-
if (JakartaAnnotation.isAvailable()) {
56-
providers.add(new JakartaAnnotation());
57+
if (JakartaAnnotationProvider.isAvailable()) {
58+
providers.add(new JakartaAnnotationProvider());
59+
}
60+
61+
if (JSpecifyAnnotationProvider.isAvailable()) {
62+
providers.add(new JSpecifyAnnotationProvider());
5763
}
5864

5965
providers.add(new SpringProvider());
@@ -251,7 +257,7 @@ private static boolean isInScope(Annotation annotation, ElementType elementType)
251257
}
252258

253259
/**
254-
* Introspect {@link Annotation} for being either a meta-annotation composed from {@code Nonnull} or {code Nonnull}
260+
* Introspect {@link Annotation} for being either a meta-annotation composed of {@code Nonnull} or {code Nonnull}
255261
* itself expressing non-nullability.
256262
*
257263
* @param annotation
@@ -274,56 +280,124 @@ static boolean isNullable(Annotation annotation) {
274280
}
275281

276282
/**
277-
* Provider based on the JSR-305 (dormant) spec. Elements can be either annotated with
278-
* {@code @Nonnull}/{@code @Nullable} directly or through meta-annotations that are composed of
279-
* {@code @Nonnull}/{@code @Nullable} and {@code @TypeQualifierDefault}.
283+
* Simplified variant of {@link Jsr305Provider} without {@code when} and {@code @TypeQualifierDefault} support.
280284
*/
281-
static class JakartaAnnotation extends NullabilityProvider {
285+
static class JakartaAnnotationProvider extends SimpleAnnotationNullabilityProvider {
282286

283287
private static final Class<Annotation> NON_NULL = findClass("jakarta.annotation.Nonnull");
284288
private static final Class<Annotation> NULLABLE = findClass("jakarta.annotation.Nullable");
285289

290+
JakartaAnnotationProvider() {
291+
super(NON_NULL, NULLABLE);
292+
}
293+
286294
public static boolean isAvailable() {
287295
return NON_NULL != null && NULLABLE != null;
288296
}
297+
}
298+
299+
/**
300+
* Provider for JSpecify annotations.
301+
*/
302+
static class JSpecifyAnnotationProvider extends NullabilityProvider {
303+
304+
private static final Class<Annotation> NON_NULL = findClass("org.jspecify.annotations.NonNull");
305+
private static final Class<Annotation> NULLABLE = findClass("org.jspecify.annotations.Nullable");
306+
private static final Class<Annotation> NULL_MARKED = findClass("org.jspecify.annotations.NullMarked");
307+
private static final Class<Annotation> NULL_UNMARKED = findClass("org.jspecify.annotations.NullUnmarked");
308+
309+
public static boolean isAvailable() {
310+
return NON_NULL != null && NULLABLE != null && NULL_MARKED != null && NULL_UNMARKED != null;
311+
}
289312

290313
@Override
291314
Spec evaluate(AnnotatedElement element, ElementType elementType) {
292315

293-
if (element.isAnnotationPresent(NULLABLE) || MergedAnnotations.from(element).isPresent(NULLABLE)) {
294-
return Spec.NULLABLE;
316+
Annotation[] annotations = element.getAnnotations();
317+
AnnotatedType annotatedType = null;
318+
319+
if (element instanceof Parameter p) {
320+
321+
Spec result = evaluate(p.getAnnotatedType(), elementType);
322+
323+
if (result != Spec.UNSPECIFIED) {
324+
return result;
325+
}
295326
}
296327

297-
Annotation[] annotations = element.getAnnotations();
328+
if (element instanceof Executable e) {
298329

330+
Spec result = evaluate(e.getAnnotatedReturnType(), elementType);
331+
332+
if (result != Spec.UNSPECIFIED) {
333+
return result;
334+
}
335+
}
336+
337+
return evaluate(annotations);
338+
}
339+
340+
private static Spec evaluate(Annotation[] annotations) {
299341
for (Annotation annotation : annotations) {
300342

301-
if (test(NON_NULL, annotation)) {
343+
if (SimpleAnnotationNullabilityProvider.test(NULL_UNMARKED, annotation)) {
344+
return Spec.UNSPECIFIED;
345+
}
346+
347+
if (SimpleAnnotationNullabilityProvider.test(NULL_MARKED, annotation)
348+
|| SimpleAnnotationNullabilityProvider.test(NON_NULL, annotation)) {
302349
return Spec.NON_NULL;
303350
}
304351

305-
if (test(NULLABLE, annotation)) {
352+
if (SimpleAnnotationNullabilityProvider.test(NULLABLE, annotation)) {
306353
return Spec.NULLABLE;
307354
}
308355
}
309356

310357
return Spec.UNSPECIFIED;
311358
}
359+
}
360+
361+
/**
362+
* Annotation-based {@link NullabilityProvider} leveraging simple or meta-annotations.
363+
*/
364+
static class SimpleAnnotationNullabilityProvider extends NullabilityProvider {
312365

313-
private static boolean test(Class<Annotation> annotationClass, Annotation annotation) {
366+
private final Class<Annotation> nonNull;
367+
private final Class<Annotation> nullable;
314368

315-
if (annotation.annotationType().equals(annotationClass)) {
316-
return true;
369+
SimpleAnnotationNullabilityProvider(Class<Annotation> nonNull, Class<Annotation> nullable) {
370+
this.nonNull = nonNull;
371+
this.nullable = nullable;
372+
}
373+
374+
@Override
375+
Spec evaluate(AnnotatedElement element, ElementType elementType) {
376+
377+
Annotation[] annotations = element.getAnnotations();
378+
379+
for (Annotation annotation : annotations) {
380+
381+
if (test(nonNull, annotation)) {
382+
return Spec.NON_NULL;
383+
}
384+
385+
if (test(nullable, annotation)) {
386+
return Spec.NULLABLE;
387+
}
317388
}
318389

319-
MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType());
320-
if (annotations.isPresent(annotationClass)) {
321-
Annotation meta = annotations.get(annotationClass).synthesize();
390+
return Spec.UNSPECIFIED;
391+
}
392+
393+
static boolean test(Class<Annotation> annotationClass, Annotation annotation) {
322394

395+
if (annotation.annotationType().equals(annotationClass)) {
323396
return true;
324397
}
325398

326-
return false;
399+
MergedAnnotations annotations = MergedAnnotations.from(annotation.annotationType());
400+
return annotations.isPresent(annotationClass);
327401
}
328402

329403
}

src/test/java/org/springframework/data/util/NullableUtilsUnitTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,29 @@ void shouldConsiderJakartaNonNullParameters() {
9393
assertThat(pn1.isNonNull()).isFalse();
9494
}
9595

96+
@Test //
97+
void shouldConsiderJSpecifyNonNullParameters() {
98+
99+
var method = ReflectionUtils.findMethod(org.springframework.data.util.nonnull.jspecify.NonNullOnPackage.class,
100+
"someMethod", String.class, String.class);
101+
Nullability.Introspector introspector = Nullability.introspect(method.getDeclaringClass());
102+
Nullability mrt = introspector.forReturnType(method);
103+
104+
assertThat(mrt.isDeclared()).isTrue();
105+
assertThat(mrt.isNonNull()).isTrue();
106+
assertThat(mrt.isNullable()).isFalse();
107+
108+
Nullability pn0 = introspector.forParameter(MethodParameter.forExecutable(method, 0));
109+
assertThat(pn0.isDeclared()).isTrue();
110+
assertThat(pn0.isNullable()).isFalse();
111+
assertThat(pn0.isNonNull()).isTrue();
112+
113+
Nullability pn1 = introspector.forParameter(MethodParameter.forExecutable(method, 1));
114+
assertThat(pn1.isDeclared()).isTrue();
115+
assertThat(pn1.isNullable()).isTrue();
116+
assertThat(pn1.isNonNull()).isFalse();
117+
}
118+
96119
@Test // DATACMNS-1154
97120
void shouldConsiderJsr305NonNullParameters() {
98121

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2017-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.util.nonnull.jspecify;
17+
18+
import org.jspecify.annotations.Nullable;
19+
20+
/**
21+
* @author Mark Paluch
22+
*/
23+
public interface NonNullOnPackage {
24+
25+
String someMethod(String arg, @Nullable String nullableArg);
26+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* @author Mark Paluch
19+
*/
20+
@NullMarked
21+
package org.springframework.data.util.nonnull.jspecify;
22+
23+
import org.jspecify.annotations.NullMarked;

0 commit comments

Comments
 (0)