← 返回题目列表

Spring IoC 和依赖注入是什么?

高频 简单 第 3 / 30 题 更新于 2026/07/26
SpringIoCDI

简化版

IoC(控制反转) 是一种设计思想:把「创建对象、管理依赖」的控制权从你自己手里,反转交给 Spring 容器。DI(依赖注入) 是 IoC 的具体实现方式:容器把一个对象需要的依赖「注入」给它,而不是让它自己 new。两者的关系:IoC 是思想,DI 是手段。

详细版

传统写法——对象自己创建依赖,紧耦合:

class OrderService {
    private UserService userService = new UserServiceImpl();  // 自己 new,写死了实现
}

OrderServiceUserServiceImpl 死死绑在一起,想换实现、想测试都得改代码。

IoC 写法——只声明「我需要什么」,剩下的交给容器:

@Service
class OrderService {
    private final UserService userService;
    public OrderService(UserService userService) {   // 构造器注入,容器负责传进来
        this.userService = userService;
    }
}

OrderService 不再关心 UserService 从哪来、是哪个实现——容器读到注解,负责实例化 Bean、装配依赖、管理生命周期,把合适的实现注入进来。

三种注入方式

  • 构造器注入(推荐):依赖 final、对象不可变、必需依赖一目了然、便于测试;
  • Setter 注入:适合可选依赖;
  • 字段注入@Autowired 直接标字段):写着最省事,但不利于测试、易掩盖过多依赖,不推荐

完整版教学

一、「控制反转」到底反转了什么

「控制」指的是对象生命周期和依赖关系的控制权。传统程序里,这个控制权在你手上:你决定何时 new、new 哪个实现、谁依赖谁。反转就是把这个控制权交出去、交给容器。

打个比方:以前是你自己去菜市场买菜做饭(主动获取依赖),IoC 之后是你只管说「我要一份番茄炒蛋」,后厨(容器)把做好的端给你(被动接受依赖)。你不再关心食材从哪来、怎么处理——这就是控制权的反转,也叫「好莱坞原则」:Don’t call us, we’ll call you。

二、IoC 带来的真正好处:解耦

IoC 的核心价值是解耦。对象之间不再直接依赖具体实现,而是依赖抽象(接口),由容器在运行时装配。好处很实在:

  • 易替换:换个 UserService 实现,业务代码一行不改,改配置即可;
  • 易测试:测试时注入 Mock 依赖,不用真起数据库;
  • 易维护:依赖关系集中由容器管理,不散落在各处的 new 里。

这正是「面向接口编程」能落地的基础。Spring 会根据 BeanDefinition 和注入点元数据解析依赖,反射、构造器调用或工厂方法只是创建与赋值时可能采用的技术手段,不能把 IoC 简化成“反射”。

三、为什么强烈推荐构造器注入

对于必需依赖,Spring 官方文档倡导使用构造器注入,原因有四:

  1. 保证依赖不为 null:对象一旦创建,依赖就齐了,不会出现「用的时候还没注入」的空指针;
  2. 可用 final:依赖引用创建后不能重新赋值,减少对象运行中被替换的可能;但 final 不等于依赖对象自身线程安全;
  3. 依赖显式化:构造器参数一长串,你会警觉「这个类依赖太多了,该拆」——字段注入会掩盖这个坏味道;
  4. 利于测试:脱离 Spring 容器也能 new OrderService(mockUserService) 直接单测。

字段注入(@Autowired 标在字段上)虽然代码短,但没法用 final、脱离容器无法实例化、还容易让一个类塞进十几个依赖而不自知。

四、循环依赖顺带一提

A 依赖 B、B 又依赖 A,就是循环依赖。Spring 对部分单例 + Setter/字段注入的循环依赖可通过提前暴露引用处理;但构造器注入的循环依赖通常解决不了(对象还没创建就需要对方),容器会检测创建环并启动报错。不过话说回来,出现循环依赖本身通常是设计有问题的信号,应该考虑重构而非依赖 Spring 的补救。

五、容器怎样把“声明”变成对象图

Spring 先把组件扫描、@Bean 或 XML 等配置解析成 BeanDefinition,再由 BeanFactory 根据这些元数据选择构造器、实例化对象、解析依赖并执行生命周期扩展点。反射是常用实现手段之一,但不是 IoC 的定义;工厂方法、生成代理、缓存 singleton 和后处理器协作同样属于完整创建过程。

@Configuration
class AppConfig {
    @Bean
    MessageSender messageSender() {
        return new SmsSender();
    }
}

这个工厂方法同样把对象注册给容器,并不要求组件类必须带 @Component。消费方只声明 MessageSender 依赖,具体对象来自扫描、工厂方法还是其他 BeanDefinition 来源,不应泄漏进业务逻辑。

配置元数据
   ↓ 解析
BeanDefinition 集合
   ↓ 选择构造器 / 工厂方法
实例化 Bean
   ↓ 按类型、限定符等解析依赖
形成对象依赖图
   ↓ 初始化与后处理
对外暴露可用 Bean

假设订单服务原先在 5 个位置分别 new SmsSender(),替换成邮件实现至少要改 5 处;改成注入 MessageSender 后,业务类保持不变,只需让容器选择另一实现。这个数字例子说明解耦不是“少写 new”本身,而是让依赖选择从消费方代码中移出。

六、三种注入方式的边界

注入方式适合的依赖优点主要问题
构造器必需依赖创建即完整、可 final、易测试参数过多会暴露类职责过重
Setter真正可选或可重配依赖可读地表达后设属性对象可能经历未完整状态
字段少量框架遗留场景代码短隐藏依赖、难脱离容器测试、不能 final

构造器参数达到 8 个时,不应把“构造器太长”当成改用字段注入的理由。长参数列表是设计反馈,提示这个类可能同时承担下单、库存、支付、通知等过多职责;隐藏参数只会遮住问题,不会减少耦合。

记忆钩子:IoC 反转的是创建与装配的控制权,DI 描述依赖怎样交到对象手中;判断注入方式时,先问依赖是不是对象成立的必要条件。

七、常见误区与追问

  • 误区:IoC 和 DI 是完全相同的概念。 IoC 是更宽的控制反转原则,DI 是 Spring 主要采用的实现方式;依赖查找也能实现控制权转移,但会让业务代码主动依赖容器。
  • 误区:IoC 的本质就是反射。 反射只是实例化和成员访问的技术手段之一,核心是对象创建、依赖选择与生命周期由容器协调。
  • 误区:字段注入代码最少,所以是最佳实践。 它隐藏必需依赖并削弱不可变性和可测试性,业务 Bean 通常优先构造器注入。
  • 追问:DI 为什么能降低耦合? 消费者只依赖契约,具体实现的选择被移动到组合根或容器配置,替换实现时不必修改业务类。
  • 追问:构造器参数很多怎么办? 先检查单一职责并拆分协作对象,不要仅为缩短构造器改成字段注入。
  • 追问:Spring 容器里还能使用 new 吗? 可以创建值对象或局部实现细节;问题在于业务组件自行 new 外部协作者,绕开统一配置、代理和生命周期管理。

八、加强记忆

IoC 是「把对象创建和依赖管理的控制权反转给容器」的设计思想,目的是解耦;DI 是它的实现方式——容器把依赖注入进来而非对象自己 new。三种注入里优先构造器注入(依赖非空、可 final、显式、好测试)。