☰
Spring IoC与DI核心机制全解析:容器、Bean与面试考点
2026/10/14 7:14:48 网站建设 项目流程

很多人学 Spring 遇到的第一道坎,就是 IoC 和 DI。这两个缩写看着高深,其实拆开之后就是“把对象的创建和依赖关系交给容器管”。这篇文章我不绕弯子,直接从概念讲到实战,再把面试里最刁钻的几道相关题目拆开揉碎,全程用大白话加可运行的代码。内容偏 Java EE 进阶,适合已经写过一段时间 Java、但还没系统梳理过 Spring 核心机制的同学。

我不打算按教科书的方式讲,毕竟 IoC 和 DI 的官方定义到处都是。我更想说的是:它们到底在解决什么问题,代码里应该怎么写,写完之后遇到报错怎么办。把这些都过一遍,你对 Spring 的理解就不只是停留在“会用”的层面了。

1. 控制反转:把“谁创建对象”这件事反过来了

1.1 没有 IoC 的时候,我们的代码长什么样

先回看一下不用 Spring 的日子。假设你要做一个消息打印功能,最直觉的写法是这样:

public class MessageService { public String getMessage() { return "Hello, world!"; } } public class MessagePrinter { private MessageService service = new MessageService(); public void print() { System.out.println(service.getMessage()); } }

这段代码在功能上没有任何问题。但它有一个很隐蔽的耦合点:MessagePrinter直接通过new创建了MessageService。如果哪一天你不再想用MessageService,而是要用EnglishMessageService,或者要给MessageService的构造器传入配置参数,你就得打开MessagePrinter的源码去改。依赖越多,改动范围越大。

我刚开始写代码的时候觉得“new 一个对象不是很正常吗?”直到有一次需要给一个类做单元测试,那个类里 new 了一个查数据库的 DAO,导致测试环境必须连数据库。那时候才意识到,如果对象不是自己 new 的,而是外部统一给的,测试会轻松太多。

这种“需要什么就自己创建什么”的写法,本质上是对象自己控制依赖的获取。而 IoC 要做的事情,就是把这个控制权交给外部容器。

1.2 IoC 到底反转了什么

控制反转(Inversion of Control)的核心,是把“对象创建、属性赋值、生命周期管理”的控制权从业务代码手上转移到容器手上。用餐厅类比很合适:

  • 以前自己做饭:买菜、洗菜、切菜、炒菜、刷锅,全流程自己掌控。
  • 现在去餐厅:你只需要点菜,后厨帮你把菜做好端上来。

你的业务类就是食客,Spring 容器就是后厨。你不用关心MessageService是何时创建的、是单例还是每次新建,你只负责声明“我要一份 message 服务”,容器就把它送到你手里。

“反转”这个词听起来玄乎,其实指的是依赖获取方式的转变。传统模式里,对象主动去拿到依赖;IoC 模式下,对象被动等待容器注入依赖。控制权从对象手里移交给了容器,这就是“反转”。

1.3 DI 是 IoC 的具体实现

IoC 本身是一种设计思想,而依赖注入(Dependency Injection,DI)是当前业界最常用的一种实现方式。二者不是对等关系,很多面试官喜欢混着问,你要能分清:

  • IoC 是思想:把控制权交给容器。
  • DI 是做法:容器通过构造器、Setter、字段等方式,把依赖对象注入到目标类里。

Spring 的整个 IoC 容器底层做的就是两件事:管理 Bean 的创建,以及把 Bean 之间的依赖关系通过 DI 建立起来。所以你在 Spring 中经常听到的“依赖注入”,本质上就是 IoC 思想的落地。

2. 理解容器与 Bean:一切都是对象,但不是所有对象

2.1 Bean 到底是什么

在 Spring 的世界里,被容器创建、装配、管理的对象统称为 Bean。注意,并不是所有对象都会成为 Bean,只有交给 Spring 管理的那些对象才是。

常见的方式是通过注解注册:

@Service public class UserService { public String getUserName() { return "张三"; } }

容器启动时扫描到@Service,就会把这个类实例化为一个 Bean 放进容器。以后你在任何地方需要UserService,都不需要自己new,直接从容器取就行。

我见过不少初学者把 Bean 理解成“Spring 特有的对象”,这种说法不准确。Bean 就是普通的 Java 对象,只不过它的生命周期被 Spring 接管了。它一样有构造器、有字段、有方法,只是创建时机和注入过程由容器统一处理。

2.2 BeanFactory 和 ApplicationContext 怎么选

