Spring Bean核心机制:作用域、生命周期、循环依赖与工厂模式深度解析
2026/9/19 1:53:05 网站建设 项目流程

Spring Bean 的核心机制,是 Java 面试的高频区,也是很多框架进阶绕不过去的一道坎。作用域、生命周期、工厂模式、实例化、循环依赖这五个问题,单拎出来都能问出深度,连在一起问基本就能看出一个人对 Spring 容器运作方式的真实理解程度。这篇文章我从实际开发角度把这些机制完整拆开,配上能直接跑通的代码示例和排查思路,帮你在原理层面把这块彻底打通。

1. Spring Bean 作用域:一次创建还是每次新建,得先搞清楚容器怎么存

1.1 五种作用域的区别与适用场景

Spring 容器启动后,Bean 的实例什么时候创建、创建几份、存活多久,都由作用域决定。日常开发里最常接触的就是 singleton 和 prototype,很多人对后者的理解就停留在"每次 getBean 都返回新对象",但真正踩过坑之后会发现,作用域选错,线上出 bug 的隐蔽程度非常高。

先看默认的 singleton 作用域。Spring 容器对一个 Bean 定义只创建一个实例,从容器启动到关闭,你拿到的永远是同一个对象。这就意味着它天然是共享的,任何线程在任意时刻操作这个 Bean,改的都是同一份内存数据。所以 singleton 作用域下的 Bean 必须无状态,或者至少状态是线程安全的。我在实际项目中见过有人把用户信息临时塞进 singleton Bean 的成员变量里,结果并发一上来数据全串了,这种问题排查起来特别费劲,因为不是必现。

prototype 作用域则完全相反,每次从容器里获取都会触发创建逻辑,相当于"用完即弃"的一次性对象。适合那些内部有状态、每次使用都需要独立初始化的场景,典型的就是消息推送模板、临时计算器等。但要注意,prototype Bean 的销毁并不由 Spring 容器负责。容器在创建后交给调用方,就不会再跟踪它的生命周期了。如果你在 prototype Bean 里开了数据库连接、IO 流这类资源,容器不会帮你关闭,必须自己手动处理。这一点是很多人刚开始接触 Spring 时完全没意识到的坑。

剩下的 request、session、application 三种作用域都只在 Web 环境可用。

  • request 作用域:每次 HTTP 请求创建一个新实例,请求结束就销毁。适合存放单次请求内的上下文数据,比如当前用户、请求链路信息。但要注意在非 Web 线程里取 request Bean 会直接报错,因为当前线程没有绑定 HTTP 请求。
  • session 作用域:同一个 HTTP 会话共享一个实例,适合存购物车、登录状态这类和用户会话绑定的数据。session 过期或失效时 Bean 销毁。
  • application 作用域:ServletContext 级别,整个 Web 应用共享一个实例,比 singleton 的生命周期还要长,和应用的启动关闭完全绑定。

我个人的建议是:没有任何特殊理由,就用 singleton;明确需要独立状态,用 prototype;Web 环境下的请求、会话上下文数据,优先考虑 request/session 作用域而不是塞到 singleton 里用 ThreadLocal 硬扛。

1.2 作用域在代码中的配置方式与实测注意点

作用域的配置方式有 XML、注解、Java Config 三种。现在主流项目基本都是注解方式,@Scope 配合 @Component 使用,value 属性指定作用域名称。默认就是 singleton,所以只要不加 @Scope,就是单例。

@Component @Scope("prototype") public class MessageTemplate { private List<String> receivers = new ArrayList<>(); public void addReceiver(String receiver) { receivers.add(receiver); } }

XML 配置方式则是:

<bean id="messageTemplate" class="com.example.MessageTemplate" scope="prototype"/>

Java Config 方式在 @Bean 注解里指定:

@Configuration public class AppConfig { @Bean @Scope("prototype") public MessageTemplate messageTemplate() { return new MessageTemplate(); } }

