Spring IoC 与 DI 深度剖析:从“控制反转”到 Bean 的集中管理


---知识点专栏---


📚 目录


1. 什么是 Spring?IoC 与 DI 概览

Spring 框架是一个轻量级、一站式、模块化 的开源框架,旨在简化企业级应用程序开发。它是一个包含众多工具方法的 IoC 容器,支持广泛的应用场景,并具有活跃而庞大的社区。

Spring 的主要功能包括:管理对象 及其之间的依赖关系面向切面编程 (AOP)数据库事务管理数据访问 以及Web 框架支持等。

1.1 Spring、Spring MVC 与 Spring Boot 的关系

为了更好地理解 Spring 生态系统,我们首先要明确三者之间的关系:

框架名称 定位/核心功能 与其他框架关系
Spring 核心框架,提供 IoC 和 AOP 等基础功能。 基础和核心,其他两个框架都建立在 Spring 之上。
Spring MVC Spring 的子框架,用于构建 Web 应用和网络接口 基于 Spring 开发的 Web 框架,提供 URL 映射和视图集成等功能。
Spring Boot 一套快速开发整合包,是对 Spring 的封装和简化。 辅助简化项目开发,通过"约定大于配置"快速搭建 Spring 应用,例如通过引入 Spring MVC 框架来完成 Web 开发。

简单来说,Spring 是地基Spring MVC 是 Web 层的工具 ,而 Spring Boot 是帮你快速搭建和整合项目的一把"脚手架"

1.2 容器(Container)的概念

在技术领域,容器本质上是用来容纳某种物品的基本装置。在编程中,我们常见的容器包括:

  • 数据存储容器 :如 ListMap
  • Web 容器:如 Tomcat,用于运行 Web 应用。
  • IoC 容器:即 Spring 容器,用于管理应用中的对象(Bean)。

1.3 控制反转(IoC):核心思想的转变

IoCInversion of Control 的缩写,中文即控制反转。它是 Spring 的核心思想。

在传统的开发模式中,当一个对象需要依赖另一个对象时,开发者通常需要手动通过 new 关键字来创建 这个依赖对象。而在 IoC 模式下,获得依赖对象的过程被反转了 。开发者不再需要自己创建对象,而是将创建对象的任务交给 IoC 容器来完成。

💡 核心: 对象的创建和管理权从应用程序代码(使用方)反转到了 IoC 容器(第三方)。

1.4 依赖注入(DI):IoC 的具体实现

DIDependency Injection 的缩写,中文即依赖注入

**依赖注入(DI)**是指容器在程序运行期间,动态地为应用程序提供其运行时所依赖的资源(即对象)的过程。

  • IoC 是一种思想和目标
  • DI 是 IoC 的一种实现方式和落地方案

从应用程序的角度来看,通过引入 IoC 容器,并利用依赖关系注入的方式,最终实现了对象之间的解耦。


2. IoC 思想:从高耦合到低耦合的演进

通过一个"造车"的案例,我们可以直观地理解 IoC 解决的耦合度过高的问题。

2.1 传统程序开发:高耦合的困境

在传统的程序设计中,依赖关系是自上而下的:汽车(Car)依赖车身(Framework),车身依赖底盘(Bottom),底盘依赖轮胎(Tire)

在代码实现中,上层类会主动创建并控制下层依赖对象。例如,Car 类在其构造函数中会主动 new Framework()Frameworknew Bottom(),以此类推:

java 复制代码
// 传统模式下,Car内部主动创建依赖对象Framework
static class Car {
    private Framework framework;
    public Car() {
        framework = new Framework(); // ⚠️ 耦合点1:Car主动创建Framework
        System.out.println("Car init....");
    }
    // ...
}

// Framework 内部主动创建依赖对象Bottom
static class Framework {
    private Bottom bottom;
    public Framework() {
        bottom = new Bottom(); // ⚠️ 耦合点2:Framework主动创建Bottom
        System.out.println("Framework init...");
    }
    // ...
}

