Skip to content

Commit b3e8882

Browse files
committed
Introduce Nullness API
This commit introduces a Nullness enum with related utility methods in order to detect if a type usage, a field, a method return type or a parameter is unspecified, nullable or not null. JSpecify annotations are fully supported, as well as Kotlin null safety and `@Nullable` annotations regardless of their package (from Spring, JSR-305 or Jakarta set of annotations for example). Closes gh-34261
1 parent 92472a6 commit b3e8882

File tree

13 files changed

+893
-27
lines changed

13 files changed

+893
-27
lines changed

spring-core/src/main/java/org/springframework/core/MethodParameter.java

+5-27
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
21-
import java.lang.reflect.AnnotatedType;
2221
import java.lang.reflect.Constructor;
2322
import java.lang.reflect.Executable;
2423
import java.lang.reflect.Field;
@@ -388,39 +387,18 @@ private MethodParameter nested(int nestingLevel, @Nullable Integer typeIndex) {
388387

389388
/**
390389
* Return whether this method indicates a parameter which is not required:
391-
* either in the form of Java 8's {@link java.util.Optional}, any variant
392-
* of a parameter-level {@code Nullable} annotation (such as from JSpecify,
393-
* JSR-305 or Jakarta set of annotations), or a language-level nullable type
390+
* either in the form of Java 8's {@link java.util.Optional}, JSpecify annotations,
391+
* any variant of a parameter-level {@code @Nullable} annotation (such as from Spring,
392+
* JSR-305 or Jakarta set of annotations), a language-level nullable type
394393
* declaration or {@code Continuation} parameter in Kotlin.
395394
* @since 4.3
395+
* @see Nullness#forMethodParameter(MethodParameter)
396396
*/
397397
public boolean isOptional() {
398-
return (getParameterType() == Optional.class || hasNullableAnnotation() ||
398+
return (getParameterType() == Optional.class || Nullness.forMethodParameter(this) == Nullness.NULLABLE ||
399399
(KotlinDetector.isKotlinType(getContainingClass()) && KotlinDelegate.isOptional(this)));
400400
}
401401

402-
/**
403-
* Check whether this method parameter is annotated with any variant of a
404-
* {@code Nullable} annotation, for example, {@code org.springframework.lang.Nullable},
405-
* {@code org.jspecify.annotations.Nullable} or {@code jakarta.annotation.Nullable}.
406-
*/
407-
private boolean hasNullableAnnotation() {
408-
for (Annotation ann : getParameterAnnotations()) {
409-
if ("Nullable".equals(ann.annotationType().getSimpleName())) {
410-
return true;
411-
}
412-
}
413-
if (this.parameterIndex >= 0) {
414-
AnnotatedType annotatedType = this.executable.getAnnotatedParameterTypes()[this.parameterIndex];
415-
for (Annotation ann : annotatedType.getAnnotations()) {
416-
if ("Nullable".equals(ann.annotationType().getSimpleName())) {
417-
return true;
418-
}
419-
}
420-
}
421-
return false;
422-
}
423-
424402
/**
425403
* Return a variant of this {@code MethodParameter} which points to
426404
* the same parameter but one nesting level deeper in case of a
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2002-2025 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+
package org.springframework.core;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.AnnotatedElement;
21+
import java.lang.reflect.AnnotatedType;
22+
import java.lang.reflect.Constructor;
23+
import java.lang.reflect.Executable;
24+
import java.lang.reflect.Field;
25+
import java.lang.reflect.Method;
26+
import java.lang.reflect.Parameter;
27+
import java.util.Objects;
28+
import java.util.function.Predicate;
29+
30+
import kotlin.reflect.KFunction;
31+
import kotlin.reflect.KParameter;
32+
import kotlin.reflect.KProperty;
33+
import kotlin.reflect.jvm.ReflectJvmMapping;
34+
import org.jspecify.annotations.NonNull;
35+
import org.jspecify.annotations.NullMarked;
36+
import org.jspecify.annotations.NullUnmarked;
37+
import org.jspecify.annotations.Nullable;
38+
39+
/**
40+
* Constants that indicate the nullness, as well as related utility methods.
41+
*
42+
* <p>The nullness applies to a type usage, a field, a method return type or a parameter.
43+
* <a href="https://jspecify.dev/docs/user-guide/">JSpecify annotations</a> are fully supported, as well as
44+
* <a href="https://kotlinlang.org/docs/null-safety.html">Kotlin null safety</a> and {@code @Nullable} annotations
45+
* regardless of their package (from Spring, JSR-305 or Jakarta set of annotations for example).
46+
*
47+
* @author Sebastien Deleuze
48+
* @since 7.0
49+
*/
50+
public enum Nullness {
51+
52+
/**
53+
* Unspecified nullness (Java and JSpecify {@code @NullUnmarked} defaults).
54+
*/
55+
UNSPECIFIED,
56+
57+
/**
58+
* Can include null (typically specified with a {@code @Nullable} annotation).
59+
*/
60+
NULLABLE,
61+
62+
/**
63+
* Will not include null (Kotlin and JSpecify {@code @NullMarked} defaults).
64+
*/
65+
NON_NULL;
66+
67+
68+
/**
69+
* Return the nullness of the given method return type.
70+
* @param method the source for the method return type
71+
* @return the corresponding nullness
72+
*/
73+
public static Nullness forMethodReturnType(Method method) {
74+
if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
75+
return KotlinDelegate.forMethodReturnType(method);
76+
}
77+
return (hasNullableAnnotation(method) ? Nullness.NULLABLE :
78+
jSpecifyNullness(method, method.getDeclaringClass(), method.getAnnotatedReturnType()));
79+
}
80+
81+
/**
82+
* Return the nullness of the given parameter.
83+
* @param parameter the parameter descriptor
84+
* @return the corresponding nullness
85+
*/
86+
public static Nullness forParameter(Parameter parameter) {
87+
if (KotlinDetector.isKotlinType(parameter.getDeclaringExecutable().getDeclaringClass())) {
88+
// TODO Optimize when kotlin-reflect provide a more direct Parameter to KParameter resolution
89+
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
90+
return KotlinDelegate.forParameter(methodParameter.getExecutable(), methodParameter.getParameterIndex());
91+
}
92+
Executable executable = parameter.getDeclaringExecutable();
93+
return (hasNullableAnnotation(parameter) ? Nullness.NULLABLE :
94+
jSpecifyNullness(executable, executable.getDeclaringClass(), parameter.getAnnotatedType()));
95+
}
96+
97+
/**
98+
* Return the nullness of the given method parameter.
99+
* @param methodParameter the method parameter descriptor
100+
* @return the corresponding nullness
101+
*/
102+
public static Nullness forMethodParameter(MethodParameter methodParameter) {
103+
return (methodParameter.getParameterIndex() < 0 ?
104+
forMethodReturnType(Objects.requireNonNull(methodParameter.getMethod())) :
105+
forParameter(methodParameter.getParameter()));
106+
}
107+
108+
/**
109+
* Return the nullness of the given field.
110+
* @param field the field descriptor
111+
* @return the corresponding nullness
112+
*/
113+
public static Nullness forField(Field field) {
114+
if (KotlinDetector.isKotlinType(field.getDeclaringClass())) {
115+
return KotlinDelegate.forField(field);
116+
}
117+
return (hasNullableAnnotation(field) ? Nullness.NULLABLE :
118+
jSpecifyNullness(field, field.getDeclaringClass(), field.getAnnotatedType()));
119+
}
120+
121+
122+
// Check method and parameter level @Nullable annotations regardless of the package (including Spring and JSR 305 annotations)
123+
private static boolean hasNullableAnnotation(AnnotatedElement element) {
124+
for (Annotation annotation : element.getDeclaredAnnotations()) {
125+
if ("Nullable".equals(annotation.annotationType().getSimpleName())) {
126+
return true;
127+
}
128+
}
129+
return false;
130+
}
131+
132+
private static Nullness jSpecifyNullness(AnnotatedElement annotatedElement, Class<?> declaringClass, AnnotatedType annotatedType) {
133+
if (annotatedType.isAnnotationPresent(Nullable.class)) {
134+
return Nullness.NULLABLE;
135+
}
136+
if (annotatedType.isAnnotationPresent(NonNull.class)) {
137+
return Nullness.NON_NULL;
138+
}
139+
Nullness nullness = Nullness.UNSPECIFIED;
140+
// Package level
141+
Package declaringPackage = declaringClass.getPackage();
142+
if (declaringPackage.isAnnotationPresent(NullMarked.class)) {
143+
nullness = Nullness.NON_NULL;
144+
}
145+
// Class level
146+
if (declaringClass.isAnnotationPresent(NullMarked.class)) {
147+
nullness = Nullness.NON_NULL;
148+
}
149+
else if (declaringClass.isAnnotationPresent(NullUnmarked.class)) {
150+
nullness = Nullness.UNSPECIFIED;
151+
}
152+
// Annotated element level
153+
if (annotatedElement.isAnnotationPresent(NullMarked.class)) {
154+
nullness = Nullness.NON_NULL;
155+
}
156+
else if (annotatedElement.isAnnotationPresent(NullUnmarked.class)) {
157+
nullness = Nullness.UNSPECIFIED;
158+
}
159+
return nullness;
160+
}
161+
162+
/**
163+
* Inner class to avoid a hard dependency on Kotlin at runtime.
164+
*/
165+
private static class KotlinDelegate {
166+
167+
168+
public static Nullness forMethodReturnType(Method method) {
169+
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
170+
if (function != null && function.getReturnType().isMarkedNullable()) {
171+
return Nullness.NULLABLE;
172+
}
173+
return Nullness.NON_NULL;
174+
}
175+
176+
public static Nullness forParameter(Executable executable, int parameterIndex) {
177+
KFunction<?> function;
178+
Predicate<KParameter> predicate;
179+
if (executable instanceof Method method) {
180+
function = ReflectJvmMapping.getKotlinFunction(method);
181+
predicate = p -> KParameter.Kind.VALUE.equals(p.getKind());
182+
}
183+
else {
184+
function = ReflectJvmMapping.getKotlinFunction((Constructor<?>) executable);
185+
predicate = p -> (KParameter.Kind.VALUE.equals(p.getKind()) ||
186+
KParameter.Kind.INSTANCE.equals(p.getKind()));
187+
}
188+
if (function == null) {
189+
return Nullness.UNSPECIFIED;
190+
}
191+
int i = 0;
192+
for (KParameter kParameter : function.getParameters()) {
193+
if (predicate.test(kParameter) && parameterIndex == i++) {
194+
return (kParameter.getType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL);
195+
}
196+
}
197+
return Nullness.UNSPECIFIED;
198+
}
199+
200+
public static Nullness forField(Field field) {
201+
KProperty<?> property = ReflectJvmMapping.getKotlinProperty(field);
202+
if (property != null && property.getReturnType().isMarkedNullable()) {
203+
return Nullness.NULLABLE;
204+
}
205+
return Nullness.NON_NULL;
206+
}
207+
208+
}
209+
210+
}

0 commit comments

Comments
 (0)