Spring 里有两个核心容器接口:

容器接口加载策略特点使用场景
BeanFactory延迟加载最基础,功能少嵌入式、资源受限环境
ApplicationContext预加载扩展了事件、国际化、AOP 等能力日常开发首选

BeanFactory是 IoC 容器的顶层接口,可以理解成“最小可用的容器”。而ApplicationContext在它之上做了大量扩展,比如启动时就把单例 Bean 创建好,方便你第一时间发现配置错误。

日常开发中没必要用BeanFactory,直接用ApplicationContext就好。启动方式有很多种,基于注解配置的现代写法是:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

拿到容器之后,可以用context.getBean(...)获取某个 Bean,也可以直接用@Autowired让 Spring 在运行时自动把依赖送进来。

2.3 Bean 的作用域与生命周期

Spring 中最常用的 Bean 作用域是singleton和prototype:

  • singleton:Spring 容器中同一个 Bean 定义只会产生一个实例,默认就是单例。
  • prototype:每次获取都会创建一个新实例。
@Service @Scope("prototype") public class OrderTask { // 每次 getBean 都会得到新对象 }

默认单例很节约资源,但要注意:单例意味着这个 Bean 会被多个线程共享,所以它内部如果有可变状态,就需要考虑线程安全。我自己写业务代码时,习惯让 Bean 只持有无状态服务,有状态的数据全部通过方法参数传递,这样能规避大量并发问题。

Bean 的生命周期大致是:实例化 → 属性填充 → 初始化回调 → 使用 → 销毁回调。如果你实现了InitializingBean或使用@PostConstruct,可以在初始化阶段做自定义处理;如果实现了DisposableBean或使用@PreDestroy,则可以在销毁前释放资源。

3. 依赖注入的三种姿势与选型心得

3.1 构造器注入、Setter 注入、字段注入

依赖注入的入口有三种常见方式,写代码时天天都在用:

// 字段注入 @Service public class UserService { @Autowired private UserDao userDao; } // Setter 注入 @Service public class UserService { private UserDao userDao; @Autowired public void setUserDao(UserDao userDao) { this.userDao = userDao; } } // 构造器注入 @Service public class UserService { private final UserDao userDao; @Autowired public UserService(UserDao userDao) { this.userDao = userDao; } }

字段注入写起来最简洁,但它有几个副作用:

  • 类被实例化后无法通过外部重新设置依赖,不利于写单元测试。
  • 依赖关系被隐藏,读代码时很难一眼看出来这个类需要什么。
  • 由于依赖是 final 还是非 final 不受控制,容易写出可变状态。

Setter 注入适合可选依赖,或者说这个依赖在运行过程中有可能被替换。但大多数业务 Bean 的依赖都是必须的,用 Setter 注入反而放宽了约束,漏注入时也不会第一时间暴露。

构造器注入是 Spring 官方推荐的方案。它保证了依赖的不可变,对象一旦创建,依赖就是确定的,也非常方便测试:直接new UserService(mockUserDao)就行。从 Spring 4.3 开始,如果类只有一个构造器,@Autowired都可以省略,代码会非常干净。

3.2 @Autowired 和 @Resource 的区别

两者都能用来注入依赖,但机制不同:

注解来源默认匹配方式可用的限定注解
@AutowiredSpring按类型@Qualifier、@Primary
@ResourceJSR-250按名称name属性

@Autowired是 Spring 原生注解,核心逻辑是:先按类型查找,如果同类型有多个 Bean,再配合@Qualifier按名称缩小范围。@Resource是 Java 标准注解,默认按字段名或属性名匹配名称,找不到再退回类型匹配。

我在项目中一般统一使用@Autowired加构造器注入。如果你维护的是老项目,里面用了@Resource,也不要急着改,它本身没有错,只需要保证注入时命名清晰。真正让人头疼的是两个同类 Bean 存在时的注入失败,这个后面我会专门列一节排查内容。

3.3 多个实现类怎么精准注入

接口多实现是 DI 最常见的场景。比如有一个PaymentGateway接口,下面有AlipayGateway、WechatGateway两个实现:

public interface PaymentGateway { void pay(BigDecimal amount); } @Component public class AlipayGateway implements PaymentGateway { public void pay(BigDecimal amount) { /* 支付宝逻辑 */ } } @Component public class WechatGateway implements PaymentGateway { public void pay(BigDecimal amount) { /* 微信逻辑 */ } }

如果直接在某个 Service 里写:

@Autowired private PaymentGateway gateway;

启动时肯定会报错,因为 Spring 不知道你要哪个实现。解决办法有两个:

@Autowired @Qualifier("wechatGateway") private PaymentGateway gateway;

或者更规范化地指定一个主实现:

@Component @Primary public class AlipayGateway implements PaymentGateway { // 默认注入这个 }

用@Qualifier可以精确到某个实现类,用@Primary可以指定默认实现。我个人的习惯是:默认实现用@Primary标记,特殊场景在构造器参数上显式加@Qualifier,这样多实现的时候代码意图非常清晰。

4. 手把手搭一个基于注解的 Spring 工程

4.1 最小依赖与启动入口

要跑通下面的例子,只需要一个 Maven 项目,加上spring-context依赖即可。不引入 Web 相关的 Spring Boot 全家桶,反而更容易看清 IoC 容器本身的工作流程。

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.31</version> </dependency> </dependencies>

然后写一个配置类:

@Configuration @ComponentScan("com.example") public class AppConfig { }

@Configuration表示这是一个配置类,@ComponentScan告诉 Spring 去哪里扫描带注解的类。这里的com.example要根据你自己的包名调整。

启动容器并获取 Bean:

public class Application { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class); System.out.println(userService.getUserName()); } }