// ...以此类推,当Tire类构造函数发生变更时...

问题分析

如果需求变动,例如需要为 Tire 类添加一个尺寸参数,那么 Tire 构造函数的变化会沿着整个调用链向上影响到 BottomFramework,最终导致 Car 类的代码也必须修改。这导致程序的耦合度非常高

2.2 IoC 改造:实现解耦与控制权反转

为了解决高耦合问题,我们采用依赖注入的方式,将原来由类自身创建下级类,改为通过**传递(注入)**的方式获取。

graph TD D[轮胎(Tire)] -->|依赖| C[底盘(Bottom)] C -->|依赖| B[车身(Framework)] B -->|依赖| A[汽车(Car)]

在 IoC 模式中,对象之间的依赖关系是反向的(控制权反转 ),对象的创建顺序也颠倒了:Tire → \to → Bottom → \to → Framework → \to → Car

以下是基于构造方法注入的 IoC 改造示例代码:

java 复制代码
// IoC 改造后的 Car 类:不再主动创建 Framework,而是通过构造方法接收
static class Car {
    private Framework framework;

    // 依赖对象(Framework)由外部注入
    public Car(Framework framework) {
        this.framework = framework; // ✅ 注入点
        System.out.println("Car init....");
    }
    // ...
}

// IoC 改造后的 Framework 类
static class Framework {
    private Bottom bottom;

    // 依赖对象(Bottom)由外部注入
    public Framework(Bottom bottom) {
        this.bottom = bottom; // ✅ 注入点
        System.out.println("Framework init...");
    }
    // ...
}
// ...
// 对象的创建和组装工作集中在"IoC容器"中完成
public class IocCarExample {
    public static void main(String[] args) {
        Tire tire = new Tire(20);
        Bottom bottom = new Bottom(tire);
        Framework framework = new Framework(bottom);
        Car car = new Car(framework);
        car.run();
    }
}

可以看到,CarFramework 等类本身不再关心如何创建其依赖对象,它们只需要通过构造函数接收即可。即使 Tire 类构造函数变化,也只需要修改最顶层的调用程序(main 方法中的组装逻辑),底层的业务类代码不受影响

2.3 IoC 带来的优势总结

控制反转带来的优势主要体现在两个方面:

  1. 资源集中管理(低配置成本)
    • 资源(对象)不由使用双方管理,而是由**第三方(IoC 容器)**统一管理。
    • 实现了资源的可配置和易管理,开发者可以专注于业务逻辑,无需关注实例创建的细节。
  2. 降低耦合度(高可维护性)
    • 降低了使用资源双方的依赖程度。
    • 依赖类发生任何改变,当前使用方类都不受影响,实现了灵活、通用的程序设计。

3. Spring Bean 的集中存储(IoC)

Spring IoC 容器管理的对象被称为 Bean。要将一个对象交给 Spring 容器管理,我们需要使用特定的注解来声明它。

Spring 提供了两类注解用于 Bean 的存储:

  1. 类注解@Controller@Service@Repository@Component@Configuration
  2. 方法注解@Bean

3.1 类注解:五大"组件存储"注解

Spring 提供了五种用于将类标记为 Bean 的注解,它们都属于 @Component 的衍生注解 。使用不同的注解是为了实现应用分层,让开发者看到注解就能直观了解类的用途。

注解名称 对应应用层 职责描述 示例类名
@Controller 控制层 (Web/API) 接收请求、处理请求并进行响应。 UserController
@Service 业务逻辑层 处理具体的业务逻辑,编排和调用 Dao/Repository 层。 UserService
@Repository 数据访问层 (持久层) 负责数据访问操作,与数据库交互。 UserRepository
@Configuration 配置层 处理项目中的配置信息,通常与 @Bean 配合使用。 AppConfig
@Component 通用组件 泛指任何不属于上述层级的通用组件或工具类。 ToolComponent
3.1.1 @Controller:控制层

用于标注处理 HTTP 请求的控制器类。

