Java-Spring入门指南(四)深入IOC本质与依赖注入(DI)实战
- 前言
- 一、IOC的本质
-
- [1.1 传统开发的控制权是什么?](#1.1 传统开发的控制权是什么?)
- [1.2 IOC的本质](#1.2 IOC的本质)
- [1.3 IOC是"思想",DI是"实现"](#1.3 IOC是“思想”,DI是“实现”)
- 二、什么是依赖注入(DI)?
-
- [2.1 什么是"依赖"?](#2.1 什么是“依赖”?)
- [2.2 依赖注入的定义](#2.2 依赖注入的定义)
- [2.3 DI的核心目标](#2.3 DI的核心目标)
- 三、Spring依赖注入实战
-
- [3.1 方式一:构造器注入](#3.1 方式一:构造器注入)
-
- [3.1.1 三种构造器注入配置](#3.1.1 三种构造器注入配置)
- [3.1.2 测试构造器注入](#3.1.2 测试构造器注入)
- [3.2 方式二:setter注入](#3.2 方式二:setter注入)
-
- [3.2.1 基本类型 + 引用类型注入](#3.2.1 基本类型 + 引用类型注入)
- [3.2.2 测试setter注入(用你的test1()方法)](#3.2.2 测试setter注入(用你的test1()方法))
- [3.2.3 复杂类型注入:数组(books)、List(hobbies)](#3.2.3 复杂类型注入:数组(books)、List(hobbies))
-
- [补充后的User Bean配置:](#补充后的User Bean配置:)
- 再次执行test1(),预期结果:
- [3.3 构造器注入 vs setter注入:怎么选?](#3.3 构造器注入 vs setter注入:怎么选?)
前言
在上一篇博客中,我们剖析了IoC容器的核心机制与Bean的生命周期,但留下了一个关键伏笔:IoC(控制反转)作为Spring的核心思想,到底是如何落地的?
其实,IoC的"反转控制"并非空中楼阁------它的具体实现,就是我们这篇要讲的依赖注入(DI)。如果说IoC是"按需分配"的理念,那DI就是"把东西送到你手上"的具体动作。
这一篇,我们将从「IoC的本质」切入,彻底讲清IoC与DI的关系,再结合你提供的Student
、User
、Address
代码,手把手实战Spring依赖注入的两种核心方式(构造器注入、setter注入),让你不仅"会用DI",更"懂DI为什么要这么设计"。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482

一、IOC的本质
很多人对IoC的理解停留在"把创建对象的权力交给容器",但这只是表面------IoC的本质是将"对象全生命周期的控制权"从开发者手中反转给容器 ,包括对象的创建、依赖组装、生命周期管理(初始化/销毁)。
1.1 传统开发的控制权是什么?
在没有Spring的传统开发中,开发者要全权掌控对象的生命周期,举个例子:
java
// 1. 手动创建对象(控制"创建")
Student student = new Student("张三", 18);
// 2. 若Student依赖Address,手动组装依赖(控制"依赖")
Address address = new Address();
address.setCityName("北京");
student.setAddress(address);
// 3. 对象销毁完全依赖JVM(无法控制"销毁")
这里的"控制权"体现在三点:
- 创建权 :用
new
关键字手动创建对象; - 组装权 :手动将依赖的对象(如
Address
)赋值给目标对象(如Student
); - 管理权:无法主动管理对象的初始化/销毁,只能依赖JVM垃圾回收。
1.2 IOC的本质
Spring IoC的核心,就是把上面三点控制权全部"反转"给容器------开发者只需要做两件事:
- 告诉容器"要什么对象"(在XML中配置
<bean>
); - 告诉容器"对象需要什么依赖"(配置
constructor-arg
或property
)。
剩下的工作(创建对象、组装依赖、调用初始化方法、销毁对象)全由容器完成。用表格对比更直观:
控制项 | 传统开发(开发者控制) | Spring IoC(容器控制) |
---|---|---|
对象创建 | 手动new (如new Student(...) ) |
容器根据<bean> 配置创建 |
依赖组装 | 手动set (如student.setAddress(...) ) |
容器自动注入(ref 或value 配置) |
初始化/销毁 | 手动调用方法(如student.init() ) |
容器调用init-method /destroy-method |
需求变更(换依赖) | 修改所有用到该对象的代码 | 只修改XML配置中的依赖引用 |
1.3 IOC是"思想",DI是"实现"
IOC与DI 两者是"思想与实现"的关系:
- IoC(控制反转) :是Spring的核心思想,定义了"将对象控制权交给容器"的目标;
- DI(依赖注入):是IoC思想的具体实现,回答了"容器如何实现控制权反转"------通过"在创建对象时自动注入依赖",完成对象的组装。
类比:IoC像"要实现全国快递上门"的理念,DI像"用快递车把包裹送到客户手上"的具体动作------没有DI,IoC只是空泛的口号;没有IoC,DI也没有存在的意义。
二、什么是依赖注入(DI)?
理解了IoC的本质后,DI就很好懂了------它是容器帮我们"组装对象依赖"的核心手段。
2.1 什么是"依赖"?
"依赖"是指:如果A对象需要调用B对象的属性或方法才能完成功能,那么A依赖B。
- 比如我们有一个
User
类需要Address
的cityName
属性来描述用户地址 →User
依赖Address
;
java
public class User {
private String name;
private Address address;
public void setAddress(Address address) {
this.address = address;
}
public void setName(String name) {
this.name = name;
}
}

-
比如我们有一个
Student
类需要name
和age
属性才能初始化 →Student
依赖name
和age
;
-
依赖的类型:可以是基本类型 (String、int)、引用类型 (Address、User),也可以是集合类型(数组、List)。
2.2 依赖注入的定义
DI(Dependency Injection)的全称是"依赖注入",直白理解就是:
容器在创建"依赖方对象"(如User、Student)时,自动将它所依赖的"被依赖方"(如Address、name值)注入到该对象中,无需依赖方自己去获取。
用我们的User
和Address
举例子:
java
public class User {
private String name;
private Address address;
public void setAddress(Address address) {
this.address = address;
}
public void setName(String name) {
this.name = name;
}
}

- 依赖方:
User
(需要Address
); - 被依赖方:
Address
(被User
依赖); - DI的过程:容器创建
User
对象时,自动把Address
对象"塞"到User
的address
属性中,不用我们写user.setAddress(new Address())
。
2.3 DI的核心目标
传统开发中,依赖关系是"硬编码"在代码里的(如Student
依赖Address
,就必须在Student
里new Address()
),导致代码高耦合。
而DI通过"配置化管理依赖",彻底打破了这种耦合:
- 比如我们想把
User
的Address
从"NX"换成"北京",只需要修改Address
Bean的cityName
配置,不用改User
类的任何代码; - 比如我们想给
Student
换个名字,只需要修改constructor-arg
的value
,不用重新new Student()
。
三、Spring依赖注入实战
Spring支持多种DI方式,最常用的是构造器注入 和setter注入。
核心原则:Spring DI的本质是"给Bean的属性赋值"------无论属性是基本类型、引用类型还是集合类型,容器都会根据配置完成赋值。
3.1 方式一:构造器注入
构造器注入是指:容器通过调用Bean的有参构造器,在创建Bean的同时给属性赋值。
- 适用场景:Bean的属性是"必填项";
- 核心要求:Bean必须有对应的有参构造器。
3.1.1 三种构造器注入配置
首先编写一个Student
类代码:
java
public class Student {
private String name;
private int age;
// 无参构造(setter注入需要,构造器注入也建议保留)
public Student() {}
// 有参构造(构造器注入的核心)
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" + "age=" + age + ", name='" + name + '\'' + '}';
}
}
对应的beans.xml
配置(三种注入方式):
方式1:根据"构造器参数名"注入(最直观)
通过name
属性指定构造器的参数名,直接匹配赋值:
xml
<bean id="st" class="org.example.pojo.Student">
<!-- name对应构造器的参数名(如"name"对应Student(String name, ...)) -->
<constructor-arg name="name" value="张三"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
</bean>
- 优点:直观易懂,参数名与构造器一一对应,不易出错;
- 注意:如果构造器参数名修改(如把
name
改成studentName
),需要同步修改XML中的name
属性。
方式2:根据"构造器参数类型"注入(谨慎使用)
通过type
属性指定参数类型,避免参数名不匹配的问题:
xml
<bean id="st" class="org.example.pojo.Student">
<!-- type对应参数的全类名(String是java.lang.String,int是int) -->
<constructor-arg type="java.lang.String" value="李四"></constructor-arg>
<constructor-arg type="int" value="20"></constructor-arg>
</bean>
- 缺点:如果构造器有多个同类型参数(如
Student(String name, String gender)
),容器无法区分,会报错; - 适用场景:构造器参数类型唯一(如只有一个String、一个int)。
方式3:根据"构造器参数下标"注入(最稳定)
通过index
属性指定参数在构造器中的位置(从0开始),这是你代码中使用的方式:
xml
<bean id="st" class="org.example.pojo.Student">
<!-- index=0对应构造器第一个参数(name),index=1对应第二个参数(age) -->
<constructor-arg index="0" value="00"></constructor-arg>
<constructor-arg index="1" value="99"></constructor-arg>
</bean>
- 优点:不依赖参数名和类型,即使参数名修改,只要顺序不变,配置依然有效;
- 推荐场景:大多数场景(尤其是构造器参数较多时)。
3.1.2 测试构造器注入
接着编写一个MyTest类
java
public class MyTest {
@Test
public void test() {
// 1. 加载配置文件,创建容器(ApplicationContext会预加载单例Bean)
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
// 2. 从容器获取Student Bean(id为"st")
Student st = (Student) ac.getBean("st");
// 3. 输出结果,验证注入是否成功
System.out.println(st);
}
}
结果 :
- 结果说明:容器通过构造器注入,成功给
Student
的name
和age
赋值,证明构造器注入生效。
常见问题:
- 如果构造器参数类型不匹配(如给
age
传字符串"abc"
),容器启动时会报TypeMismatchException
; - 如果没有对应的有参构造器(如只写了无参构造),容器会报
NoSuchMethodException
。
3.2 方式二:setter注入
setter注入是指:容器先通过无参构造器创建Bean,再调用属性的setter方法给属性赋值。
- 适用场景:Bean的属性是"可选项"(比如
User
的books
数组,没有也能正常使用); - 核心要求:Bean必须有无参构造器 (默认有,除非手动写了有参构造却没写无参)和属性的setter方法(容器通过反射调用setter赋值)。
3.2.1 基本类型 + 引用类型注入
首先编写我们的User
和Address
类代码:
java
// Address类(被依赖方)
public class Address {
private String cityName;
private int code;
// setter方法(必须有,否则容器无法赋值)
public void setCityName(String cityName) {this.cityName = cityName;}
public void setCode(int code) {this.code = code;}
@Override
public String toString() {
return "Address{" + "cityName='" + cityName + '\'' + ", code=" + code + '}';
}
}
// User类(依赖方)
public class User {
private String name; // 基本类型(String)
private Address address; // 引用类型(Address)
// setter方法(必须有)
public void setName(String name) {this.name = name;}
public void setAddress(Address address) {this.address = address;}
@Override
public String toString() {
return "User{" + "address=" + address + ", name='" + name + '\'' + '}';
}
}
对应的applicationContext.xml
配置:
xml
<?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">
<!-- 1. 先配置被依赖方Bean:Address(User依赖Address,必须先创建) -->
<bean id="address" class="org.example.pojo.Address">
<!-- 基本类型注入:用value(String、int等) -->
<property name="cityName" value="NX"></property> <!-- name对应Address的属性名 -->
<property name="code" value="10011"></property>
</bean>
<!-- 2. 配置依赖方Bean:User -->
<bean id="user" class="org.example.pojo.User">
<!-- 基本类型注入(String):用value -->
<property name="name" value="Bob"></property>
<!-- 引用类型注入(Address):用ref(指向被依赖Bean的id) -->
<property name="address" ref="address"></property> <!-- 关键:ref=被依赖Bean的id -->
</bean>
</beans>
3.2.2 测试setter注入(用你的test1()方法)
测试代码:
java
@Test
public void test1() {
// 加载配置文件,创建容器
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 从容器获取User Bean
User user = (User) applicationContext.getBean("user");
// 输出结果,验证注入
System.out.println(user);
}
预期结果 :
- 结果说明:
- 基本类型
name
被注入为"Bob"
; - 引用类型
address
被注入为Address{cityName='NX', code=10011}
; books
和hobbies
因未配置,暂时为null
。
- 基本类型
3.2.3 复杂类型注入:数组(books)、List(hobbies)
我们在User
类中加入数组(String[] books
)和List(List<String> hobbies
),Spring也支持这类复杂类型的注入,只需在配置中使用<array>
和<list>
标签。
java
public class User {
private String name;
private Address address;
private String[] books;
private List<String> hobbies;
public void setAddress(Address address) {
this.address = address;
}
public void setBooks(String[] books) {
this.books = books;
}
public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"address=" + address +
", name='" + name + '\'' +
", books=" + Arrays.toString(books) +
", hobbies=" + hobbies +
'}';
}
}
补充后的User Bean配置:
xml
<bean id="user" class="org.example.pojo.User">
<property name="name" value="Bob"></property>
<property name="address" ref="address"></property>
<!-- 1. 数组类型注入:用<array>标签,子标签<value>写数组元素 -->
<property name="books">
<array>
<value>《Spring实战》</value>
<value>《Java编程思想》</value>
<value>《设计模式》</value>
</array>
</property>
<!-- 2. List类型注入:用<list>标签,子标签<value>写List元素(与数组类似) -->
<property name="hobbies">
<list>
<value>打篮球</value>
<value>写代码</value>
<value>看电影</value>
</list>
</property>
</bean>

再次执行test1(),预期结果:

3.3 构造器注入 vs setter注入:怎么选?
两种注入方式各有适用场景,开发中需根据属性的"必填性"选择:
对比维度 | 构造器注入 | setter注入 |
---|---|---|
注入时机 | Bean创建时(有参构造器调用) | Bean创建后(无参构造器+setter调用) |
适用场景 | 必填属性(如Student的name/age) | 可选属性(如User的books/hobbies) |
属性安全性 | 确保Bean创建时属性已赋值(无null) | 可能存在属性未赋值(null) |
灵活性 | 创建后无法修改依赖(构造器只调用一次) | 创建后可通过setter修改依赖 |
推荐原则:
- 必填属性用构造器注入:避免Bean创建后因缺少依赖导致空指针;
- 可选属性用setter注入:灵活配置,后续可动态修改;
- Spring官方推荐构造器注入(从Spring 4.3+开始,支持无XML的构造器注入,后续讲注解时会提)。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482
|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |
