Spring 中的依赖注入
# 30.Spring 中的依赖注入
之前我们仅仅是消除了表现层对于 service 层的依赖;而 service 层对于 dao 层的依赖,我们还没有消除,例如在 service 层中还有一个 new dao 对象的代码,本文我们就来讲讲如何用注入的方式,降低耦合性
# 什么是依赖注入
依赖注入,全称 Dependency Injection,简称 DI
IoC 的作用:降低程序间的耦合,依赖关系的管理,对象的创建,就交由 Spring;在当前类需要用到其他类的对象时,由 Spring 为我们提供,我们只需要在配置文件中说明依赖关系,就可以在程序中使用了。
例如我们的 service 层依赖 dao 层,这就是一种依赖关系。
依赖关系的维护,就称之为依赖注入。也就是说,某个对象 A 的创建,依赖另一个对象 B,而我们需要将对象 B“注入”到对象 A 中,这样对象 A 才能创建成功。
能注入的数据有三类:
- 基本类型和 String
- 其他 bean 类型(在配置文件中或者注解配置过的 bean)
- 复杂类型/集合类型
注入的方式有三种:
- 使用构造函数提供
- 使用 set 方法提供
- 使用注解提供(后续会讲)
# 构造函数注入
接下来我们来演示如何注入
- 在 service 层中,新建几个成员变量,其中包含了基本类型和 String 类型,和其他 bean 类型(Date 也可以用 Bean 来管理)
- 新建构造函数,需要传入成员变量的值进行初始化
public class AccountServiceImpl implements IAccountService {
private String name;
private Integer age;
private Date birthday;
public AccountServiceImpl(String name, Integer age, Date birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
@Override
public void saveAccount() {
System.out.println("service中的saveAccount方法执行了" + name + "," + age + "," + birthday);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
注意:如果是经常变化的数据,并不适用于注入的方式。比如一个注册用户的功能,其用户名、密码等信息的每个人都不同的,用配置文件的方式肯定不合适;后续我们对于这种情况怎么处理。本文我们主要讲解依赖之间的关系,Spring 是怎么管理的。
然后我们配置注入,使用的标签是 constructor-arg,标签出现的位置在 bean 标签的内部。标签中的属性:
- type: 用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
- index: 用于指定要注入的数据给构造函数中指定索引位置的参数赋值,索引的位置是从 0 开始的
- name: 用于指定要注入的数据给构造函数中指定名称的参数赋值
- value: 用于提供基本类型和 String 类型的数据
- ref: 用于指定其他的 bean 类型数据,它指的是在 Spring 的 IoC 核心容器中出现过的 bean 对象
其中,前 3 个属性是用来定位给哪个属性赋值,后面 2 个属性是决定赋什么值
例如,我们想给 String 属性赋值:
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl">
<constructor-arg type="java.lang.String" value="Peterjxl"/>
</bean>
2
3
但如果参数列表中有两个 String 类型,就判断不出到底是给哪个 String 赋值,因此 type 属性通常不能独立完成注入的功能。
如果我们可以用 index 属性,由于通过索引位置能确定一个变量,能独立完成注入,但这种方式要记住索引的位置,如果方法参数有变化还得改,也比较麻烦。
因此,我们一般是用 name 属性,来定位变量。
至此,我们可以给 name 和 age 赋值了:
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="Peterjxl"/>
<constructor-arg name="age" value="18"/>
</bean>
2
3
4
虽然 value 的内容是字符串,但是 Spring 很智能,能帮我们将字符串 18 转为 Integer 类型;
但对于日期类型这种比较复杂的,Spring 就无能为例了,如果我们这样配置:
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="Peterjxl"/>
<constructor-arg name="age" value="18"/>
<constructor-arg name="birthday" value="2022-5-21"/>
</bean>
2
3
4
5
运行是会报错的:
Error creating bean with name 'accountService' defined in class path resource [bean.xml]:Unsatisfied dependency expressed through constructor parameter 2: Could not convert argument value of type [java.lang.String] to required type [java.util.Date]
大意:构造方法中,索引位置为 2 的参数赋值失败,不能转换 String 类型为 Date 类型
解决方法一:我们将 Date 也用容器来管理,并用 ref 来引用:
<bean id="now" class="java.util.Date"/>
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="Peterjxl"/>
<constructor-arg name="age" value="18"/>
<constructor-arg name="birthday" ref="now"/>
</bean>
2
3
4
5
6
7
此时,是可以正常运行的:
service中的saveAccount方法执行了Peterjxl,18,Mon May 01 21:36:40 CST 2023
使用构造函数注入的优缺点:
- 优势:在获取 bean 对象时,注入数据是必须的操作,否则对象无法创建成功
- 弊端:改变了 bean 对象的实例化方式,使我们在创建对象时,即使用不到这些数据,也必须提供
所以我们一般比较少用构造函数注入的方式。
# set 方法注入
接下来我们演示使用 set 方法注入的方式,我们创建一个新的类 AccountServiceImpl2:
public class AccountServiceImpl2 implements IAccountService {
private String name;
private Integer age;
private Date birthday;
@Override
public void saveAccount() {
System.out.println("service中的saveAccount方法执行了" + name + "," + age + "," + birthday);
}
}
2
3
4
5
6
7
8
9
10
请读者自行生成 getter 和 setter
接下来我们配置 bean.xml,使用 property 来完成注入。property 标签出现的位置:bean 标签的内部
标签的属性:
- name: 用于指定注入时所调用的 set 方法名称(去掉方法名前面的 set,然后首字母小写)
- value: 用于提供基本类型和 String 类型的数据
- ref: 用于指定其他的 bean 类型数据,它指的是在 Spring 的 IOC 核心容器中出现过的 bean 对象
<!-- set方法注入 -->
<bean id="accountService2" class="com.peterjxl.service.impl.AccountServiceImpl2">
<property name="name" value="Peterjxl"/>
<property name="age" value="18"/>
<property name="birthday" ref="now"/>
</bean>
2
3
4
5
6
然后我们在 Client 中演示:
IAccountService as2 = (IAccountService)ac.getBean("accountService2");
as2.saveAccount();
2
运行结果:
service中的saveAccount方法执行了Peterjxl,18,Mon May 01 21:49:32 CST 2023
使用 set 方法注入的优缺点:
优势:创建对象时没有明确的限制,可以直接使用默认构造函数(注意,如果有其他的有参的构造方法,那么得定义一个无参的构造方法)
弊端:如果有某个成员必须有值,但没有配置对应的 property 标签,则获取对象时 set 方法没有执行,导致对象创建出来是不能用的
总而言之,我们经常用的 set 方式注入
# 集合数据注入
最后我们来说下集合类型的数据如何注入。我们创建一个新的类 AccountServiceImpl3,并加入一些集合类型的成员变量:
public class AccountServiceImpl3 implements IAccountService {
private String[] myStrs;
private List<String> myList;
private Set<String> mySet;
private Map<String, String> myMap;
private Properties myProps;
@Override
public void saveAccount() {
System.out.println(Arrays.toString(myStrs));
System.out.println(myList);
System.out.println(mySet);
System.out.println(myMap);
System.out.println(myProps);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
请注意生成 setter 方法,getter 方法我们暂时不用。
接下来我们配置 bean.xml:我们在 property 标签里使用对应的集合标签,来完成注入:
<!-- 复杂类型/集合类型的注入-->
<bean id="accountService3" class="com.peterjxl.service.impl.AccountServiceImpl3">
<property name="myStrs">
<array>
<value>aaa</value>
<value>bbb</value>
<value>ccc</value>
</array>
</property>
<property name="myList">
<list>
<value>aaa</value>
<value>bbb</value>
<value>ccc</value>
</list>
</property>
<property name="mySet">
<set>
<value>aaa</value>
<value>bbb</value>
<value>ccc</value>
</set>
</property>
<property name="myMap">
<map>
<entry key="aaa" value="111"/>
<entry key="bbb">
<value>222</value>
</entry>
</map>
</property>
<property name="myProps">
<props>
<prop key="testC">111</prop>
<prop key="testD">222</prop>
</props>
</property>
</bean>
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
28
29
30
31
32
33
34
35
36
37
38
39
运行结果:
[aaa, bbb, ccc]
[aaa, bbb, ccc]
[aaa, bbb, ccc]
{aaa=111, bbb=222}
{testD=222, testC=111}
2
3
4
5
其实,对应的标签可以互换。
用于给 List 结构集合类型属性注入的标签是:<list> <array> <set>
用于给 Map 集合类型属性注入的标签是: <map> <props>
结构相同,标签可以互换。例如我们给数组注入时,使用的是 array 标签,其实用 list 标签也可以:
<property name="myStrs">
<list>
<value>aaa</value>
<value>bbb</value>
<value>ccc</value>
</list>
</property>
2
3
4
5
6
7
# 源码
本项目已将源码上传到 GitHub (opens new window) 和 Gitee (opens new window) 上。并且创建了分支 demo3,读者可以通过切换分支来查看本文的示例代码。