java 复制代码
@Controller // 将对象存储到 Spring 中
public class UserController {
    public void sayHi() {
        System.out.println("hi, UserController...");
    }
}
3.1.2 @Service:业务逻辑层

用于标注业务逻辑处理类。

java 复制代码
@Service
public class UserService {
    public void sayHi (String name) {
        System.out.println("Hi," + name);
    }
}
3.1.3 @Repository:数据访问层/持久层

用于标注数据访问或持久化操作的类。

java 复制代码
@Repository
public class UserRepository {
    public void sayHi() {
        System.out.println("Hi, UserRepository~");
    }
}
3.1.4 @Configuration:配置层

用于标注配置类,通常包含 @Bean 方法用于创建 Bean。

java 复制代码
@Configuration
public class UserConfiguration {
    public void sayHi() {
        System.out.println("Hi, UserConfiguration~");
    }
}
3.1.5 @Component:通用组件

一个通用注解,是所有组件存储注解的"父类"。

java 复制代码
@Component
public class UserComponent {
    public void sayHi() {
        System.out.println("Hi, UserComponent~");
    }
}

3.2 类注解的本质:@Component 的衍生

查看 @Controller@Service@Repository 等注解的源码,我们会发现它们内部都包含了 @Component 注解。

因此,@Component 被称为元注解 ,而 @Controller@Service@Repository 等是它的衍生注解 。它们在功能上等价于 @Component,但提供了更明确的语义,便于应用分层和工具处理。在实际开发中,我们应尽量使用语义更明确的衍生注解,如在业务逻辑层使用 @Service,而非 @Component

3.3 方法注解:@Bean 的灵活应用

@Bean 注解是添加到方法上的,用于将方法的返回值对象注册为 Spring Bean。它主要用于解决以下问题:

  1. 外部包的类:无法修改源码,无法添加类注解。
  2. 一个类需要定义多个对象:例如配置多个数据源实例。

注意: @Bean 方法必须定义在一个被 @Component@Configuration(本身也包含 @Component)注解标记的类中才能生效。

java 复制代码
// BeanConfig 必须被组件注解标记
@Component
public class BeanConfig {
    @Bean // 将方法的返回值对象注册为 Bean
    public User user() {
        User user = new User();
        user.setName("zhangsan");
        user.setAge(18);
        return user;
    }
}
3.3.1 Bean 的默认命名规则

在使用 @Bean 标记方法时,如果没有显式指定名称,Spring 会默认将方法名作为 Bean 的名称(BeanId)。

java 复制代码
// Bean 名称默认为 "user1"
@Bean
public User user1() { /* ... */ }

// Bean 名称默认为 "user2"
@Bean
public User user2() { /* ... */ }
3.3.2 自定义 Bean 名称

可以通过设置 @Bean 注解的 name 属性或直接作为参数,为 Bean 对象进行重命名,并且可以指定多个别名。

java 复制代码
// 使用 name 属性指定多个别名
@Bean(name = {"u1", "user1Alias"})
public User user1() { /* ... */ }

// name={} 可以省略,直接传入字符串数组
@Bean({"u2", "user2Alias"}) // Bean 名称/别名为 "u2" 和 "user2Alias"
public User user2() { /* ... */ }

// 只有一个名称时,{} 也可以省略
@Bean("u3") // Bean 名称为 "u3"
public User user3() { /* ... */ }

3.4 Bean 扫描机制与 @SpringBootApplication

使用五大类注解声明的 Bean,想要生效,必须被 Spring 扫描到

Spring 通过 @ComponentScan 注解来配置扫描路径。

java 复制代码
@ComponentScan({"com.example.demo.controller", "com.example.demo.service"})
@SpringBootApplication
// ...

在 Spring Boot 应用中,通常无需手动添加 @ComponentScan,这是因为启动类上的 @SpringBootApplication 注解已经默认包含了 @ComponentScan

默认扫描范围 :Spring Boot 启动类 (@SpringBootApplication 所在的类) 所在包及其所有子包

推荐做法 :将 Spring Boot 启动类 (SpringIocDemoApplication) 放置在项目根包下(例如 com.example.demo),这样它就能默认扫描到所有的子包(如 controllerservicerepository 等)中定义的 Bean。


4. 依赖注入(DI)的实现方式与进阶

依赖注入是 IoC 容器在创建 Bean 时,为其提供运行时所依赖的资源(对象)的过程。Spring 提供了三种主要的依赖注入方式:

  1. 属性注入 (Field Injection)
  2. 构造方法注入 (Constructor Injection)
  3. Setter 注入 (Setter Injection)

所有注入方式主要通过 @Autowired 注解实现。

4.1 属性注入(Field Injection)

在类的成员属性上使用 @Autowired 注解。这种方式最简洁、方便。

java 复制代码
@Controller
public class UserController {
    // 注入方法1: 属性注入
    @Autowired 
    private UserService userService; //

    public void sayHi () {
        System.out.println("hi, UserController...");
        userService.sayHi(); // 如果没有 @Autowired,这里会抛出 NullPointerException
    }
}

4.2 构造方法注入(Constructor Injection) 【Spring 推荐】

在类的构造方法上使用 @Autowired 注解。如果类中只有一个构造方法,@Autowired 可以省略

java 复制代码
@Controller
public class UserController2 {
    private final UserService userService; // 推荐使用 final 修饰

    @Autowired // 如果是唯一的构造方法,@Autowired 可省略
    public UserController2(UserService userService) {
        this.userService = userService;
    }
    
    // ...
}

4.3 Setter 注入(Setter Injection)

在类的 Setter 方法上使用 @Autowired 注解。

java 复制代码
@Controller
public class UserController3 {
    private UserService userService;

    @Autowired // 在 Setter 方法上添加 @Autowired
    public void setUserService (UserService userService) {
        this.userService = userService;
    }
    
    // ...
}

4.4 三种注入方式的优缺点对比

注入方式 优点 缺点 推荐度
属性注入 简洁、使用方便 1. 只能用于 IoC 容器,耦合度较高。 2. 无法注入 final 属性。 3. 只有在使用时才会抛出空指针异常 (NPE)。 不推荐
构造方法注入 1. 可以注入 final 修饰的属性。 2. 依赖对象在使用前一定被完全初始化。 3. 注入的对象不会被修改(天然不可变)。 4. 通用性好,JDK支持。 注入多个对象时,代码会比较繁琐。 Spring 4.X/5.X 推荐
Setter 注入 方便在类实例之后,重新对该对象进行配置或注入。 1. 不能注入 final 属性。 2. 注入对象可能会被修改(setter 可被多次调用)。 Spring 3.X 推荐

最佳实践: 构造方法注入是 Spring 官方推荐的依赖注入方式,因为它保证了依赖的不可变性(final)和非空性(依赖必须在构造时传入)。


5. 解决多 Bean 冲突:@Autowired@Qualifier@Resource

当 Spring 容器中存在多个相同类型 的 Bean 时,如果仅使用 @Autowired 按类型注入,程序会因无法确定注入哪个 Bean 而报错 NoUniqueBeanDefinitionException(非唯一 Bean 对象)。

5.1 @Autowired 装配顺序

为了解决多 Bean 冲突,Spring 提供了明确的装配顺序:

  1. 按类型查找 Bean
    • 如果只找到一个,则自动装配。
    • 如果找到多个,则进入下一步。
  2. 是否配置 @Qualifier@Primary
    • 如果配置了 @Qualifier,则Qualifier 参数查找 Bean
    • 如果没有配置 @Qualifier,则进入下一步。
  3. 按名称查找 Bean
    • 使用注入字段的名称作为 Bean 的名称进行查找。
  4. 最终结果
    • 如果找到一个,则自动装配。
    • 如果仍未找到或找到多个,则抛出异常

5.2 解决方案一:@Primary 指定默认 Bean

