Spring 依赖注入

1,296 阅读5分钟

三种依赖注入方式

Spring bootBean 的依赖注入通常会使用 @Autowired 注解,该注解用于成员变量setter() 方法以及构造方法上,显式地声明依赖。

在最新的文档中依赖注入方式有两大类:

  • 基于构造方法的依赖注入(推荐使用)
  • 基于 setter() 方法的依赖注入

但是通常认为还有一种是基于 成员变量 的依赖注入(spring framerwork 4.0 后不推荐使用)

基于构造方法的依赖注入

用法如下

@Service
public class A {
    private B b;

    @Autowired // 这里的 @Autowired 可以加也可以不加
    public A(B b) {
        this.b = b;
    }
}

基于构造方法的依赖注入的缺点

无法解决循环依赖,例如

@Service
public class A {
    private B b;

    public A(B b) {
        this.b = b;
    }
}


@Service
public class B {
    private A a;

    public B(A a) {
        this.a = a;
    }
}

A 依赖 B,B 又依赖 A,这种相互依赖无法通过构造函数实例话,这也是基于构造方法的依赖注入最大的缺点,如果涉及到循环依赖,只能使用 基于 setter() 方法的依赖注入和基于成员变量的依赖注入来解决

基于 setter() 方法的依赖注入

用法如下

@Service
public class A {
    private B b;

    @Autowired // 也可以换成 @Resource
    public void setB(B b) {
        this.b = b;
    }
}

基于成员变量的依赖注入

基于 成员变量 的依赖注入,也有人称为基于 属性 的依赖注入或基于 字段 的依赖注入,它是通过反射机制进行依赖注入

用法如下

@Service
public class A {
    @Autowired // 也可以换成 @Resource
    private B b;
}

使用这种方式的依赖注入,idea 会在 @Autowired 处有一个黄色的波浪线提示 "Field injection is not recommended"(不推荐使用字段注入) image.png

至于为什么不推荐字段注入,原因如下

基于成员变量的依赖注入的缺点

无法注入 final 修饰的变量

final 修饰的变量,在实例化后就不可变,因此只能使用基于构造函数的依赖注入的方式,这个是基于成员变量的依赖注入和基于 setter() 方法的依赖注入做不到

掩盖了单一职责的设计思想

OOP 的设计当中有一个单一职责思想,如果你采用的是基于构造函数的依赖注入的方式来使用 SpringIOC 的时候,当你注入的太多依赖的时候,这个构造方法的参数就会很庞大,例如

@Service
public class A {
    private B b;
    private C c;
    private D d;
    private E e;
    private F f;

    public A(B b, C c, D d, E e, F f) {
        this.b = b;
        this.c = c;
        this.d = d;
        this.e = e;
        this.f = f;
    }
}

当你看到这个类的构造方法那么多参数的时候,你自然而然的会想到 这个类是不是违反了单一职责思想?

但是使用基于成员变量的依赖注入不会让你察觉,你会很沉浸在 @Autowire 当中

与 Spring 的 IOC 机制紧密耦合

使用基于字段的依赖注入方式的时候,确实可以省略构造方法setter() 方法,但是,你把控制权全给 SpringIOC 了,别的类想重新设置下某个属性,都没法处理 (当然反射可以做到),你的 Bean 脱离 Spring IOC 都很难使用,想换框架时,可移植性极差

而本身 Spring 的目的本来就是解藕和依赖反转,结果通过再次与类注入器耦合,失去了通过自动装配类字段而实现的对类的解耦,从而使类在 Spring 容器之外无效

隐藏依赖性

当你使用 SpringIOC 的时候,被注入的类应当使用一些 public 类型,如构造方法、setter() 方法,来向外界表达:我需要什么依赖。

但是基于字段的依赖注入的方式,基本都是 private 形式的,private 把属性都给封印到 class 当中了

三种依赖注入方式使用总结

通过上面,我们可以看到,基于成员变量的依赖注入方式有很多缺点,我们应当避免使用

推荐使用基于构造函数基于 setter 的依赖注入

  • 对于必需的依赖项,建议使用基于构造函数的依赖注入
  • 对于可选的依赖项,建议使用基于 setter() 方法的依赖注入
  • 对于需要解决循环依赖时,建议使用基于 setter() 方法的依赖注入

两种依赖查找方式

依赖注入是一个过程,查找依赖是其中的一个步骤,

查找依赖有两种策略

  • byType :按照 Bean 的类型查找,@Autowired 注解使用的是该策略
  • byName : 按照 Bean 的名称查找,@Resource 注解使用的是该策略

byName

Spring 中,按照 Bean 的名称查找依赖,最终会调用

Object bean = BeanFactory.getBean("beanName")

拿到依赖对象

byType

Spring 中,@Autowired 注解是按照类型查找(注入)依赖,例如

@Service
public class A {
    @Autowired 
    private B b;
}

在为 A 注入依赖 B 时,会调用

String[] beanNames = ListableBeanFactory.getBeanNamesForType(B.class)

拿到所有类型为 B.classBean 的名称,然后调用

Object bean = BeanFactory.getBean("beanName")

拿到依赖对象

也就是说,当类型为 B.classBean 只有一个时,那么使用 @Autowired 没有问题,但如果类型为 B.classBean 有多个时,A 就不知道该注入哪一个 B 了

例如

@Controller
public class AController {
    private BService b;

    @Autowired
    public AController(BService b) {
        this.b = b;
    }
}

interface BService {}

@Service
class BServiceImpl1 implements BService {}

@Service
class BServiceImpl2 implements BService {}

BService 有两个实现类 BServiceImpl1 和 BServiceImpl2,AController 在使用 @Autowired 进行依赖注入时,如果不指明使用哪个实现类就会报错

怎么解决?IDEA 已经提示你了,添加 @Qualifier 注解,指明具体要使用的 bean 的名称 image.png

例如

@Controller
public class AController {
    private BService b;

    public void setB(@Qualifier("BServiceImpl1") BService b) {
        this.b = b;
    }
}

或者使用 @Resource 注解指明要使用的 bean 的名称

@Controller
public class AController {
    private BService b;

    @Resource(name = "BServiceImpl1")
    public void setB(BService b) {
        this.b = b;
    }
}