关于依赖注入

209 阅读6分钟

背景

最近使用@Autowired进行属性注入,idea总是提示Field injection is not recommended,意思是不推荐属性注入的方式。今天正好有些空闲时间,研究下其中原因。

注入方式

什么是注入,以我目前功力的理解,注入就是给对象的成员变量赋值。那么不外乎有三种方式:

  • 直接给属性赋值
  • 通过set方法
  • 通过构造方法
 package keai.yiyi.inject;
 ​
 /**
  * @author Null
  * @description 注入演示
  */
 public class Inject {
 ​
     /**
      * 属性注入
      */
     private Injecter injecter = new Injecter();
 ​
     /**
      * setter方法注入
      */
     public void setInjectFiled(Injecter injecter) {
         this.injecter = injecter;
     }
 ​
     /**
      * 构造注入
      */
     public Inject(Injecter injecter) {
         this.injecter = injecter;
     }
 }
 ​
 ​
 class Injecter {
 }

为了简化注入难度,spring提供了@Autowired、java提供了@Resource等注解,可以方便开发者省去很多代码(比如可以直接使用注解进行属性注入,不必写setting方法和构造方法代码)。

注入方式对比

  • Field注入

    • 尽量少使用,如果需要则使用@Resource进行替代,以降低和IOC容器的耦合性;
    • Spring的IOC对待属性的注入使用的是set形式,但是final类型的变量在调用class的构造函数的这个过程当中就得初始化完成,这个是基于字段的依赖注入做不到的地方,只能使用基于构造函数的依赖注入的方式,也就是Field注入和Set注入无法使用final修饰的变量;
    • 在OOP的设计当中有一个 单一职责原则 ,当采用构造器注入的方式注入的太多的时候可以很直观的想到单一职责原则,进儿业务调整;
    • 对外隐藏属性,和IOC容器的耦合性高
  • Setter注入

    • 适用于具有可选性和可变性的依赖注入;
    • 同属性注入,无法注入final修饰的变量;
  • 构造器注入

    • 适用具有强依赖和不变性的依赖(如使用final修饰的变量)
    • 构造器依赖注入通过容器触发一个类的构造器来实现的,通过强制指明依赖注入来保证这个类的运行,防止NullPointerException
    • 依赖对外部可见

@Autowired

@Autowired注解由Spring提供的,所以当你程序的IOC容器由spring切换到其他IOC容器,并且你使用的注入方式都是@Autowired字段注入时,那么届时你将耗费大量的精力在切换依赖注入的方式上。

 @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface Autowired {
 ​
   /**
    * Declares whether the annotated dependency is required.
    * <p>Defaults to {@code true}.
    */
   boolean required() default true;
 ​
 }

通过源码分析,@Autowired可以用到构造函数普通方法方法参数成员变量上,通过@Autowired可以实现多种方式的属性注入。

成员变量注入

正常注入

 @Component
 public class AutowiredFieldInject {
 ​
     private static final Logger logger = LoggerFactory.getLogger(AutowiredFieldInject.class);
 ​
 ​
     @Autowired
     private Field field;
 ​
     public void printOut() {
         logger.info("field is null ? [{}] --- print field [{}]", field == null, field);
     }
 ​
 }
 ​
 @Component
 class Field {
 }
 ​
 // 单元测试
 @RunWith(SpringRunner.class)
 @SpringBootTest
 public class AutowiredFieldInjectTest {
 ​
     @Autowired
     private AutowiredFieldInject autowiredFieldInject;
 ​
     @Test
     public void fieldInjectTest() {
         autowiredFieldInject.printOut();
     }
 ​
 }
 // 输出内容
 field is null ? [false] --- print field [keai.yiyi.inject.Field@3afae281]

注入一个不被spring管理的bean

 @Component
 public class AutowiredFieldInject {
 ​
     private static final Logger logger = LoggerFactory.getLogger(AutowiredFieldInject.class);
 ​
 ​
     @Autowired
     private Field field;
 ​
     public void printOut() {
         logger.info("field is null ? [{}] --- print field [{}]", field == null, field);
     }
 ​
 }
 ​
 class Field {
 ​
 }
 ​
 // 输出内容
 No qualifying bean of type 'keai.yiyi.inject.Field'

所以@Autowired正常情况下是强依赖型的,也就是说如果不存在Field的依赖,那么将会启动应用不成功。

可以通过设置@Autowired(required = false)解决

 @Autowired(required = false)
 private Field field;
 ​
 // 输出内容
 field is null ? [true] --- print field [null]

多个相同类型的bean

 @Component
 public class AutowiredFieldInject {
 ​
     private static final Logger logger = LoggerFactory.getLogger(AutowiredFieldInject.class);
 ​
 ​
     @Autowired
     private Field field;
 ​
     public void printOut() {
         logger.info("field is null ? [{}] --- print field [{}]", field == null, field.toString());
     }
 ​
 }
 ​
 @Configuration
 class Conf {
 ​
     @Bean
     public Field fieldA() {
         return new Field("A");
     }
 ​
     @Bean
     public Field fieldB() {
         return new Field("B");
     }
 ​
 ​
 }
 ​
 @AllArgsConstructor
 @NoArgsConstructor
 class Field{
     private String desc;
 }
 ​
 // 单元测试
 @RunWith(SpringRunner.class)
 @SpringBootTest
 public class AutowiredFieldInjectTest {
 ​
     @Autowired
     private AutowiredFieldInject autowiredFieldInject;
 ​
     @Test
     public void fieldInjectTest() {
         autowiredFieldInject.printOut();
     }
 ​
 }
 // 输出内容,测试报错
 No qualifying bean of type 'keai.yiyi.inject.Field' available: expected single matching bean but found 2: fieldA,fieldB

