Java-Spring入门指南(四)深入IOC本质与依赖注入(DI)实战

Java-Spring入门指南(四)深入IOC本质与依赖注入(DI)实战


前言

在上一篇博客中,我们剖析了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的传统开发中,开发者要全权掌控对象的生命周期,举个例子:

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的核心,就是把上面三点控制权全部"反转"给容器------开发者只需要做两件事:

  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
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类需要nameage属性才能初始化 → Student依赖nameage

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

2.2 依赖注入的定义

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

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

用我们的UserAddress举例子:

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对象"塞"到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类代码:

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

结果

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

3.2 方式二:setter注入

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

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

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

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

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

预期结果

  • 结果说明:
    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>标签。

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修改依赖
推荐原则:
  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

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

相关推荐
A 风2 小时前
封装日期选择器组件,带有上周,下周按钮
开发语言·javascript·vue.js
索迪迈科技2 小时前
C语言 memcpy 的使用
c语言·开发语言
yuyousheng3 小时前
C语言中sizeof和strlen的区别
c语言·开发语言·哈希算法
Mr_sun.3 小时前
Day04_苍穹外卖——套餐管理(实战)
开发语言·python
南棱笑笑生3 小时前
20250910在荣品RD-RK3588-MID开发板的Android13系统下修改短按power按键的休眠/唤醒为关闭/打开背光
开发语言·python·rockchip
练习时长一年3 小时前
自定义事件发布器
java·前端·数据库
nightunderblackcat3 小时前
新手向:实现验证码程序
java·spring boot·spring·java-ee·kafka·maven·intellij-idea
悠悠~飘3 小时前
php学习(第二天)
开发语言·学习·php
oioihoii3 小时前
构造函数和析构函数中的多态陷阱:C++的隐秘角落
java·开发语言·c++