这里就是 IoC 容器最直观的体现:UserService我们没有new,而是从容器里拿。容器在创建它的时候,发现它依赖UserDao,也会先创建好UserDao再注入进去。

提示:@ComponentScan的扫描路径写错了是新手最容易踩的坑。Spring 默认只扫描配置类同级及子包,如果 Bean 放在其他包,一定要在basePackages里明确指定。

4.2 @Component 家族与 @Bean 的分工

Spring 提供了一批注解,它们本质上都是@Component的具象化:

  • @Service:业务层
  • @Repository:数据访问层
  • @Controller:Web 层
  • @Component:通用组件

这些注解的作用是让容器自动注册 Bean。而@Bean不同,它通常写在配置类里,用于手动注册一个对象,特别是第三方库的实例。比如你不想在业务代码里给某个外部类加 Spring 注解,就用@Bean把它包装进来:

@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setUrl("jdbc:h2:mem:test"); dataSource.setUsername("sa"); dataSource.setPassword(""); return dataSource; } }

@Bean方法本身也可以接收参数,Spring 会把容器中的其他 Bean 注入到这个方法的参数里。比如:

@Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }

这里DataSource就是被容器自动注入的。有了这一套机制,使用任何外部库都能无缝接入 Spring。

4.3 配置文件读取与 @Value 注入

IoC 容器不仅能管理对象,还能管理配置值。推荐用@PropertySource加载外部的properties文件:

@Configuration @PropertySource("classpath:application.properties") public class AppConfig { @Value("${app.name}") private String appName; @Bean public AppInfo appInfo() { return new AppInfo(appName); } }

当然,如果你已经用了 Spring Boot,这些配置都会被自动处理。但理解底层机制仍然有必要,因为面试考察的是原理,运行环境再方便也不能替代底层认知。

5. 面试考点:把常见问题答到点子上

5.1 IoC 和 DI 的关系怎么答

这是最基础也是最容易答偏的问题。建议回答框架是:

  1. IoC 是一种设计思想,核心是反转控制权,让容器代替业务代码管理对象依赖。
  2. DI 是 IoC 的具体实现方式,通过构造器、Setter、字段注入等途径,把依赖对象传给目标类。
  3. Spring IoC 容器既承担了对象创建,也承担了依赖管理,因此谈论 Spring 时,IoC 和 DI 经常被放在一起说。

不要只背定义,最好能随手画出“以前 new 对象 vs 现在容器注入”的对比代码,面试官会认为你理解得更深入。

5.2 Spring 如何解决循环依赖

循环依赖也是高频面试题。场景很简单:

@Component public class A { @Autowired private B b; } @Component public class B { @Autowired private A a; }

Spring 默认对单例 Bean 的 Setter 注入循环依赖是能处理的,核心是三级缓存。我理解的三级缓存是这样的:

  • 一级缓存:存放已经完成初始化、可以直接使用的单例 Bean。
  • 二级缓存:存放早期暴露的 Bean,也就是属性还没填完的“半成品”。
  • 三级缓存:存放ObjectFactory,留下一个生成代理对象的入口。

A 创建时,先放入三级缓存,然后去填充属性 B。B 创建时发现需要 A,于是从三级缓存拿到 A 的早期引用,完成 B 的创建。B 放入一级缓存后,A 再继续填充 B,最终完成 A 的创建。