解决办法:使用@Qualifier设置为byName注入,@Qualifier 作用:指定注入bean的名称

 @Autowired
 @Qualifier("fieldA")
 private Field field;
 ​
 // 输出结果
 field is null ? [false] --- print field [Field(desc=A)]

另外,使用@Primary也可以解决多个同类型bean的问题

 @Bean
 public Field fieldA() {
   return new Field("A");
 }
 ​
 @Bean
 @Primary
 public Field fieldB() {
   return new Field("B");
 }
 ​
 // 输出结果
 field is null ? [false] --- print field [Field(desc=B)]

方法注入

 @Component
 public class AutowiredFieldInject {
 ​
     private static final Logger logger = LoggerFactory.getLogger(AutowiredFieldInject.class);
 ​
 ​
     private Field field;
 ​
     @Autowired
     public void setxxField(Field field) {
         this.field = field;
     }
 ​
 ​
 ​
     public void printOut() {
         logger.info("field is null ? [{}] --- print field [{}]", field == null, field.toString());
     }
 ​
 }
 ​
 @Configuration
 class Field{
 }
 ​
 // 输出内容
 field is null ? [false] --- print field [keai.yiyi.inject.Field$$EnhancerBySpringCGLIB$$3bd432d9@771db12c]

取消@Autowired后

 public void setxxField(Field field) {
   this.field = field;
 }
 ​
 // 输出报错,显示field为空
 java.lang.NullPointerException

不使用@Autowired且方法为set方法

 public void setField(Field field) {
   this.field = field;
 }
 // 输出报错,显示field为空
 java.lang.NullPointerException

构造注入

 @Component
 public class AutowiredFieldInject {
 ​
     private static final Logger logger = LoggerFactory.getLogger(AutowiredFieldInject.class);
 ​
 ​
     private Field field;
 ​
     public AutowiredFieldInject(Field field) {
         this.field = field;
     }
 ​
     public void printOut() {
         logger.info("field is null ? [{}] --- print field [{}]", field == null, field.toString());
     }
 ​
 }
 ​
 @Configuration
 class Field{
 }
 ​
 // 输出
 field is null ? [false] --- print field [keai.yiyi.inject.Field$$EnhancerBySpringCGLIB$$3bd432d9@771db12c]

添加@Autowired注解

 @Autowired
 public AutowiredFieldInject(Field field) {
   this.field = field;
 }
 ​
 // 输出
 field is null ? [false] --- print field [keai.yiyi.inject.Field$$EnhancerBySpringCGLIB$$c09ed17f@26ae880a]

为什么构造注入可以不使用@Autowired呢,是因为在 Spring4.x 中增加了新的特性:如果类只提供了一个带参数的构造方法,则不需要对对其内部的属性写 @Autowired 注解,Spring 会自动为你注入属性,也就是非显示注入方式。

@Resource

@Resource 由Java提供,意味着脱离了spring容器更换其他容器也可以正常工作。

成员变量注入

 @Component
 public class AutowiredFieldInject {
 ​
     private static final Logger logger = LoggerFactory.getLogger(AutowiredFieldInject.class);
 ​
 ​
     @Resource
     private Field field;
 ​
 ​
     public void printOut() {
         logger.info("field is null ? [{}] --- print field [{}]", field == null, field.toString());
     }
 ​
 }
 ​
 @Configuration
 class Field{
 }
 ​
 ​
 // 输出
 field is null ? [false] --- print field [keai.yiyi.inject.Field$$EnhancerBySpringCGLIB$$3bd432d9@5c645b43]

@Resource默认使用byName注入,如果成员变量名不同于bean名称且同类型的bean只有一个实例,则会使用byType注入,而@Autowired默认使用byType注入,不会自动使用byName,需要配合@Qualifier使用

方法注入

 @Component
 public class AutowiredFieldInject {
 ​
     private static final Logger logger = LoggerFactory.getLogger(AutowiredFieldInject.class);
 ​
 ​
     private Field field;
 ​
     @Resource
     private void setxxField(Field field) {
         this.field = field;
     }
 ​
     public void printOut() {
         logger.info("field is null ? [{}] --- print field [{}]", field == null, field.toString());
     }
 ​
 }
 ​
 @Configuration
 class Field {
 }
 ​
 // 输出
 field is null ? [false] --- print field [keai.yiyi.inject.Field$$EnhancerBySpringCGLIB$$ab02e0c5@6bd16207]
 // 去掉注解后
 java.lang.NullPointerException

构造注入

@Resource无法用在static方法和构造函数上

@Autowired警告

最主要的原因是: @Autowired是Spring提供的,是特定IoC提供的特定注解,与框架形成了强绑定,一旦换用其他IoC框架,是无法支持注入的。而@Resource是JSR-250提供的,IoC容器应当去兼容它,即使更换容器,也可以正常工作。

另外可能还跟这两种注解的工作机制有关。默认情况下@Autowired是以类型(ByType)进行匹配的,@Resource是以名字(ByName)进行匹配的。也就是说当容器中存在两个相同类型的Bean时,使用@Autowired注入会报错,而使用@Resource会更精准。当然@Autowired也可以指定名称(还需配合@Qualifier注解)。