当存在多个相同类型的 Bean 时,在其中一个 Bean 的定义上加上 @Primary 注解,可以指定它为默认实现

java 复制代码
@Component
public class BeanConfig {
    @Primary // 指定该 bean 为默认 bean 的实现
    @Bean("u1")
    public User user1() { /* user1: zhangsan */ }

    @Bean
    public User user2() { /* user2: lisi */ }
}

@Controller
public class UserController {
    // 此时 @Autowired 会优先注入带有 @Primary 的 user1
    @Autowired 
    private User user; 
}

5.3 解决方案二:@Qualifier 按名称匹配

@Qualifier 注解用于指定当前要注入的 Bean 的名称,它必须配合 @Autowired 一起使用

java 复制代码
@Controller
public class UserController {
    @Qualifier("user2") // 明确指定注入名为 "user2" 的 Bean
    @Autowired
    private User user; // 将注入 user2
}

5.4 解决方案三:@Resource 按名称注入 【JDK标准】

@ResourceJDK 提供 的注解,默认是按照 Bean 的名称 (即 name 属性)进行注入。

java 复制代码
@Controller
public class UserController {
    // 通过 name 属性指定注入名为 "user2" 的 Bean
    @Resource(name = "user2")
    private User user; // 将注入 user2
}

5.5 面试题:@Autowired@Resource 的区别

特性 @Autowired @Resource
提供方 Spring 框架 JDK (Java 标准)
默认注入方式 **按类型(Type)**注入 **按名称(Name)**注入
名称查找 在按类型查找失败(多 Bean 冲突)后,会尝试按名称查找。 直接通过 name 属性或字段名查找 Bean。
配合注解 可配合 @Qualifier 实现按名称注入。 支持更多参数设置,例如 name 属性。

推荐使用: 在 Spring Boot/Spring Cloud 项目中,@Autowired 配合构造方法注入是首选;如果需要兼容 Java EE 或按名称明确指定依赖,可以使用 @Resource


总结与展望

本文对 Spring IoC 和 DI 进行了全面且深入的探讨:

  1. IoC(控制反转)是核心思想,将对象创建和管理的控制权从应用代码反转给 IoC 容器,实现了彻底的解耦
  2. **DI(依赖注入)**是实现 IoC 的具体手段,即容器在运行时为对象提供其依赖资源。
  3. Bean 存储 :我们使用 @Controller@Service@Repository@Configuration (均是 @Component 的衍生) 和 @Bean 来将对象注册到 Spring 容器中。
  4. 依赖注入方式:包括属性注入、构造方法注入(推荐)和 Setter 注入。
  5. 多 Bean 解决 :通过 @Primary@Qualifier(配合 @Autowired)或 @Resource 来解决多类型 Bean 注入冲突问题。

掌握 IoC 和 DI,是迈向 Spring 高级开发的第一步。它们是 Spring 框架的基石,也是设计高内聚、低耦合、可维护系统的关键。


相关推荐
南河的南1 小时前
解决IDEA无法下载Maven仓库的源码
java·maven·intellij-idea
无名-CODING1 小时前
#Servlet与Tomcat完全指南 - 从入门到精通(含面试题)
java·servlet·tomcat
想个名字太难1 小时前
ElasticSearch编程操作
java·elasticsearch·全文检索
小马爱打代码1 小时前
Spring AI:RAG 增强检索介绍
java·人工智能·spring
Franciz小测测1 小时前
Python APScheduler 定时任务 独立调度系统设计与实现
java·数据库·sql
天一生水water2 小时前
Eclipse数值模拟软件详细介绍(油藏开发的“工业级仿真引擎”)
java·数学建模·eclipse
谷粒.4 小时前
Cypress vs Playwright vs Selenium:现代Web自动化测试框架深度评测
java·前端·网络·人工智能·python·selenium·测试工具
uzong7 小时前
程序员从大厂回重庆工作一年
java·后端·面试
kyle~7 小时前
C++---value_type 解决泛型编程中的类型信息获取问题
java·开发语言·c++