diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index 13a469be01e..c7321ad40ff 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -19,6 +19,7 @@ package org.springframework.beans.factory.config; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -155,6 +156,10 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable /** * Return whether this dependency is required. + *

Optional semantics are derived from Java 8's {@link java.util.Optional}, + * any variant of a parameter-level {@code Nullable} annotation (such as from + * JSR-305 or the FindBugs set of annotations), or a language-level nullable + * type declaration in Kotlin. */ public boolean isRequired() { if (!this.required) { @@ -162,7 +167,7 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable } if (this.field != null) { - return !(this.field.getType() == Optional.class || + return !(this.field.getType() == Optional.class || hasNullableAnnotation() || (kotlinPresent && KotlinDelegate.isNullable(this.field))); } else { @@ -170,6 +175,20 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable } } + /** + * Check whether the underlying field is annotated with any variant of a + * {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or + * {@code edu.umd.cs.findbugs.annotations.Nullable}. + */ + private boolean hasNullableAnnotation() { + for (Annotation ann : getAnnotations()) { + if ("Nullable".equals(ann.annotationType().getSimpleName())) { + return true; + } + } + return false; + } + /** * Return whether this dependency is 'eager' in the sense of * eagerly resolving potential target beans for type matching. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java index 63390cb8ca7..9bcac1203c3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java @@ -17,6 +17,8 @@ package org.springframework.beans.factory.annotation; import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; import java.util.Optional; @@ -574,6 +576,60 @@ public class InjectAnnotationBeanPostProcessorTests { bf.destroySingletons(); } + @Test + public void testNullableFieldInjectionWithBeanAvailable() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean"); + assertSame(bf.getBean("testBean"), bean.getTestBean()); + bf.destroySingletons(); + } + + @Test + public void testNullableFieldInjectionWithBeanNotAvailable() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class)); + + NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean"); + assertNull(bean.getTestBean()); + bf.destroySingletons(); + } + + @Test + public void testNullableMethodInjectionWithBeanAvailable() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean"); + assertSame(bf.getBean("testBean"), bean.getTestBean()); + bf.destroySingletons(); + } + + @Test + public void testNullableMethodInjectionWithBeanNotAvailable() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class)); + + NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean"); + assertNull(bean.getTestBean()); + bf.destroySingletons(); + } + @Test public void testOptionalFieldInjectionWithBeanAvailable() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -1275,6 +1331,36 @@ public class InjectAnnotationBeanPostProcessorTests { } + @Retention(RetentionPolicy.RUNTIME) + public @interface Nullable {} + + + public static class NullableFieldInjectionBean { + + @Inject @Nullable + private TestBean testBean; + + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class NullableMethodInjectionBean { + + private TestBean testBean; + + @Inject + public void setTestBean(@Nullable TestBean testBean) { + this.testBean = testBean; + } + + public TestBean getTestBean() { + return this.testBean; + } + } + + public static class OptionalFieldInjectionBean { @Inject @@ -1291,8 +1377,8 @@ public class InjectAnnotationBeanPostProcessorTests { private Optional testBean; @Inject - public void setTestBean(Optional testBeanFactory) { - this.testBean = testBeanFactory; + public void setTestBean(Optional testBean) { + this.testBean = testBean; } public Optional getTestBean() { @@ -1317,8 +1403,8 @@ public class InjectAnnotationBeanPostProcessorTests { private Optional> testBean; @Inject - public void setTestBean(Optional> testBeanFactory) { - this.testBean = testBeanFactory; + public void setTestBean(Optional> testBean) { + this.testBean = testBean; } public Optional> getTestBean() { @@ -1343,8 +1429,8 @@ public class InjectAnnotationBeanPostProcessorTests { private Provider> testBean; @Inject - public void setTestBean(Provider> testBeanFactory) { - this.testBean = testBeanFactory; + public void setTestBean(Provider> testBean) { + this.testBean = testBean; } public Optional getTestBean() { diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 924f5aafbfb..57bee0cb23b 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -322,16 +322,32 @@ public class MethodParameter { } /** - * Return whether this method indicates a parameter which is not required - * (either in the form of Java 8's {@link java.util.Optional} or Kotlin's - * nullable type). + * Return whether this method indicates a parameter which is not required: + * either in the form of Java 8's {@link java.util.Optional}, any variant + * of a parameter-level {@code Nullable} annotation (such as from JSR-305 + * or the FindBugs set of annotations), or a language-level nullable type + * declaration in Kotlin. * @since 4.3 */ public boolean isOptional() { - return (getParameterType() == Optional.class || + return (getParameterType() == Optional.class || hasNullableAnnotation() || (kotlinPresent && KotlinDelegate.isNullable(this))); } + /** + * Check whether this method parameter is annotated with any variant of a + * {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or + * {@code edu.umd.cs.findbugs.annotations.Nullable}. + */ + private boolean hasNullableAnnotation() { + for (Annotation ann : getParameterAnnotations()) { + if ("Nullable".equals(ann.annotationType().getSimpleName())) { + return true; + } + } + return false; + } + /** * Return a variant of this {@code MethodParameter} which points to * the same parameter but one nesting level deeper in case of a