Skip to content

Commit c63b236

Browse files
committed
Introduce MethodNullability API.
1 parent ba1485a commit c63b236

File tree

3 files changed

+141
-15
lines changed

3 files changed

+141
-15
lines changed

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

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,24 @@ public interface Nullability {
7878
*/
7979
boolean isNonNull();
8080

81+
/**
82+
* Creates a new {@link MethodNullability} instance by introspecting the {@link Method} return type.
83+
*
84+
* @param method the source method.
85+
* @return a {@code Nullability} instance containing the element's nullability declaration.
86+
*/
87+
static MethodNullability forMethod(Method method) {
88+
return new NullabilityIntrospector(method.getDeclaringClass(), true).forMethod(method);
89+
}
90+
8191
/**
8292
* Creates a new {@link Nullability} instance by introspecting the {@link MethodParameter}.
8393
*
8494
* @param parameter the source method parameter.
8595
* @return a {@code Nullability} instance containing the element's nullability declaration.
8696
*/
87-
static Nullability from(MethodParameter parameter) {
88-
return introspect(parameter.getContainingClass()).forParameter(parameter);
97+
static Nullability forParameter(MethodParameter parameter) {
98+
return new NullabilityIntrospector(parameter.getContainingClass(), false).forParameter(parameter);
8999
}
90100

91101
/**
@@ -95,7 +105,7 @@ static Nullability from(MethodParameter parameter) {
95105
* @return a {@code Nullability} instance containing the element's nullability declaration.
96106
*/
97107
static Nullability forMethodReturnType(Method method) {
98-
return introspect(method.getDeclaringClass()).forReturnType(method);
108+
return new NullabilityIntrospector(method.getDeclaringClass(), false).forReturnType(method);
99109
}
100110

101111
/**
@@ -104,8 +114,9 @@ static Nullability forMethodReturnType(Method method) {
104114
* @param parameter the source method parameter.
105115
* @return a {@code Nullability} instance containing the element's nullability declaration.
106116
*/
107-
static Nullability forMethodParameter(Parameter parameter) {
108-
return introspect(parameter.getDeclaringExecutable().getDeclaringClass()).forParameter(parameter);
117+
static Nullability forParameter(Parameter parameter) {
118+
return new NullabilityIntrospector(parameter.getDeclaringExecutable().getDeclaringClass(), false)
119+
.forParameter(parameter);
109120
}
110121

111122
/**
@@ -115,7 +126,7 @@ static Nullability forMethodParameter(Parameter parameter) {
115126
* @return a {@code Introspector} instance considering nullability declarations from the {@link Class} and package.
116127
*/
117128
static Introspector introspect(Class<?> cls) {
118-
return new NullabilityIntrospector(cls);
129+
return new NullabilityIntrospector(cls, true);
119130
}
120131

121132
/**
@@ -125,14 +136,25 @@ static Introspector introspect(Class<?> cls) {
125136
* @return a {@code Introspector} instance considering nullability declarations from package.
126137
*/
127138
static Introspector introspect(Package pkg) {
128-
return new NullabilityIntrospector(pkg);
139+
return new NullabilityIntrospector(pkg, true);
129140
}
130141

131142
/**
132143
* Nullability introspector to introspect multiple elements within the context of their source container.
133144
*/
134145
interface Introspector {
135146

147+
/**
148+
* Creates a new {@link MethodNullability} instance by introspecting the {@link Method}.
149+
* <p>
150+
* If the method parameter does not declare any nullability rules, then introspection falls back to the source
151+
* container that was used to create the introspector.
152+
*
153+
* @param method the source method.
154+
* @return a {@code Nullability} instance containing the element's nullability declaration.
155+
*/
156+
MethodNullability forMethod(Method method);
157+
136158
/**
137159
* Creates a new {@link Nullability} instance by introspecting the {@link MethodParameter}.
138160
* <p>
@@ -159,7 +181,7 @@ default Nullability forParameter(MethodParameter parameter) {
159181
Nullability forReturnType(Method method);
160182

161183
/**
162-
* Creates a new {@link Nullability} instance by introspecting the {@link MethodParameter}.
184+
* Creates a new {@link Nullability} instance by introspecting the {@link Parameter}.
163185
* <p>
164186
* If the method parameter does not declare any nullability rules, then introspection falls back to the source
165187
* container that was used to create the introspector.
@@ -171,4 +193,41 @@ default Nullability forParameter(MethodParameter parameter) {
171193

172194
}
173195

196+
/**
197+
* Nullability introspector to introspect multiple elements within the context of their source container. Inherited
198+
* nullability methods nullability of the method return type.
199+
*/
200+
interface MethodNullability extends Nullability {
201+
202+
/**
203+
* Returns a {@link Nullability} instance for the method return type.
204+
*
205+
* @return a {@link Nullability} instance for the method return type.
206+
*/
207+
default Nullability forReturnType() {
208+
return this;
209+
}
210+
211+
/**
212+
* Returns a {@link Nullability} instance for a method parameter.
213+
*
214+
* @param parameter the method parameter.
215+
* @return a {@link Nullability} instance for a method parameter.
216+
* @throws IllegalArgumentException if the method parameter is not defined by the underlying method.
217+
*/
218+
Nullability forParameter(Parameter parameter);
219+
220+
/**
221+
* Returns a {@link Nullability} instance for a method parameter.
222+
*
223+
* @param parameter the method parameter.
224+
* @return a {@link Nullability} instance for a method parameter.
225+
* @throws IllegalArgumentException if the method parameter is not defined by the underlying method.
226+
*/
227+
default Nullability forParameter(MethodParameter parameter) {
228+
return parameter.getParameterIndex() == -1 ? forReturnType() : forParameter(parameter.getParameter());
229+
}
230+
231+
}
232+
174233
}

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

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
import java.util.ArrayList;
2626
import java.util.Arrays;
2727
import java.util.Collections;
28+
import java.util.HashMap;
2829
import java.util.HashSet;
2930
import java.util.List;
31+
import java.util.Map;
3032
import java.util.Set;
3133
import java.util.function.Function;
3234
import java.util.function.Predicate;
@@ -37,6 +39,7 @@
3739
import org.springframework.lang.NonNullFields;
3840
import org.springframework.lang.Nullable;
3941
import org.springframework.util.ClassUtils;
42+
import org.springframework.util.ConcurrentLruCache;
4043

4144
/**
4245
* Default {@link Nullability.Introspector} implementation backed by {@link NullabilityProvider nullability providers}.
@@ -67,8 +70,9 @@ class NullabilityIntrospector implements Nullability.Introspector {
6770

6871
private final DeclarationAnchor anchor;
6972

70-
NullabilityIntrospector(AnnotatedElement segment) {
71-
this.anchor = createTree(segment);
73+
NullabilityIntrospector(AnnotatedElement segment, boolean cache) {
74+
DeclarationAnchor tree = createTree(segment);
75+
this.anchor = cache ? new CachingDeclarationAnchor(tree) : tree;
7276
}
7377

7478
/**
@@ -107,6 +111,19 @@ static DeclarationAnchor createTree(AnnotatedElement element) {
107111
throw new IllegalArgumentException(String.format("Cannot create DeclarationAnchor for %s", element));
108112
}
109113

114+
@Override
115+
public Nullability.MethodNullability forMethod(Method method) {
116+
117+
HierarchicalAnnotatedElementAnchor element = new HierarchicalAnnotatedElementAnchor(anchor, method);
118+
Map<Parameter, Nullability> parameters = new HashMap<>();
119+
120+
for (Parameter parameter : method.getParameters()) {
121+
parameters.put(parameter, Nullability.forParameter(parameter));
122+
}
123+
124+
return new DefaultMethodNullability(element.evaluate(ElementType.METHOD), parameters, method);
125+
}
126+
110127
@Override
111128
public Nullability forReturnType(Method method) {
112129

@@ -437,16 +454,67 @@ public boolean isNonNull() {
437454
}
438455
}
439456

457+
static class DefaultMethodNullability extends TheNullability implements Nullability.MethodNullability {
458+
459+
private final Map<Parameter, Nullability> parameters;
460+
private final Method method;
461+
462+
public DefaultMethodNullability(Spec spec, Map<Parameter, Nullability> parameters, Method method) {
463+
super(spec);
464+
this.parameters = parameters;
465+
this.method = method;
466+
}
467+
468+
@Override
469+
public Nullability forParameter(Parameter parameter) {
470+
471+
Nullability nullability = parameters.get(parameter);
472+
473+
if (nullability == null) {
474+
throw new IllegalArgumentException(String.format("Parameter %s is not defined by %s", parameter, method));
475+
}
476+
477+
return nullability;
478+
}
479+
}
480+
440481
enum Spec {
441482
UNSPECIFIED, NULLABLE, NON_NULL
442483
}
443484

485+
/**
486+
* Declaration anchors represent elements that hold nullability declarations such as classes, packages, modules.
487+
*/
444488
interface DeclarationAnchor {
445489

490+
/**
491+
* Evaluate nullability declarations for the given {@link ElementType}.
492+
*
493+
* @param target
494+
* @return
495+
*/
446496
Spec evaluate(ElementType target);
447497

448498
}
449499

500+
/**
501+
* Caching variant of {@link DeclarationAnchor}.
502+
*/
503+
static class CachingDeclarationAnchor implements DeclarationAnchor {
504+
505+
private final ConcurrentLruCache<ElementType, Spec> cache;
506+
507+
public CachingDeclarationAnchor(DeclarationAnchor delegate) {
508+
this.cache = new ConcurrentLruCache<>(ElementType.values().length, delegate::evaluate);
509+
}
510+
511+
@Override
512+
public Spec evaluate(ElementType target) {
513+
return this.cache.get(target);
514+
}
515+
516+
}
517+
450518
static class AnnotatedElementAnchor implements DeclarationAnchor {
451519

452520
private final AnnotatedElement element;

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,12 @@ void packageAnnotatedShouldConsiderNonNullAnnotation() {
4545
assertThat(NullableUtils.isNonNull(method, ElementType.PARAMETER)).isTrue();
4646
assertThat(NullableUtils.isNonNull(method, ElementType.PACKAGE)).isFalse();
4747

48-
Nullability.Introspector introspector = Nullability.introspect(NonNullOnPackage.class);
49-
Nullability mrt = introspector.forReturnType(method);
48+
Nullability.MethodNullability mrt = Nullability.forMethod(method);
5049

5150
assertThat(mrt.isNullable()).isFalse();
5251
assertThat(mrt.isNonNull()).isTrue();
5352

54-
Nullability pn = introspector.forParameter(MethodParameter.forExecutable(method, 0));
53+
Nullability pn = mrt.forParameter(MethodParameter.forExecutable(method, 0));
5554

5655
assertThat(pn.isNullable()).isFalse();
5756
assertThat(pn.isNonNull()).isTrue();
@@ -145,7 +144,7 @@ void shouldConsiderJsr305NonNullAnnotation() {
145144
var method = ReflectionUtils.findMethod(Jsr305NonnullAnnotatedType.class, "someMethod", String.class);
146145

147146
Nullability mrt = Nullability.forMethodReturnType(method);
148-
Nullability pn = Nullability.forMethodParameter(method.getParameters()[0]);
147+
Nullability pn = Nullability.forParameter(method.getParameters()[0]);
149148

150149
assertThat(mrt.isDeclared()).isTrue();
151150
assertThat(mrt.isNullable()).isFalse();
@@ -165,7 +164,7 @@ void shouldConsiderNonAnnotatedTypeNullable() {
165164
var method = ReflectionUtils.findMethod(NonAnnotatedType.class, "someMethod", String.class);
166165

167166
Nullability mrt = Nullability.forMethodReturnType(method);
168-
Nullability pn = Nullability.forMethodParameter(method.getParameters()[0]);
167+
Nullability pn = Nullability.forParameter(method.getParameters()[0]);
169168

170169
assertThat(mrt.isDeclared()).isFalse();
171170
assertThat(mrt.isNullable()).isTrue();

0 commit comments

Comments
 (0)