这里要重点提醒一句:singleton Bean 里注入 prototype Bean 是个大坑。很多人以为 singleton 里注入了一个 prototype Bean,每次用的时候都会拿到新的,实际上完全不是这样。Spring 在创建 singleton Bean 时做依赖注入,只会注入一次,也就是说 prototype Bean 在 singleton 里被"固定"了,后面拿到的永远是最开始那个实例。解决方式有两个:一个是注入 ObjectProvider,一个是加 @Lookup 注解。

使用 ObjectProvider 的方式最直观,注入后每次调用 getObject() 方法都能拿到新的 prototype 实例:

@Component public class SingletonService { private final ObjectProvider<MessageTemplate> templateProvider; public SingletonService(ObjectProvider<MessageTemplate> templateProvider) { this.templateProvider = templateProvider; } public void handle() { MessageTemplate template = templateProvider.getObject(); // 每次使用都是新实例 } }

聊到这我顺便提一个问题:很多框架文档里说 singleton 是"饿汉式加载",也就是容器启动时就会创建。这个说法在实际使用中要打个折扣。Spring 默认是启动时就实例化非懒加载的 singleton Bean,但如果某个 singleton Bean 配置了 @Lazy,就会推迟到第一次使用才创建。所以严格来说,singleton 不等于容器启动后就一定有,要看有没有配置懒加载。

2. Bean 生命周期:从类到可用对象,容器替我们做了哪些事

2.1 生命周期的完整阶段拆解

Bean 生命周期是整个 Spring 原理里信息量最大的一块。面试官问生命周期,本质上就想确认你是否了解 Bean 从"类的定义"变成"容器管理的成熟对象"这一路上经历了哪些关键节点。我在实际调试 Spring 应用时,也常常通过生命周期回调来排查初始化逻辑混乱的问题。

一个标准 singleton Bean 的完整生命周期可以拆成四个大阶段,每个阶段里又有若干细分节点。

第一阶段是 BeanDefinition 加载与解析。配置文件或者注解被解析成 BeanDefinition 后,Spring 才能知道要创建什么对象、有哪些属性需要注入、采用什么初始化策略。这个阶段发生在实例化之前,很多人会把 Bean 的"定义"和"实例"混为一谈,需要记住,BeanDefinition 只是蓝图。

第二阶段是实例化前的准备。Spring 会通过 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation 方法给开发者一个"劫持"创建过程的机会。如果这里返回了一个代理对象,后续的实例化流程就会跳过,直接用代理对象代替。

第三阶段是真正的实例化和属性填充。Spring 根据 BeanDefinition 选择构造器或工厂方法创建对象,然后依次完成属性注入、Aware 接口回调、BeanPostProcessor 前置处理、初始化方法调用、BeanPostProcessor 后置处理。这里顺序非常关键,后面我再展开。

第四阶段是 Bean 就绪后的使用与销毁。这个阶段对 singleton 而言非常长,一直持续到容器关闭;对 prototype 而言则没有销毁回调,容器直接"撒手不管"。

我放一张完整顺序的口诀方便记忆:构造 -> 属性填充 -> BeanNameAware -> BeanFactoryAware -> ApplicationContextAware -> BeanPostProcessor 前置 -> @PostConstruct -> InitializingBean -> init-method -> BeanPostProcessor 后置 -> 使用 -> @PreDestroy -> DisposableBean -> destroy-method。

2.2 初始化回调的三种实现方式与执行顺序

初始化回调有三种实现方式友好共存,且执行顺序固定:@PostConstruct 注解方法最先执行,然后是 InitializingBean 接口的 afterPropertiesSet 方法,最后是 XML/注解里配置的 init-method。

@Component public class UserService implements InitializingBean { public UserService() { System.out.println("1. 构造方法执行"); } @PostConstruct public void postConstruct() { System.out.println("2. @PostConstruct 执行"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("3. InitializingBean.afterPropertiesSet 执行"); } @Bean(initMethod = "init") public void init() { System.out.println("4. init-method 执行"); } }

为什么设计这么多套初始化机制?因为不同的使用场景接触 Spring 的深度不同。如果你自己写框架,或者做底层组件,可以用 InitializingBean 快捷实现;如果你的类不该依赖 Spring 的接口,就用 @PostConstruct;如果你需要兼容旧的 XML 配置且不想改代码,就指定 init-method。我在实际项目里见过一个非常混乱的代码库:一个 Bean 同时用三种方式做初始化,互相之间还产生了依赖关系,排查问题的时候根本不知道哪段代码先跑。所以团队里最好统一规范,优先用 @PostConstruct,特殊场景再用 InitializingBean。

销毁逻辑同样有三套对应机制:@PreDestroy、DisposableBean 的 destroy 方法、destroy-method。执行顺序正好和初始化相反,@PreDestroy 最先执行,然后 DisposableBean.destroy,最后 destroy-method。

2.3 BeanPostProcessor 这个扩展点的实际应用

BeanPostProcessor 是 Spring 生命周期里最强大的扩展点之一,它有两个方法:postProcessBeforeInitialization 在初始化方法执行前调用,postProcessAfterInitialization 在初始化方法执行后调用。所有 Bean 的创建过程都会经过这里。

这个扩展点最经典的用途就是 Spring AOP 的代理创建。AbstractAutoProxyCreator 就是一个 BeanPostProcessor,它会在 postProcessAfterInitialization 阶段判断当前 Bean 是否需要被代理,如果需要就生成代理对象返回。所以你要记住,Spring AOP 的代理是在 Bean 生命周期中完成的,不是在 Bean 实例化时完成的。

我自己做过一个实际项目:需要对某些特定注解标注的方法做统一日志埋点。当时没有用 AOP,而是写了一个 BeanPostProcessor,在 Bean 创建后扫描它的方法,对有 @LogRecord 注解的方法动态生成代理。通过这个方式,完全绕开了对业务代码的侵入,所有增强逻辑都收敛在一个独立组件里。如果你以后自己要设计类似的框架级功能,BeanPostProcessor 绝对是最值得优先考虑的扩展点。

使用 BeanPostProcessor 时要注意:它会影响容器里所有 Bean,所以逻辑里一定要做类型判断,否则会在无关对象上浪费性能。另外,尽量不要在 postProcessBeforeInitialization 阶段做需要依赖注入的属性操作,因为这个时候属性还没有填充完成。

3. 工厂模式在 Spring 中的体现:BeanFactory 与 FactoryBean 的分工

3.1 为什么要引入工厂模式来管理对象

工厂模式的核心价值是把"创建对象"和"使用对象"解耦。传统方式中,业务代码里直接 new 对象,一旦构造逻辑变了,所有调用处都要改。引入工厂后,调用方只需要和工厂交互,至于工厂内部怎么创建对象、要不要做增强、需不需要走代理逻辑,调用方完全不关心。

Spring 本身就是"一个巨大的工厂容器",这个说法并不夸张。我们写的业务类没有直接 new,而是声明 Bean 让 Spring 去创建和管理;开发时感觉不到,是因为框架把这些细节都藏起来了。理解工厂模式在 Spring 中的应用时,有一个非常容易混淆的点:BeanFactory 和 FactoryBean 是两个不同的概念。

BeanFactory 是 Spring IoC 容器的最顶层接口,定义了 getBean 等一系列基础方法。ApplicationContext 继承了它,并在其基础上增加了资源加载、事件发布、国际化等企业级能力。可以这么理解:BeanFactory 是容器的"骨架",ApplicationContext 是完整形态的"容器"。开发中我们用的 AnnotationConfigApplicationContext 就是 ApplicationContext 的典型实现。

FactoryBean 则是 Spring 提供的一个特殊的"工厂 Bean 接口"。当一个 Bean 本身需要复杂的创建逻辑时,可以把创建过程放进 FactoryBean 的实现类里,让 Spring 在需要时通过这个工厂来生成实际的目标对象。

3.2 FactoryBean 源码级理解与典型应用场景

FactoryBean 接口有三个方法:getObject() 返回由工厂创建的对象,getObjectType() 返回创建对象的类型,isSingleton() 表示创建的对象是否为单例。需要注意,从容器中 getBean("factoryBeanName") 拿到的是 getObject() 创建的"产品对象",而不是 FactoryBean 本身;只有 getBean("&factoryBeanName") 才能拿到 FactoryBean 的实例。

这样说有点抽象,我写一个例子。假设我们有一个 complexDataSource 配置,需要根据环境配置动态创建不同类型的数据库连接池:

@Component public class DataSourceFactoryBean implements FactoryBean<DataSource> { @Value("${db.type}") private String dbType; @Value("${db.url}") private String url; @Override public DataSource getObject() throws Exception { if ("druid".equals(dbType)) { return createDruidDataSource(); } else if ("hikari".equals(dbType)) { return createHikariDataSource(); } throw new IllegalArgumentException("unsupported db type: " + dbType); } @Override public Class<?> getObjectType() { return DataSource.class; } @Override public boolean isSingleton() { return true; } }

这样配置之后,业务代码注入 DataSource 时,Spring 会自动调用 DataSourceFactoryBean.getObject() 拿到真正的连接池对象,而业务代码本身并不知道有 FactoryBean 的存在。

FactoryBean 的典型应用场景,我看到过的有这么几类:一是封装第三方框架的复杂 Bean 创建逻辑,比如 MyBatis 的 MapperFactoryBean,通过 FactoryBean 机制为每个 Mapper 接口生成代理对象;二是根据配置动态选择创建策略;三是实现需要从本地方法、反射调用等特殊渠道获取的目标对象。自己写框架时遇到"Bean 的创建逻辑太复杂且不适合写在配置类里"的情况,优先考虑 FactoryBean。

3.3 你知道 ApplicationContext 和 BeanFactory 在对象创建上的差异吗

这个差异化问题在面试里经常被问到,也是很多人背了概念却说不清楚的地方。简单说,BeanFactory 默认在第一次 getBean 时才创建对象,也就是懒加载;而 ApplicationContext 默认在容器启动时就会预实例化所有非懒加载的 singleton Bean。这个差异本质上是"可用性和性能"的权衡:启动时就创建,可以更早发现 Bean 定义错误,但启动时间会变长;按需创建,启动更快,但初始化问题要到使用时才暴露。

拿阿里内部的一些中间件做例子,很多基础组件被设计成 ApplicationContext 启动时就完成初始化,因为运行时再发现 Bean 创建失败,会影响线上服务稳定性。而业务代码里那些创建成本极高的对象,比如一个需要远程调用的缓存客户端,可以通过 @Lazy 让它延迟加载,把初始化时间错开。

4. Bean 的实例化方式:构造器、静态工厂、实例工厂到底怎么选

4.1 三种实例化方式对比

Bean 实例化和对象创建不完全等同。Spring 支持三种实例化方式,分别是构造器实例化、静态工厂实例化、实例工厂实例化。

构造器实例化是最常用的方式。无论是无参构造器还是有参构造器,Spring 都能处理,有参时它会根据参数类型和名称自动进行匹配。日常开发里我们看到的 @Component、@Service 注解的类,最终走的基本都是构造器实例化流程。

静态工厂实例化指的是通过某个类的静态方法来创建对象,适用于创建逻辑封装在静态方法里的场景。比如你有一个 DateConverter 类,它的创建不能直接 new,而是通过 DateConverter.create() 静态方法生成:

public class DateConverter { private static DateConverter instance = new DateConverter(); private DateConverter() {} public static DateConverter create() { return instance; } }

XML 配置时用 factory-method 指定创建方法:

<bean id="dateConverter" class="com.example.DateConverter" factory-method="create"/>

实例工厂实例化则是通过一个已存在的工厂 Bean 的普通方法来创建目标对象。比如有一个 PrinterFactory 类,它有一个 createPrinter() 方法,Spring 会先创建 PrinterFactory 这个 Bean,然后调用它的工厂方法生成目标对象:

<bean id="printerFactory" class="com.example.PrinterFactory"/> <bean id="printer" factory-bean="printerFactory" factory-method="createPrinter"/>

这三种方式的使用建议是:日常开发百分之九十九都是构造器实例化,因为 Spring Boot 的自动配置体系已经帮你把大多数工厂逻辑都封装好了;静态工厂适合第三方工具类,且不依赖实例状态;实例工厂适合创建过程需要多个步骤、多个参数或者有复杂前置条件的场景。如果用了其中某一种方式却解决问题不顺手,先想想是不是设计本身有问题,而不是纠结换工厂方式。

4.2 构造器注入、Setter 注入、字段注入的选择逻辑

类的实例化思路确定后,属性怎么注入又是一个重要决策。构造器注入、Setter 注入、字段注入各有优劣,现在 Spring 官方推荐的是构造器注入,原因是它天然保证不可变性和完整性——对象一旦创建,所有依赖就位,不会出现属性为空的情况。

@Component public class OrderService { private final OrderDao orderDao; private final UserService userService; public OrderService(OrderDao orderDao, UserService userService) { this.orderDao = orderDao; this.userService = userService; } }

字段注入代码写起来最省事,但带来的问题也很明显:类无法脱离 Spring 容器独立测试;依赖关系对调用方完全不透明;而且无法用 final 修饰,存在对象在依赖没就绪时被使用的风险。我看到过很多团队觉得"字段注入很简洁"就用它,结果写单元测试时各种艰难。Setter 注入则适合那些可选依赖或者创建后可以调整的依赖,但要注意最终对象状态可能不完整。

我个人的工程标准是:强制依赖用构造器注入,可选依赖用 Setter 注入并给默认值,字段注入不作为代码规范的一部分。之前接手过一个老项目,代码里全是 @Autowired 字段注入,想测试一个服务类,发现几乎每个内部依赖都通过 Spring 容器才能正确初始化,单测写起来简直是灾难。后来在代码评审里开始推构造器注入,半年后单测效率提升非常明显。

5. 循环依赖:三级缓存的设计思路与源码拆解

5.1 循环依赖产生的根本原因

循环依赖就是两个或多个 Bean 之间互相引用,形成一个环。比如 A 依赖 B,B 又依赖 A。Spring 的默认行为是单例 Bean 之间的 setter 注入循环依赖是可以自动解决的,但构造器注入的循环依赖解决不了,会直接报 BeanCurrentlyInCreationException。

理解解决方案之前,先理解 Spring 单例 Bean 的创建流程。大致是:实例化(new 出一个对象)-> 属性填充 -> 初始化。关键在于:实例化和属性填充是分开的两个步骤。Spring 能解决 setter 循环依赖,靠的就是"先实例化、后填属性"这个时间差。对象先 new 出来放在缓存里(哪怕属性还没填全),另一个 Bean 在创建时就能引用到它,等到两边属性都填充完成,整个依赖链也就闭合了。

而构造器注入的循环依赖无法解决,是因为对象连 new 都 new 不出来。构造器里就需要传入依赖对象,A 创建时需要 B,B 创建时需要 A,两边都卡在第一步,死锁。所以结论非常清晰:要避免循环依赖,最稳妥的方法就是设计时就打破环,或优先使用构造器注入让问题尽早暴露。

5.2 三级缓存的作用:从源码级别看懂 Spring 如何解决 setter 循环依赖

Spring 解决 setter 循环依赖的底层原理是通过三级缓存。三级缓存分别指:

