4. [核心实践] 依赖注入详解:构筑对象关系的艺术

4. [核心实践] 依赖注入详解:构筑对象关系的艺术

摘要: IoC 是一种思想,而 DI 是其最重要的实现。本章是 Spring 学习的重中之重。我们将系统性地学习如何通过 XML 配置,将不同类型的依赖(其他 Bean、字面量、集合等)注入到目标对象中,并掌握 p/c 命名空间等简化配置的实用技巧。

4.1. 注入的两种主要方式:Setter 注入 vs 构造器注入

依赖注入(DI)的本质,是容器将一个对象所依赖的其他对象(或值)“注入”到该对象中的过程。Spring 提供了两种主要的注入方式:

  • Setter 注入: 这是最常用、最灵活的方式。容器先通过无参构造器创建 Bean 实例,然后调用该实例的 setXxx() 方法来完成依赖注入。它的优点是允许我们选择性地注入依赖,非常灵活。
  • 构造器注入: 容器通过调用 Bean 的构造方法,在实例化 Bean 的同时就完成依赖注入。它的优点是能保证依赖在对象创建时就已就绪,通常用于注入那些必不可少的、不可变的依赖。

我们将从最经典的 Setter 注入开始,深入探索其各种应用场景。


4.2. Setter 注入深度实践

4.2.1. 注入其他 Bean 对象 (ref)

这是最常见的场景:一个 Bean 依赖于另一个 Bean。例如,UserService 的运行需要依赖一个 UserDao 对象来操作数据库。

1. 准备代码

我们在之前的 spring6 项目中继续操作,首先创建 UserDaoUserService 两个类。

文件路径: src/main/java/com/example/spring6/bean/UserDao.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.spring6.bean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 数据访问层 Bean
public class UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

public void insert() {
logger.info("数据库正在保存用户数据...");
}
}

文件路径: src/main/java/com/example/spring6/bean/UserService.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.bean;

// 服务层 Bean
public class UserService {
// UserService 依赖 UserDao
private UserDao userDao;

// 关键:Spring 容器将通过调用此 set 方法,将 UserDao 的实例注入进来
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
// 调用依赖对象的方法
userDao.insert();
}
}

2. 配置与测试

接下来,我们需要在 Spring 的 XML 配置文件中,明确声明这两个 Bean 以及它们之间的依赖关系。

文件路径: src/main/resources/beans.xml (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="userBean" class="com.example.spring6.bean.User"/>
<bean id="userDaoBean" class="com.example.spring6.bean.UserDao"/>
<bean id="userServiceBean" class="com.example.spring6.bean.UserService">
<!--
使用 <property> 标签来完成 Setter 注入
- name 属性:值是 Java 类中的属性名 (userDao)
Spring 会根据这个名字推导出对应的 setter 方法 (setUserDao)
- ref 属性: 值是另一个 Bean 的 id
ref 是 "reference" (引用) 的缩写
它告诉 Spring 容器,需要将 id 为 userDaoBean 的那个对象注入进来
-->
<property name="userDao" ref="userDaoBean"/>
</bean>

</beans>

测试代码:

文件路径: src/test/java/com/example/spring6/test/DITest.java (新增测试类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.spring6.test;

import com.example.spring6.bean.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class DITest {
@Test
public void testDIBySetter() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = applicationContext.getBean("userServiceBean", UserService.class);
userService.saveUser();
}
}

在 Spring Boot 中,我们可以使用注解方式来简化 Bean 的注入和配置。

Java 代码:
文件路径: src/main/java/com/example/spring6/bean/UserDao.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.spring6.bean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

// 数据访问层 Bean
@Component
public class UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

public void insert() {
logger.info("数据库正在保存用户数据...");
}
}

Java 代码:
文件路径: src/main/java/com/example/spring6/bean/UserService.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.spring6.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

// 服务层 Bean
@Service
public class UserService {
// Spring Boot 自动注入 UserDao
private final UserDao userDao;

@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
userDao.insert();
}
}

测试代码:
文件路径: src/test/java/com/example/spring6/test/DITest.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.test;

import com.example.spring6.bean.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class DITest {
@Autowired
private UserService userService; // 自动注入

@Test
public void testDIBySetter() {
userService.saveUser();
}
}

4.2.2. 注入字面量/简单类型 (value)

除了注入对象,我们还可以注入字符串、数字、布尔值等简单类型,这些值被称为“字面量”。

1. 修改 User

我们为之前的 User 类添加 name (String) 和 age (int) 属性,并提供对应的 setter 方法。

文件路径: src/main/java/com/example/spring6/bean/User.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.spring6.bean;

public class User {
private String name;
private int age;

public User() {} // 保留无参构造器

public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }

@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}

2. 配置与测试

配置思路解读
当注入的是字面量时,我们同样使用 <property> 标签,但与之配合的是 value 属性,而不是 ref 属性。下面我们对比三种主流的配置方案。

