Skip to content

Commit 12aa14d

Browse files
committed
Support @nullable annotations as indicators for optional injection points
Issue: SPR-15028
1 parent 8be791c commit 12aa14d

File tree

3 files changed

+132
-11
lines changed

3 files changed

+132
-11
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.io.ObjectInputStream;
2121
import java.io.Serializable;
22+
import java.lang.annotation.Annotation;
2223
import java.lang.reflect.Field;
2324
import java.lang.reflect.ParameterizedType;
2425
import java.lang.reflect.Type;
@@ -155,21 +156,39 @@ public DependencyDescriptor(DependencyDescriptor original) {
155156

156157
/**
157158
* Return whether this dependency is required.
159+
* <p>Optional semantics are derived from Java 8's {@link java.util.Optional},
160+
* any variant of a parameter-level {@code Nullable} annotation (such as from
161+
* JSR-305 or the FindBugs set of annotations), or a language-level nullable
162+
* type declaration in Kotlin.
158163
*/
159164
public boolean isRequired() {
160165
if (!this.required) {
161166
return false;
162167
}
163168

164169
if (this.field != null) {
165-
return !(this.field.getType() == Optional.class ||
170+
return !(this.field.getType() == Optional.class || hasNullableAnnotation() ||
166171
(kotlinPresent && KotlinDelegate.isNullable(this.field)));
167172
}
168173
else {
169174
return !this.methodParameter.isOptional();
170175
}
171176
}
172177

178+
/**
179+
* Check whether the underlying field is annotated with any variant of a
180+
* {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or
181+
* {@code edu.umd.cs.findbugs.annotations.Nullable}.
182+
*/
183+
private boolean hasNullableAnnotation() {
184+
for (Annotation ann : getAnnotations()) {
185+
if ("Nullable".equals(ann.annotationType().getSimpleName())) {
186+
return true;
187+
}
188+
}
189+
return false;
190+
}
191+
173192
/**
174193
* Return whether this dependency is 'eager' in the sense of
175194
* eagerly resolving potential target beans for type matching.

spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.beans.factory.annotation;
1818

1919
import java.io.Serializable;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
2022
import java.util.List;
2123
import java.util.Map;
2224
import java.util.Optional;
@@ -574,6 +576,60 @@ public void testBeanAutowiredWithFactoryBean() {
574576
bf.destroySingletons();
575577
}
576578

579+
@Test
580+
public void testNullableFieldInjectionWithBeanAvailable() {
581+
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
582+
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
583+
bpp.setBeanFactory(bf);
584+
bf.addBeanPostProcessor(bpp);
585+
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class));
586+
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
587+
588+
NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean");
589+
assertSame(bf.getBean("testBean"), bean.getTestBean());
590+
bf.destroySingletons();
591+
}
592+
593+
@Test
594+
public void testNullableFieldInjectionWithBeanNotAvailable() {
595+
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
596+
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
597+
bpp.setBeanFactory(bf);
598+
bf.addBeanPostProcessor(bpp);
599+
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class));
600+
601+
NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean");
602+
assertNull(bean.getTestBean());
603+
bf.destroySingletons();
604+
}
605+
606+
@Test
607+
public void testNullableMethodInjectionWithBeanAvailable() {
608+
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
609+
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
610+
bpp.setBeanFactory(bf);
611+
bf.addBeanPostProcessor(bpp);
612+
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class));
613+
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
614+
615+
NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean");
616+
assertSame(bf.getBean("testBean"), bean.getTestBean());
617+
bf.destroySingletons();
618+
}
619+
620+
@Test
621+
public void testNullableMethodInjectionWithBeanNotAvailable() {
622+
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
623+
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
624+
bpp.setBeanFactory(bf);
625+
bf.addBeanPostProcessor(bpp);
626+
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class));
627+
628+
NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean");
629+
assertNull(bean.getTestBean());
630+
bf.destroySingletons();
631+
}
632+
577633
@Test
578634
public void testOptionalFieldInjectionWithBeanAvailable() {
579635
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@@ -1275,6 +1331,36 @@ public boolean isSingleton() {
12751331
}
12761332

12771333

1334+
@Retention(RetentionPolicy.RUNTIME)
1335+
public @interface Nullable {}
1336+
1337+
1338+
public static class NullableFieldInjectionBean {
1339+
1340+
@Inject @Nullable
1341+
private TestBean testBean;
1342+
1343+
public TestBean getTestBean() {
1344+
return this.testBean;
1345+
}
1346+
}
1347+
1348+
1349+
public static class NullableMethodInjectionBean {
1350+
1351+
private TestBean testBean;
1352+
1353+
@Inject
1354+
public void setTestBean(@Nullable TestBean testBean) {
1355+
this.testBean = testBean;
1356+
}
1357+
1358+
public TestBean getTestBean() {
1359+
return this.testBean;
1360+
}
1361+
}
1362+
1363+
12781364
public static class OptionalFieldInjectionBean {
12791365

12801366
@Inject
@@ -1291,8 +1377,8 @@ public static class OptionalMethodInjectionBean {
12911377
private Optional<TestBean> testBean;
12921378

12931379
@Inject
1294-
public void setTestBean(Optional<TestBean> testBeanFactory) {
1295-
this.testBean = testBeanFactory;
1380+
public void setTestBean(Optional<TestBean> testBean) {
1381+
this.testBean = testBean;
12961382
}
12971383

12981384
public Optional<TestBean> getTestBean() {
@@ -1317,8 +1403,8 @@ public static class OptionalListMethodInjectionBean {
13171403
private Optional<List<TestBean>> testBean;
13181404

13191405
@Inject
1320-
public void setTestBean(Optional<List<TestBean>> testBeanFactory) {
1321-
this.testBean = testBeanFactory;
1406+
public void setTestBean(Optional<List<TestBean>> testBean) {
1407+
this.testBean = testBean;
13221408
}
13231409

13241410
public Optional<List<TestBean>> getTestBean() {
@@ -1343,8 +1429,8 @@ public static class ProviderOfOptionalMethodInjectionBean {
13431429
private Provider<Optional<TestBean>> testBean;
13441430

13451431
@Inject
1346-
public void setTestBean(Provider<Optional<TestBean>> testBeanFactory) {
1347-
this.testBean = testBeanFactory;
1432+
public void setTestBean(Provider<Optional<TestBean>> testBean) {
1433+
this.testBean = testBean;
13481434
}
13491435

13501436
public Optional<TestBean> getTestBean() {

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,16 +322,32 @@ public MethodParameter nested() {
322322
}
323323

324324
/**
325-
* Return whether this method indicates a parameter which is not required
326-
* (either in the form of Java 8's {@link java.util.Optional} or Kotlin's
327-
* nullable type).
325+
* Return whether this method indicates a parameter which is not required:
326+
* either in the form of Java 8's {@link java.util.Optional}, any variant
327+
* of a parameter-level {@code Nullable} annotation (such as from JSR-305
328+
* or the FindBugs set of annotations), or a language-level nullable type
329+
* declaration in Kotlin.
328330
* @since 4.3
329331
*/
330332
public boolean isOptional() {
331-
return (getParameterType() == Optional.class ||
333+
return (getParameterType() == Optional.class || hasNullableAnnotation() ||
332334
(kotlinPresent && KotlinDelegate.isNullable(this)));
333335
}
334336

337+
/**
338+
* Check whether this method parameter is annotated with any variant of a
339+
* {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or
340+
* {@code edu.umd.cs.findbugs.annotations.Nullable}.
341+
*/
342+
private boolean hasNullableAnnotation() {
343+
for (Annotation ann : getParameterAnnotations()) {
344+
if ("Nullable".equals(ann.annotationType().getSimpleName())) {
345+
return true;
346+
}
347+
}
348+
return false;
349+
}
350+
335351
/**
336352
* Return a variant of this {@code MethodParameter} which points to
337353
* the same parameter but one nesting level deeper in case of a

0 commit comments

Comments
 (0)