  • 一级缓存 singletonObjects:保存已经完成初始化的完整单例 Bean。
  • 二级缓存 earlySingletonObjects:保存提前暴露的早期 Bean 引用,也就是对象已创建但还没完成属性填充和初始化的实例。
  • 三级缓存 singletonFactories:保存单例 Bean 的工厂,用来生成早期 Bean 引用。

使用三级缓存的前提是:该 Bean 允许早期引用暴露,默认情况单例 Bean 都是允许的,除非显式配置禁止。

整个解决过程用 A、B 循环依赖的例子来串一遍。假设 A 先创建:

  1. A 开始实例化,new 出 a 对象。
  2. A 的实例化完成后,Spring 会把一个 ObjectFactory 放入三级缓存,这个工厂可以在需要时通过反射把当前对象转换成代理等目标形式。
  3. A 开始属性填充,发现需要 B,于是去容器中找 B。
  4. B 开始实例化,new 出 b 对象,B 的工厂也放入三级缓存。
  5. B 开始属性填充,发现需要 A,去缓存找 A。此时一级缓存没有 A(A 还没创建完),二级缓存也没有,但三级缓存有 A 的工厂。
  6. Spring 通过三级缓存的工厂拿到 A 的早期引用,放进二级缓存,然后交给 B 的属性填充,B 顺利创建完成。
  7. B 创建完成后,A 的属性填充可以从一级缓存或二级缓存拿到 B,A 继续走完初始化流程,最后 A 也进入一级缓存。
  8. 此时二级缓存里的 A 早期引用会被清除,之后所有线程拿到的都是完整初始化后的 A。

为什么需要三级缓存而不是二级?核心原因是 AOP 代理。如果 A 需要被代理,那么在 B 引用 A 时,直接暴露原始对象还不够,需要暴露代理对象。三级缓存里的 ObjectFactory 就是干这个的——它能够在暴露时判断是否需要生成代理对象。如果改成二级缓存,虽然也可以实现循环依赖的支持,但需要提前完成代理生成,这会让那些不需要代理的 Bean 也白白走一遍代理判断流程,性能上不划算。所以三级缓存不仅是"能解决"的问题,更是"用最合理的方式解决"的问题。

5.3 @Lazy 解决循环依赖的原理与真实项目中的取舍

除了三级缓存,Spring 还提供了 @Lazy 注解,它可以打破循环依赖。原理是:注入的对象不需要立即创建,而是生成一个代理占位符,等真正使用的时候才去容器里获取目标对象。

@Component public class A { private final B b; public A(@Lazy B b) { this.b = b; } }

通过 @Lazy 注入 B,A 在创建时不需要真正获取 B,而是先拿到一个 B 的代理,等到 A 创建完成、后续使用到 B 时,代理才会去容器里触发 B 的创建。这样原本卡住的循环依赖就被绕开了。

它在真实项目里的使用要非常克制。@Lazy 虽然能"解决"循环依赖,但本质上是在掩盖设计问题。如果代码里大量使用 @Lazy 打破循环依赖,说明模块边界可能已经划分不清晰了。我个人经验是:新代码里出现循环依赖,优先重构;只有改造成本很高、或者临时需要快速解耦的场景才考虑 @Lazy。另外要注意,@Lazy 解决的是依赖注入时的协同问题,不是说加了 @Lazy 之后就不会再有循环依赖报错,用的时候还是要检查清楚依赖链。

有一个线上问题我一直记忆深刻:某个服务启动时一直报 BeanCurrentlyInCreationException,定位后发现三个服务互相依赖,A 构造器注入 B,B 构造器注入 C,C 又构造器注入 A。因为三个都是构造器注入,三级缓存完全帮不上忙。最终我们用拆分配置、把 C 的依赖改为 Setter 注入才解决。这个教训让我在评审代码时,对循环依赖零容忍,全部要求提前根治。

6. 核心五问速查表与常见问题实录

为了方便你面试前复盘和工作中快速查阅,我把这五个核心问题的关键结论整理成了一张表,配合我实际踩过的一些问题记录一并放出。

核心问题核心结论高频注意点
作用域singleton 默认、prototype 每次新对象、request/session 仅 Web 可用singleton 注入 prototype 要使用 ObjectProvider 或 @Lookup
生命周期实例化 -> 属性填充 -> Aware -> BeanPostProcessor -> 初始化 -> 后置 -> 使用 -> 销毁@PostConstruct 先于 InitializingBean 先于 init-method
工厂模式BeanFactory 是容器接口,FactoryBean 是创建对象的工厂接口getBean("xxx") 取产品对象,getBean("&xxx") 取工厂本身
实例化构造器实例化为主,静态/实例工厂方式用于特殊场景强制依赖用构造器注入,避免字段注入
循环依赖单例 setter 循环依赖由三级缓存解决,构造器循环依赖无法解决新代码尽量避免循环依赖,重构优先,@Lazy 只做临时解围

6.1 高频问题一:prototype Bean 里注入了 singleton 组件,能正常使用吗

能正常使用,而且是常见做法。prototype Bean 每次创建时,容器会把依赖的 singleton 组件注入进去。这里真正需要注意的是反向场景,即 singleton 里注入 prototype,此时 prototype 会被固定为单例。我们前面讲过通过 ObjectProvider 或 @Lookup 解决,不再重复。实际工作中,很多团队用 prototype Bean 封装"每次请求都变化"的上下文信息,内部再注入一些无状态的 singleton 服务,这种组合是没有问题的。

6.2 高频问题二:BeanPostProcessor 和 BeanFactoryPostProcessor 有什么区别

这两个概念特别容易混淆,我见过不少初级开发把名字都记错。BeanFactoryPostProcessor 是在 BeanDefinition 加载完成后、Bean 实例化之前执行的,它的作用是修改 BeanDefinition 本身,比如修改某个 Bean 的作用域、属性值、是否懒加载等。而 BeanPostProcessor 是在 Bean 实例化之后、初始化前后执行的,它的作用是干预 Bean 的"加工"过程。

用生活化例子解释:BeanPostProcessor 像是汽车走下生产线后的装配流程,轮子、引擎都装好了再检查调试;BeanFactoryPostProcessor 则是修改设计图纸,在汽车正式投产前把图纸参数改掉。Spring 内部很多配置占位符的解析就是通过 BeanFactoryPostProcessor 完成的,比如 ${...} 占位符替换。

6.3 高频问题三:只要加了 @Autowired 就一定能注入成功吗

不一定。能否注入成功取决于多个条件。首先,容器中必须有匹配类型的 Bean,如果存在多个候选 Bean 且没有用 @Primary、@Qualifier 指定,会抛 NoUniqueBeanDefinitionException。其次,如果 Bean 是懒加载的且当前还没有触发创建,也会出现运行时获取不到的情况。还有一种是构造器注入时依赖链中有循环依赖,创建直接失败。所以 @Autowired 只是声明"我需要一个 Bean 并交给容器注入",真正的注入成功取决于容器状态和 Bean 定义是否正确。

6.4 高频问题四:如何在你自己的代码里排查循环依赖

排查循环依赖有一个非常实用的经验:启动日志里出现 BeanCurrentlyInCreationException 时,异常信息一般会打印出"Requested bean is currently in creation: Xxx",同时附带创建依赖栈。根据栈信息里反复出现的两个类,基本就能确定环的位置。然后从设计层面拆环。

我在项目中总结的一套拆环思路是这样的:先明确环里每个 Bean 的关系,看哪些是强关联、哪些是弱关联;强关联的部分尝试通过拆分接口、提取公共组件把依赖关系打平;弱关联的可以考虑用事件发布、异步处理来解耦;如果都不可行,再考虑用 @Lazy 临时突破,但要记录技术债,在后续迭代里重构成干净的设计。

6.5 高频问题五:自定义一个 Bean 且需要依赖 Spring 关闭时自动清理,有什么方案

如果你希望容器关闭时自动回调某个清理逻辑,优先用 @PreDestroy。这个注解由 Spring 容器在销毁 Bean 前自动调用,适合释放连接、关闭线程池、保存缓存等场景。InitializingBean、DisposableBean 接口也能实现同样的效果,但会让业务类直接耦合 Spring 的 API,对自定义框架或公共组件不太友好。注意 prototype Bean 不会触发销毁回调,所以 prototype 里的资源清理必须由调用方负责。

另外还有一个小技巧:如果项目使用的是 ApplicationContext,可以在关闭时先调用 context.close(),然后观察日志确认 Bean 的销毁方法是否按预期执行。排查资源泄漏问题时,这个日志非常关键。我遇到过线上连接池不释放的问题,最终查到就是因为有人写的清理逻辑放在了 prototype Bean 的 @PreDestroy 方法里,而它压根不会被容器调用。了解了生命周期机制之后,这个问题几分钟就能定位。

Spring Bean 这套机制,说到底是容器帮我们管理对象的创建、组装和销毁,看起来简单,但里面环环相扣。这篇文章里列出的五问,每一问都有源码设计和工程实践的影子。你在自己写框架、排查注入问题、设计模块依赖时,带着这套思路去理解 Spring 的行为,会比死记面试题高效得多。

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

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

立即咨询