这种方式最简单,Java 类是纯净的 POJO,不需要任何 Spring 注解。

XML 配置:

1
2
3
4
<bean id="userBean" class="com.example.spring6.bean.User">
<property name="name" value="Prorise"/>
<property name="age" value="25"/>
</bean>

Java 代码:

1
2
3
4
5
6
// User.java (无需任何注解)
public class User {
private String name;
private int age;
// ... setters
}

这种混合方式下,Bean 的定义仍在 XML 中,但值的注入可以通过注解完成。前提是必须在 XML 中开启注解处理

XML 配置:

1
2
3
<context:annotation-config/>

<bean id="userBean" class="com.example.spring6.bean.User"/>

Java 代码:

1
2
3
4
5
6
7
8
// User.java (无需 @Component)
public class User {
@Value("Prorise") // 使用 @Value 注入值
private String name;
@Value("25")
private int age;
// ... setters
}

这是现代 Spring/Spring Boot 的标准做法,XML 中只需开启组件扫描即可。

XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 开启注解配置 -->
<context:annotation-config/>

<!-- 组件扫描,自动扫描指定包下的注解 -->
<context:component-scan base-package="com.example.spring6.bean"/>

Java 代码:

1
2
3
4
5
6
7
8
9
// User.java
@Component("userBean") // 使用 @Component 将其声明为 Bean
public class User {
@Value("Prorise")
private String name;
@Value("25")
private int age;
// ...
}

测试代码:

1
2
3
4
5
6
7
@Test
public void testSimpleTypeDI() {
// 确保加载了包含以上三种配置之一的 beans.xml 文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}

4.2.3. 注入集合类型 (List, Set, Map, Properties)

Spring 提供了专门的 <list>, <set>, <map>, <props> 标签来为集合类型的属性注入值。

1. 新增 Person

文件路径: src/main/java/com/example/spring6/bean/Person.java (新增文件)

1
2
3
4
5
6
7
8
@Setter
@ToString
public class Person {
private List<String> interests;
private Set<String> phones;
private Map<String, String> family;
private Properties dbConfig;
}

2. 配置与测试

配置思路解读:
这种方式将所有配置硬编码在 XML 文件中,每个集合类型都有其对应的专属标签。

XML 配置:
文件路径: src/main/resources/beans-collection.xml (新增配置文件)

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
28
29
30
31
32
33
34
35
36
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="personBean" class="com.example.spring6.bean.Person">
<property name="interests">
<list>
<value>编程</value>
<value>游戏</value>
<value>旅行</value>
</list>
</property>

<property name="phones">
<set>
<value>13888888888</value>
<value>13999999999</value>
</set>
</property>

<property name="family">
<map>
<entry key="father" value="老王"/>
<entry key="mother" value="老李"/>
</map>
</property>

<property name="dbConfig">
<props>
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/db</prop>
</props>
</property>
</bean>
</beans>

测试代码:

1
2
3
4
5
6
@Test
public void testCollectionDI() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans-collection.xml");
Person person = context.getBean("personBean", Person.class);
System.out.println(person);
}

配置思路解读:
Spring Boot 的核心思想是 约定大于配置配置外部化。我们将所有配置数据移至 application.yml 文件中,并通过 @ConfigurationProperties 注解,以类型安全的方式将这些数据绑定到 Java 对象上。

