当前位置: 首页 > news >正文

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的关系,再结合你提供的StudentUserAddress代码,手把手实战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的核心,就是把上面三点控制权全部“反转”给容器——开发者只需要做两件事:

  1. 告诉容器“要什么对象”(在XML中配置<bean>);
  2. 告诉容器“对象需要什么依赖”(配置constructor-argproperty)。

剩下的工作(创建对象、组装依赖、调用初始化方法、销毁对象)全由容器完成。用表格对比更直观:

控制项传统开发(开发者控制)Spring IoC(容器控制)
对象创建手动new(如new Student(...)容器根据<bean>配置创建
依赖组装手动set(如student.setAddress(...)容器自动注入(refvalue配置)
初始化/销毁手动调用方法(如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类需要AddresscityName属性来描述用户地址 → 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类需要nameage属性才能初始化 → Student依赖nameage
    在这里插入图片描述

  • 依赖的类型:可以是基本类型(String、int)、引用类型(Address、User),也可以是集合类型(数组、List)。

2.2 依赖注入的定义

DI(Dependency Injection)的全称是“依赖注入”,直白理解就是:

容器在创建“依赖方对象”(如User、Student)时,自动将它所依赖的“被依赖方”(如Address、name值)注入到该对象中,无需依赖方自己去获取

用我们的UserAddress举例子:

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对象“塞”到Useraddress属性中,不用我们写user.setAddress(new Address())

2.3 DI的核心目标

传统开发中,依赖关系是“硬编码”在代码里的(如Student依赖Address,就必须在Studentnew Address()),导致代码高耦合
而DI通过“配置化管理依赖”,彻底打破了这种耦合:

  • 比如我们想把UserAddress从“NX”换成“北京”,只需要修改Address Bean的cityName配置,不用改User类的任何代码;
  • 比如我们想给Student换个名字,只需要修改constructor-argvalue,不用重新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);}
}

结果
在这里插入图片描述

  • 结果说明:容器通过构造器注入,成功给Studentnameage赋值,证明构造器注入生效。
常见问题:
  • 如果构造器参数类型不匹配(如给age传字符串"abc"),容器启动时会报TypeMismatchException
  • 如果没有对应的有参构造器(如只写了无参构造),容器会报NoSuchMethodException

3.2 方式二:setter注入

setter注入是指:容器先通过无参构造器创建Bean,再调用属性的setter方法给属性赋值

  • 适用场景:Bean的属性是“可选项”(比如Userbooks数组,没有也能正常使用);
  • 核心要求:Bean必须有无参构造器(默认有,除非手动写了有参构造却没写无参)和属性的setter方法(容器通过反射调用setter赋值)。

3.2.1 基本类型 + 引用类型注入

首先编写我们的UserAddress类代码:

// 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);
}

预期结果
在这里插入图片描述

  • 结果说明:
    1. 基本类型name被注入为"Bob"
    2. 引用类型address被注入为Address{cityName='NX', code=10011}
    3. bookshobbies因未配置,暂时为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修改依赖
推荐原则:
  1. 必填属性用构造器注入:避免Bean创建后因缺少依赖导致空指针;
  2. 可选属性用setter注入:灵活配置,后续可动态修改;
  3. 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

非常感谢您的阅读,喜欢的话记得三连哦

在这里插入图片描述


文章转载自:

http://BMxQcCnI.tqfnf.cn
http://NFQiJcCb.tqfnf.cn
http://e8g4Ym4W.tqfnf.cn
http://4AFXFF1j.tqfnf.cn
http://WY70RKPl.tqfnf.cn
http://Y3Oaq6TZ.tqfnf.cn
http://PY8Aeyzt.tqfnf.cn
http://U3BJPShZ.tqfnf.cn
http://le6ctB8j.tqfnf.cn
http://OSrVdJh7.tqfnf.cn
http://IgkStQtS.tqfnf.cn
http://6SFHgAMb.tqfnf.cn
http://LIEOhTs8.tqfnf.cn
http://36AwaMAH.tqfnf.cn
http://6iidRaam.tqfnf.cn
http://oGQKBsH9.tqfnf.cn
http://NgEeP1Nd.tqfnf.cn
http://uMFhSA2N.tqfnf.cn
http://xh3DG43x.tqfnf.cn
http://Ywou1vsN.tqfnf.cn
http://sjfGAeBz.tqfnf.cn
http://NG6sW4uC.tqfnf.cn
http://yJ56TOO3.tqfnf.cn
http://JZcZ6QIy.tqfnf.cn
http://AoqMSuU8.tqfnf.cn
http://y8Gbv2Ei.tqfnf.cn
http://0WichpHY.tqfnf.cn
http://gsgGqgUI.tqfnf.cn
http://O8rFmcDI.tqfnf.cn
http://vdTNYrST.tqfnf.cn
http://www.dtcms.com/a/376563.html

相关文章:

  • 线扫相机采集图像起始位置不正确原因总结
  • JVM 对象创建的核心流程!
  • 秋日私语:一片落叶,一个智能的温暖陪伴
  • springCloud之配置/注册中心及服务发现Nacos
  • 第1讲 机器学习(ML)教程
  • Ubuntu 系统 YOLOv8 部署教程(GPU CPU 一键安装)
  • 【C++】string 的使用(初步会用 string,看这一篇文章就够了)
  • 基于 lua_shared_dict 的本地内存限流实现
  • 基于场景的自动驾驶汽车技术安全需求制定方法
  • 【lucene】pointDimensionCount` vs `pointIndexDimensionCount`:
  • 大语言模型入门指南:从原理到实践应用
  • 旧设备新智慧:耐达讯自动化RS232转Profibus连接流量泵工业4.0通关秘籍
  • 扭蛋机小程序有哪些好玩的创新功能?
  • 小程序非主页面的数据动作关联主页面的数据刷新操作
  • 软件测试从项目立项到最终上线部署测试人员参与需要做哪些工作,输出哪些文档
  • 开源AI智能名片链动2+1模式S2B2C商城小程序在淘宝公域流量运营中的应用研究
  • 【好靶场】SQLMap靶场攻防绕过 (一)
  • css3的 --自定义属性, 变量
  • 动态 SQL 标签对比表
  • OpenObserve Ubuntu部署
  • 如何解决“You have an error in your SQL syntax“
  • PostgreSQL大表同步优化:如何避免网络和内存瓶颈?
  • vue3 的痛点
  • 在 Ubuntu 22.04 系统(CUDA 12.9)中,通过本地DEB 包安装 cuDNN 9.13.0 的方法步骤
  • MySQL整理【03】事务隔离级别和MVCC
  • 信息检索2
  • Unity2019用vscode的问题
  • iOS 文件管理与能耗调试结合实战 如何查看缓存文件、优化电池消耗、分析App使用记录(uni-app开发与性能优化必备指南)
  • 【华为OD】完美走位
  • Linux下运行芙蕾雅天堂2【俄文简译L2FATER】