这里必须强调:构造器循环依赖是解决不了的,因为对象还没构造出来,无法提前暴露。另外,如果 A 和 B 都被设置成prototype作用域,也无法解决循环依赖,因为 Spring 不会缓存原型 Bean 的早期引用。

提示:面试时不需要背得一字不差,重点是说清楚“提前暴露半成品”这个核心思路,顺带说出三级缓存各自的作用。

5.3 @Autowired 的查找流程

@Autowired在面试里经常被追问到源码层。它的查找逻辑可以总结为四步:

  1. 先按照类型查找容器内所有对应类型的 Bean。
  2. 如果找到多个,会继续按照字段名或参数名匹配。
  3. 如果字段名也匹配不上,看是否有@Qualifier做精确指定。
  4. 以上都找不到,就会抛出NoUniqueBeanDefinitionException或NoSuchBeanDefinitionException。

所以当你看到“expected single matching bean but found 2”这类报错时,第一反应就是:容器里有两个相同类型的 Bean,需要进一步指定。

5.4 Bean 生命周期的完整链路

面试官如果让你讲 Bean 生命周期,你可以按照下面的顺序:

  • 推断构造器并实例化对象。
  • 进行属性填充,也就是依赖注入。
  • 处理各类型 Aware 回调,比如BeanNameAware、ApplicationContextAware。
  • 调用BeanPostProcessor的前置处理。
  • 执行@PostConstruct或InitializingBean.afterPropertiesSet()。
  • 调用BeanPostProcessor的后置处理。
  • Bean 正常就绪,等待使用。
  • 容器关闭时执行@PreDestroy或DisposableBean.destroy()。

这里面最重要的扩展点是BeanPostProcessor,Spring AOP、代理增强都是靠它实现的。面试提到这里,基本就能体现出源码阅读的深度。

6. 实战中的常见问题与排查技巧

6.1 NoSuchBeanDefinitionException

这个异常最常见的提示是:

No qualifying bean of type 'com.example.UserDao' available

原因一般有三个:

  • 类上没加@Component或其派生注解。
  • @ComponentScan扫描路径没覆盖到这个类所在的包。
  • Bean 类型确实不匹配,比如你注入了接口类型,但实现类没注册。

排查时最直接的办法是在启动类里打印所有 Bean 名称:

public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } }

这样一看就能发现,你的类有没有被容器扫描到。这个方法我几乎每次遇到“找不到 Bean”都会用,比反复看配置快得多。

6.2 UnsatisfiedDependencyException 与循环依赖

如果错误信息里出现The dependencies of some of the beans in the application context form a cycle,就是循环依赖了。虽然 Spring 能解决一部分 setter 注入的循环依赖,但工程上解法通常是重构。

我的建议是:不要把两个类设计成互相依赖。把公共逻辑抽到第三层,或者用@Lazy延迟其中一方的初始化,也能绕过问题,但只是兜底方案,不能治本。

6.3 多个同类型 Bean 的注入歧义

报错信息:

NoUniqueBeanDefinitionException: expected single matching bean but found 2

说明同一个接口有两个实现类都被 Spring 管理了。解决方案我在 3.3 里已经给过:@Primary设置默认实现,@Qualifier精确指定某一个。

还有一个更优雅的方式,是直接注入List<接口类型>,让 Spring 把所有实现类都给你:

@Autowired private List<PaymentGateway> gateways;

这在做策略模式时非常香,通过遍历列表挑选合适的实现,既避免了歧义,也方便后续扩展。

6.4 一个调试小技巧

除了打印 Bean 名称,还有一个隐藏技能:在 IDE 里开启 Spring 的日志级别到 DEBUG,能看到 Bean 创建和注入的完整过程。对于定位“到底是哪个属性注入失败”,比猜代码高效得多。日志不是万能的,但在 IoC 容器问题上,它经常能直接把线索摆在你面前。

就我个人经验,学 IoC 和 DI 最笨但最有效的方法,是找一个简单的例子,把@Autowired、@Service、@ComponentScan这三个注解手动写一遍,跑通启动类之后再故意制造一个循环依赖、一个多实现冲突,亲眼看到报错长什么样。踩过这些坑之后,再回头读源码也好、刷面试题也好,都会有根。Spring 的 IoC 容器本质上是一套对象管理框架,把它看成一个会做饭的后厨,你的业务代码只需要负责“点菜”,剩下的交给它就好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询