YAML 配置:
文件路径: src/main/resources/application.yml (新增或修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 将所有配置结构化地定义在 yml 文件中
person:
interests:
- 编程
- 游戏
- 旅行
phones:
- 13888888888
- 13999999999
family:
father: 老王
mother: 老李
db-config: # yml 中的 kebab-case (短横线) 会自动映射到 Java 的 camelCase (驼峰)
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db

Java 代码:
文件路径: src/main/java/com/example/spring6/bean/Person.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.bean;
// ... imports
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

// 1. 将其声明为一个 Spring Bean
@Component
// 2. 声明此类用于绑定配置文件中 "person" 前缀的属性
@ConfigurationProperties(prefix = "person")
@Setter
@ToString
public class Person {
private List<String> interests;
private Set<String> phones;
private Map<String, String> family;
private Properties dbConfig;
}

测试代码 (Spring Boot 风格):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DiApplicationTests {
@Autowired
private Person person; // 直接注入

@Test
void testConfigurationProperties() {
System.out.println(person);
}
}

4.2.4. 处理特殊值:注入 null 与含特殊符号的 CDATA

注入 null

如果你需要明确地将一个 null 注入给属性,可以使用 <null/> 标签。

1
2
3
4
<bean id="userBean" class="com.example.spring6.bean.User">
<property name="name">
<null/> </property>
</bean>

注入含特殊符号的字符串

XML 中 <>& 等是特殊字符。如果你的字符串值包含它们,需要使用 CDATA 块来包裹,以避免 XML 解析错误。

1
2
3
4
5
<bean id="mathBean" class="com.example.spring6.bean.MathBean">
<property name="expression">
<value><![CDATA[ a < b && b > c ]]></value>
</property>
</bean>

4.3. 构造器注入的应用场景与配置

当一个依赖是 必需的,我们希望在对象创建时就保证其存在,这时就应该使用构造器注入。

1. 修改 UserService

我们将 UserService 修改为通过构造器接收 UserDao

文件路径: src/main/java/com/example/spring6/bean/UserService.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.spring6.bean;

public class UserService {
// 推荐将依赖声明为 final,确保不可变
private final UserDao userDao;

// 使用构造器进行注入
public UserService(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
userDao.insert();
}
}

2. 配置与测试

1
2
3
<bean id="service1" class="com.example.spring6.bean.UserService">
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>

注意:@Service 以及相关注解属于 springframework, 并不只属于 Spring-boot,所以在没有 Springboot 的环境也是可以运行的,不要搞混!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.spring6.bean;

import org.springframework.stereotype.Service;

@Service
public class UserService {
// 推荐将依赖声明为 final,确保不可变
private final UserDao userDao;

// 使用构造器进行注入
public UserService(UserDao userDao) {
this.userDao = userDao;
}

public void saveUser() {
userDao.insert();
}
}

由于我们之前开启过组件扫描了,所以我们将 beans.xmluserServiceBean 删除即可


4.4. 简化 XML:p 命名空间与 c 命名空间

SpringBoot 简化: 由于现代开发全面转向注解,pc 命名空间已成为历史,我们了解即可,完全无需记忆

为了简化 XML 的冗长写法,Spring 提供了 pc 两个命名空间。

  • p 命名空间:用于简化 Setter 注入 (p for property)。
  • c 命名空间:用于简化 构造器注入 (c for constructor)。

1. 添加命名空间声明

首先,需要在 <beans> 根标签上添加 pc 的声明。

1
2
3
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c" ...>

2. 使用示例

1
2
3
4
5
6
7
8
9
<bean id="user1" class="com.example.spring6.bean.User">
<property name="name" value="AnZhiYu"/>
</bean>
<bean id="user2" class="com.example.spring6.bean.User" p:name="Prorise"/>

<bean id="service1" class="com.example.spring6.bean.UserService">
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>
<bean id="service2" class="com.example.spring6.bean.UserService" c:userDao-ref="userDaoBean"/>

4.5. 外部化配置:引入 .properties 属性文件

在实际项目中,数据库连接信息等敏感、易变的数据不应硬编码在 XML 中。正确的做法是将其放在外部的 .properties 文件中,由 Spring 动态加载。

1. 创建 jdbc.properties 文件

文件路径: src/main/resources/jdbc.properties (新增文件)

1
2
3
4
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/testdb
db.username=root
db.password=123456

2. 添加 context 命名空间并加载文件

我们需要 context 命名空间下的 <context:property-placeholder> 标签来加载属性文件。

文件路径: src/main/resources/beans-db.xml (新增配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<context:property-placeholder location="classpath:jdbc.properties"/>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
</beans>

SpringBoot 简化: Spring Boot 约定大于配置,它会自动加载 src/main/resources/application.propertiesapplication.yml 文件。我们只需在类中使用 @Value("${db.driver}") 注解即可直接注入属性值,或通过 @ConfigurationProperties 进行类型安全的属性绑定,比 XML 配置简洁得多。

3.Spring Boot 示例

在 Spring Boot 中,我们可以通过直接在 application.properties 文件中进行配置,使用 @Value@ConfigurationProperties 注解来实现外部化配置的功能。

application.properties 配置:
文件路径: src/main/resources/application.properties (新增或修改)

1
2
3
4
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/testdb
db.username=root
db.password=123456

Java 代码:
文件路径: src/main/java/com/example/spring6/config/DataSourceConfig.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
28
29
30
31
32
package com.example.spring6.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;

@Configuration
public class DataSourceConfig {

@Value("${db.driver}")
private String driverClassName;

@Value("${db.url}")
private String url;

@Value("${db.username}")
private String username;

@Value("${db.password}")
private String password;

@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}

application.yml 配置:
文件路径: src/main/resources/application.yml (替代 application.properties,如果需要使用 YAML 格式)

1
2
3
4
5
db:
driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/testdb
username: root
password: 123456

Java 代码: 使用 @ConfigurationProperties 绑定配置属性

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
package com.example.spring6.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "db")
public class DataSourceConfig {

private String driver;
private String url;
private String username;
private String password;

@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}

// Getters and Setters
}

本章总结: 至此,我们已经全面掌握了在 Spring 经典 XML 配置中进行依赖注入的各种核心技能。虽然现代开发已更多地转向注解,但理解这些基于 XML 的配置原理,对于我们深入把握 Spring 的 IoC 本质、排查疑难问题具有不可替代的价值。