Spring系列之依赖注入

Spring 中所有的 Bean 都是通过容器来进行管理的。每个 POJO 都可以是一个 Spring Bean。容器会管理 Bean 的依赖关系,这种依赖关系有可能是 Bean 之间的,也有可能是 Bean 对配置数据的依赖。在使用 Spring 的时候,开发者需要做的就是让 Spring 容器知道这些依赖关系,然后剩下的事情交给 Spring 容器就行了。

Spring 容器在初始化的时候会做两件事,将配置中的所有 POJO 加载进容器生成 Bean 并且注入 Bean 之间的依赖关系。配置 Bean 之间的依赖关系就是我们所说的依赖注入,需要注意的是这个生成 Bean 和依赖注入之间并不存在严格的先后关系,具体下面再说。

Bean 的配置

如果我们想让 Spring 来管理 Bean,第一步就是要将这些 Bean 装入容器中。把 Bean 装入容器中的方式有三种:

  • 使用 xml 配置文件
  • 使用注解78
  • 使用 Java 代码

这三种方式完成的效果都一样,而且这三种方式可以混合使用。在下面我会主要使用 xml 的方式来作为例子来进行演示,因为 xml 配置文件相对比较直观,也会提供注解版本和 Java 代码版本的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Gun implements Weapon{
@Override
public boolean attack() {
return false;
}
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="gun" name="gun1,gun2" class="cn.rayjun.springdemo.springcontainer.Gun">
</bean>
</beans>

在上面的配置文件中,我们创建一个 xml 配置文件,其中 xml 根元素是 beans,然后里面有一个 bean 元素,这就是一个 Spring Bean。上面配置文件的意思就是告诉容器,我有一个名字叫做 Gun 的 Java 类,现在交给你管理了,就这么简单。

上面 XML Bean 的配置中有 id,name,class 等几个属性。其中 class 很简单,就是类的全限定名称,这个相当于告诉 Spring 容器要创建哪个类的实例。id 和 name 都是用来标识一个 Bean 的唯一性。每个 Bean 的 id 在容器中只能是唯一的,而 name 则可以有多个,每个 name 使用 ,; 或者空格来隔开,id 的命名需要符合 XML 的命名规范,也就是不能使用特殊字符,但 name 则可以使用特殊字符来进行命名,如果没有定义 id,则会把 name 的第一个值定为 id,如果 id 和 name 都没有定义,则使用类全名加上数字编号来作为 id。获取 bean 的时候,可以通过如下的方式获取:

1
2
3
4
5
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Gun gun1 = (Gun) context.getBean("gun1");
Gun gun = (Gun) context.getBean("gun");

System.out.println(gun == gun1); // true

在实际使用 Spring 的过程中是不会使用上面的方式来使用 Bean,都会通过依赖注入的方式来获取。

使用注解如何配置呢,使用注解的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class Gun implements Weapon{
@Override
public boolean attack() {
return false;
}
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<context:component-scan base-package="cn.rayjun.springdemo"/>
</beans>

上面的代码与完全使用 xml 配置的效果相同,你可能会想,这样更麻烦了,其实不是,在 xml 中添加 context:component-scan 之后,cn.rayjun.springdemo 包下所有被 @Component 注解的类都会自动添加到容器中。也就是 xml 中就不再需要 <bean> 这个标签了,如果想把 xml 从代码中完全剔除掉上面的代码可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class Gun implements Weapon{
@Override
public boolean attack() {
return false;
}
}

@Configuration
@ComponentScan(basePackages = "cn.rayjun.springdemo")
public class SoldierConfig {
}

除了 @Component 注解之外,还有 @Repository, @Service, @Controller 等注解,@Repository 主要用来标识数据层,@Service 主要用来标识 Service 层,@Controller 主要用来标识 Controller 层。这些注解其实并没有什么不同,这么做只是为了让代码的分层更加清晰,这些注解都可以使用 @Componenet 替代。@Repository注解的实现如下:

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
@AliasFor(
annotation = Component.class
)
String value() default "";
}

实际上 @Repository 等注解就是使用 @Component 注解实现的。Spring 号称是无侵入性的,但是上面的代码却在 Gun 类上添加了 @Component 的注解,虽然不影响代码的功能,但还是稍微有点侵入性的,下面是终极的方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Gun implements Weapon{
@Override
public boolean attack() {
return false;
}
}

@Configuration
public class SoldierConfig {
@Bean
public Gun gun() {
return new Gun();
}
}

这里使用带 @Configuration 的配置类完全替代 XML,这样一来,代码中没有 XML,通过也做到了对代码真正的无侵入性。

上面是将代码放入到容器中的三种方法,这三种方法不是独立存在的,而是可以混用的。这三种配置各有各的好处,XML 让 Bean 之间的依赖很清晰,而且不用修改源码;注解可以基本消除掉配置文件,但是这样代码中就会充斥各种注解;Java 配置则可以完全替代 XML。

依赖注入

在上面我们说生成 Bean 和依赖注入不存在严格的先后关系,这是因为 DI有两种方式:构造方法参数注入Setter 方法注入。如果是构造参数注入,那么在 Bean 生成对象的时候就需要将依赖注入,如果是 Setter 方法注入,则是在 Bean 对象生成之后才会注入。

下面来看看如何进行依赖注入,先来看 XML 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 构造方法注入
public class Soldier {
private Weapon weapon;

public Soldier(Weapon weapon) {
this.weapon = weapon;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<bean id="gun" class="cn.rayjun.springdemo.springcontainer.Gun">
</bean>

<bean id="solider" class="cn.rayjun.springdemo.springcontainer.Soldier">
<constructor-arg name="weapon" ref="gun"/>
</bean>
</beans>

使用构造方法注入时,需要在构造方法的参数上声明所需要的依赖,然后在 XML 配置中通过 <constructor-arg/> 将依赖注入。再来看看 Setter 方法注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// setter 方法注入
public class Soldier {
private Weapon weapon;

public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<bean id="gun" class="cn.rayjun.springdemo.springcontainer.Gun">
</bean>

<bean id="solider" class="cn.rayjun.springdemo.springcontainer.Soldier">
<property name="weapon" ref="gun"/>
</bean>
</beans>

首先 Solider 中需要有待注入依赖的 Setter 方法,然后在 XML 配置中通过 <property/> 来进行注入。那么构造参数注入和 Setter 方法注入怎么选呢?官方的推荐的做法是:如果依赖关系是强依赖时,使用构造参数注入,如果是可选依赖,则使用 setter 进行注入。强依赖可以理解为当前这个 Bean 正常运行所必须的依赖

下面再来看看注解的是如何来进行依赖注入的,先看下面这段代码,下面的这段代码使用的是构造函数参数注入的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class Soldier {
private Weapon weapon;
@Autowired
public Soldier(Weapon weapon) {
this.weapon = weapon;
}
}
@Component
public class Knife implements Weapon{
public boolean attack() {
return false;
}
}
@Component
public class Gun implements Weapon{
public boolean attack() {
return false;
}
}

除了使用构造函数参数注入之外,还可以使用 Setter 方法注入和成员变量注入,这些注入方式的效果都一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Setter 方法注入
@Component
public class Soldier {
private Weapon weapon;
@Autowired
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
}
// 私有成员变量注入
@Component
public class Soldier {
@Autowired
private Weapon weapon;
}

在上面的代码中,我们使用的是 @Autowired 来进行注入,也可以使用 @Inject 来进行依赖注入。这两个注解是等价的,可以在代码中互相替换,但是推荐在整个项目中保持统一。如果项目的依赖比较复杂,那么代码中就会充斥着这些注解,这也是使用注解配置的问题,大量的这样的注解会让代码不好管。注解使用是最便利的,但也是最难管理的。

还有最后一种注入方式,使用 Java 代码进行注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
public class SoldierConfig {

@Bean
public Gun newGun() {
return new Gun();
}

@Bean
public Knife newKnife() {
return new Knife();
}

@Bean
public Soldier newSoldier() {
// 使用构造方法参数注入
return new Soldier(newGun());
}

@Bean
public Soldier newSoldier() {
// 使用 Setter方法参数注入
Soldier s = new Soldier();
s.setWeapon(newGun());
return s;
}
}

@ComponentScan 注解用来配置需要扫描的包的范围,被 @Bean 注解的方法表示会得到一个返回类型的 Bean,然后可以直接通过调用相应方法为 Bean 注入依赖。使用 Java 配置的好处是即可以不使用 XML 配置文件,又不会对代码有侵入。上面的代码中都是类之间的依赖,如果依赖的是普通的字面量或者一些容器类型,也可以进行注入,注入的方式如下:

1
2
3
4
<bean id="soldier" class="cn.rayjun.springdemo.container.Soldier" >
<property name="name" value="Tom"/>
<property name="age" value="12" />
</bean>

配置的 value 会根据 Bean 中成员的类型自动进行转换。还可以注入容器类型的值:

1
2
3
4
5
6
7
8
9
10
11
<bean id="soldier" class="cn.rayjun.springdemo.container.Soldier" >
<property name="name" value="Tom"/>
<property name="age" value="12" />
<property name="foods">
<list>
<value>apple</value>
<value>orange</value>
<value>banana</value>
</list>
</property>
</bean>

依赖冲突

Soldier 中需要注入一个 Weapon 类型的 Bean,现在 Gun 和 Knife 都实现了 Weapon,采用上面的方式注入时,就会报 UnsatisfiedDependencyException 异常,因为容器无法决定要注入哪一个。解决的方法有三种:

  • @Primary 注解
  • @Qualifier 注解
  • 自定义注解

被 @Primary 注解的 Bean 会被优先注入,这样就不会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class Soldier {
private Weapon weapon;
@Autowired
public Soldier(Weapon weapon) {
this.weapon = weapon;
}
}

@Component
public class Knife implements Weapon{
public boolean attack() {
return false;
}
}

@Component
@Primary
public class Gun implements Weapon{
public boolean attack() {
return false;
}
}

还可以使用 @Qualifier 来明确告诉容器使用的是哪个 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class Soldier {
private Weapon weapon;
@Autowired
public Soldier(@Qualifier("gun") Weapon weapon) {
this.weapon = weapon;
}
}

@Component
@Qualifier("knife")
public class Knife implements Weapon{
public boolean attack() {
return false;
}
}

@Component
@Qualifier("gun")
public class Gun implements Weapon{
public boolean attack() {
return false;
}
}

自定义注解稍微复杂点,后续再写文章来说明。

循环依赖

//循环依赖图

在一些情况下,会出现循环依赖,虽然这种情况比较少,但还是有可能会出现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ClassA {

private ClassB classB;

public ClassA(ClassB classB) {
this.classB = classB;
}
}
public class ClassB {

private ClassA classA;

public ClassB(ClassA classA) {
this.classA = classA;
}
}

<bean id="classB" class="cn.rayjun.springdemo.springcontainer.cycle.ClassB">
<constructor-arg name="classA" ref="classA"/>
</bean>

<bean id="classA" class="cn.rayjun.springdemo.springcontainer.cycle.ClassA">
<constructor-arg name="classB" ref="classB"/>
</bean>

如果按照上面的配置,启动的时候会报 UnsatisfiedDependencyException 异常,这就是循环依赖,解决的办法也很简单,就是把构造方法注入改成 Setter 方法注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ClassA {
private ClassB classB;

public void setClassB(ClassB classB) {
this.classB = classB;
}
}
public class ClassB {

private ClassA classA;

public void setClassA(ClassA classA) {
this.classA = classA;
}
}
<bean id="classB" class="cn.rayjun.springdemo.springcontainer.cycle.ClassB">
<property name="classA" ref="classA"/>
</bean>
<bean id="classA" class="cn.rayjun.springdemo.springcontainer.cycle.ClassA">
<property name="classB" ref="classB"/>
</bean>

改成 Setter 方法之后就可以注入成功了,原因就是构造函数注入和 Setter 方法注入的时机不同,构造函数需要在生成对象的时候就注入,这是 ClassA 和 ClassB 就产生了鸡生蛋还是蛋生鸡的问题。而 Setter 方法注入这是在生成对象之后,那就可以成功注入了。

Spring 容器的内容很多,上面仅仅介绍了 Spring 最核心的部分,这是 Spring 容器构建的基础,下一篇会详细介绍 Spring 的另一大特性 AOP。关于容器的一些语法细节可以去查询官方文档。

© 2020 Rayjun    PowerBy Hexo