Java-Spring入门指南(四)深入IOC本质与依赖注入(DI)实战
Java-Spring入门指南(四)深入IOC本质与依赖注入(DI)实战
- 前言
- 一、IOC的本质
- 1.1 传统开发的控制权是什么?
- 1.2 IOC的本质
- 1.3 IOC是“思想”,DI是“实现”
- 二、什么是依赖注入(DI)?
- 2.1 什么是“依赖”?
- 2.2 依赖注入的定义
- 2.3 DI的核心目标
- 三、Spring依赖注入实战
- 3.1 方式一:构造器注入
- 3.1.1 三种构造器注入配置
- 方式1:根据“构造器参数名”注入(最直观)
- 方式2:根据“构造器参数类型”注入(谨慎使用)
- 方式3:根据“构造器参数下标”注入(最稳定)
- 3.1.2 测试构造器注入
- 常见问题:
- 3.2 方式二:setter注入
- 3.2.1 基本类型 + 引用类型注入
- 3.2.2 测试setter注入(用你的test1()方法)
- 3.2.3 复杂类型注入:数组(books)、List(hobbies)
- 补充后的User Bean配置:
- 再次执行test1(),预期结果:
- 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的传统开发中,开发者要全权掌控对象的生命周期,举个例子:
// 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
;
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
举例子:
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
类代码:
public class Student {private String name;private int age;// 无参构造(setter注入需要,构造器注入也建议保留)public Student() {}// 有参构造(构造器注入的核心)public Student(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Student{" + "age=" + age + ", name='" + name + '\'' + '}';}
}
对应的beans.xml
配置(三种注入方式):
方式1:根据“构造器参数名”注入(最直观)
通过name
属性指定构造器的参数名,直接匹配赋值:
<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
属性指定参数类型,避免参数名不匹配的问题:
<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开始),这是你代码中使用的方式:
<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类
public class MyTest {@Testpublic 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
类代码:
// 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;}@Overridepublic 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;}@Overridepublic String toString() {return "User{" + "address=" + address + ", name='" + name + '\'' + '}';}
}
对应的applicationContext.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()方法)
测试代码:
@Test
public void test1() {// 加载配置文件,创建容器ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");// 从容器获取User BeanUser 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>
标签。
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;}@Overridepublic String toString() {return "User{" +"address=" + address +", name='" + name + '\'' +", books=" + Arrays.toString(books) +", hobbies=" + hobbies +'}';}
}
补充后的User Bean配置:
<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
非常感谢您的阅读,喜欢的话记得三连哦 |