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
对象来操作数据库。
Spring6实现方式 Spring Boot 风格 1. 准备代码 我们在之前的 spring6
项目中继续操作,首先创建 UserDao
和 UserService
两个类。
文件路径 : 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;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;public class UserService { private UserDao 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 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;@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;@Service public class UserService { 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
属性。下面我们对比三种主流的配置方案。
纯XML配置 XML定义Bean + 注解注入值 纯注解方式 (现代标准) 这种方式最简单,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 public class User { private String name; private int age; }
这种混合方式下,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 public class User { @Value("Prorise") private String name; @Value("25") private int age; }
这是现代 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 @Component("userBean") public class User { @Value("Prorise") private String name; @Value("25") private int age; }
测试代码 :
1 2 3 4 5 6 7 @Test public void testSimpleTypeDI () { ApplicationContext applicationContext = new ClassPathXmlApplicationContext ("beans.xml" ); User user = applicationContext.getBean("userBean" , User.class); System.out.println(user); }
1 User{name='Prorise' , age=25}
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 配置 (原理理解) 方案二:Spring Boot 类型安全配置 (推荐实践) 配置思路解读 : 这种方式将所有配置硬编码在 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 person: interests: - 编程 - 游戏 - 旅行 phones: - 13888888888 - 13999999999 family: father: 老王 mother: 老李 db-config: 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;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;@Component @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 { private final UserDao userDao; public UserService (UserDao userDao) { this .userDao = userDao; } public void saveUser () { userDao.insert(); } }
2. 配置与测试 Spring-boot使用方法 Spring-boot使用方法 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 { private final UserDao userDao; public UserService (UserDao userDao) { this .userDao = userDao; } public void saveUser () { userDao.insert(); } }
由于我们之前开启过组件扫描了,所以我们将 beans.xml
的 userServiceBean
删除即可
4.4. 简化 XML:p 命名空间与 c 命名空间 SpringBoot 简化 : 由于现代开发全面转向注解,p
和 c
命名空间已成为历史,我们了解即可,完全无需记忆 。
为了简化 XML 的冗长写法,Spring 提供了 p
和 c
两个命名空间。
p
命名空间:用于简化 Setter 注入 (p
for property)。c
命名空间:用于简化 构造器注入 (c
for constructor)。1. 添加命名空间声明 首先,需要在 <beans>
根标签上添加 p
和 c
的声明。
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.properties
或 application.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; } }
本章总结 : 至此,我们已经全面掌握了在 Spring 经典 XML 配置中进行依赖注入的各种核心技能。虽然现代开发已更多地转向注解,但理解这些基于 XML 的配置原理,对于我们深入把握 Spring 的 IoC 本质、排查疑难问题具有不可替代的价值。