拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Bean生命周期与工厂类:从注解配置到三级缓存的全链路解析

Spring Bean生命周期与工厂类:从注解配置到三级缓存的全链路解析

1. 先把三件事串成一条线:工厂类、Bean生命周期与注解配置的内在关系

做Spring开发这么多年,我发现一个特别有意思的现象:很多同学能熟练用@Component、@Autowired、@Configuration,项目也能正常跑起来,但一旦遇到循环依赖报错、Bean被创建了两次、@Value注入不生效这类问题,就完全没了排查思路。原因很简单——你只学会了注解怎么写,没搞懂注解背后的执行引擎。

Spring的整个IoC容器,本质上就干三件事:解析配置、按规则造对象、管理对象的全过程。这三件事正好对应标题里的三个关键词:注解配置负责"描述规则",工厂类负责"按规则造对象",Bean生命周期负责"对象从出生到消亡的全程管理"。它们不是三个独立的知识点,而是一条流水线上的三个环节。

拿一个最普通的@Autowired注入举例:容器启动时,Spring先扫描你标了注解的类,把它们的信息封装成BeanDefinition(配置环节);接着通过BeanFactory这个工厂去创建对象,但在创建过程中会经过一系列扩展点(工厂环节);对象创建完成后,Spring还要负责属性填充、初始化回调、代理包装,甚至当容器关闭时还要触发销毁方法(生命周期环节)。任何一环出了问题,整个应用就起不来。

这篇文章我不打算写成一章一节的教科书式讲解,而是按照"流水线"的顺序往下走:先讲工厂类为什么是容器的心脏,再顺着Bean的完整生命周期把每个钩子过一遍,最后解释注解配置是怎么变成实实在在的BeanDefinition的。每一部分都会配代码和踩坑记录,希望能帮你在脑海里建立起Spring的完整执行模型——有了这个模型,面试和排故障都会轻松很多。

2. 工厂类不只是new:BeanFactory与FactoryBean的职责边界

2.1 BeanFactory是"容器"而非"工厂"

很多初学者会把BeanFactory理解成"创建Bean的工厂类",这个理解不能说错,但容易把后面的机制绕晕。准确地说,BeanFactory是容器的最顶层接口,它的核心职责是管理Bean,包括获取、查找、判断类型、判断是否单例这些操作。真正负责"创建"动作的,是它内部实现的getBean方法——但这个方法背后调用的又是由一系列策略组件(比如InstantiationStrategy、BeanPostProcessor)协作完成的。

从这个角度理解,BeanFactory更像是一个"对象托管中心",而不是一个简单的"new工厂"。举个例子:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class);

这里的context(ApplicationContext)底层就持有一个DefaultListableBeanFactory。你调用getBean,它先去单例池里找有没有现成的,没有才会走创建流程。这种设计最大的好处是:调用方完全不需要关心对象是怎么创建出来的,只需要知道"我给它一个类型/名字,它给我一个可用的实例"。这也正是控制反转的本质——创建对象的控制权从业务代码转移到了容器。

2.2 FactoryBean:当Bean本身就是一个工厂时

BeanFactory是容器,FactoryBean是容器里可以被管理的一个特殊Bean。这个区别很绕,但特别重要。FactoryBean的定位是:当某个对象的创建逻辑过于复杂,不适合用普通的构造器+属性填充来表达时,你给它做一个专门的工厂。

我记得自己第一次真正理解FactoryBean,是在看MyBatis的源码时。MyBatis-Spring里的SqlSessionFactoryBean就是一个典型的FactoryBean实现。正常情况下你写一个Bean需要指定class,然后容器负责实例化;但SqlSessionFactoryBean需要读取数据源、解析XML映射文件、配置插件等一系列复杂步骤,这些逻辑如果全塞给容器去猜,容器根本做不到。于是MyBatis框架提供SqlSessionFactoryBean实现FactoryBean接口,容器只负责创建SqlSessionFactoryBean本身(这个很简单,就是new一下),然后调用它的getObject()方法,返回真正的SqlSessionFactory对象。

用代码来体现一下FactoryBean的定义方式:

@Component public class MySpecialBeanFactory implements FactoryBean<SpecialBean> { @Override public SpecialBean getObject() throws Exception { // 这里可以做复杂的构造逻辑,比如读取远程配置 SpecialBean bean = new SpecialBean(); bean.setConfig(loadRemoteConfig()); bean.init(); return bean; } @Override public Class<?> getObjectType() { return SpecialBean.class; } @Override public boolean isSingleton() { return true; } }

需要注意一个容易混淆的点:当你通过容器getBean("mySpecialBeanFactory")时,拿到的不是SpecialBeanFactory实例,而是它getObject()返回的SpecialBean。要想拿到工厂本身,必须用&前缀:getBean("&mySpecialBeanFactory")。这个细节我在实际项目里见过不止一次有人在这上面翻车——注入半天发现类型对不上,就是因为没搞清楚容器里注册的到底是工厂还是工厂的产品。

2.3 三种实例化方式的对比与取舍

在Spring里,Bean的实例化方式大致可以归纳为三类:构造器实例化、静态工厂方法实例化、实例工厂方法实例化。加上FactoryBean,常被拿来对比的就有四种。我整理了一张对比表:

实例化方式配置写法使用场景注意事项
构造器实例化<bean class="com.example.User"/>或直接@Component大多数常规业务Bean私有构造器会导致实例化失败
静态工厂方法<bean class="ExampleFactory" factory-method="createInstance"/>工具类、历史遗留的静态工厂代码工厂方法必须为static且返回类型匹配
实例工厂方法<bean factory-bean="factory" factory-method="getInstance"/>需要先有工厂对象再产出产品工厂本身也得注册为Bean
FactoryBean实现FactoryBean接口复杂对象、第三方集成、代理生成注意&前缀取工厂本身

从实践角度来看,现在用纯注解开发时,90%的Bean都是构造器实例化——Spring会自动推断构造函数,如果有多个构造函数需要配合@Autowired指定。静态工厂和实例工厂在SpringBoot的自动配置里偶尔会出现,比如一些第三方Starter会用工厂方法模式注册组件,但业务代码里基本用不上。

我个人的观点是:FactoryBean是这几个方案中最值得花时间去学的,因为它代表了一种"延迟复杂创建逻辑"的封装思想。框架层的很多魔法都是基于它实现的,比如Ribbon的LoadBalancer、Feign的FeignClientFactoryBean。理解透FactoryBean,后面看SpringCloud的源码会顺畅很多。

3. Bean生命周期全链路:从BeanDefinition到销毁钩子

3.1 实例化(Instantiation)与初始化(Initialization)不是一回事

说到生命周期,我最常纠正新人的一个误解是:实例化和初始化是两个阶段,之间还隔着一个极其重要的属性填充阶段。实例化只是调用构造函数把对象new出来,此时对象里的依赖字段全是null,处于"还没准备好的状态";属性填充(Populate)才会把@Autowired、@Resource、@Value这些依赖注入进去;初始化才是执行InitializingBean回调、@PostConstruct、init-method这些定制逻辑。

顺序大概是这样的:

BeanDefinition解析 -> 实例化前回调(InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation) -> 构造器实例化 -> 实例化后回调(postProcessAfterInstantiation) -> 属性填充(Autowired、Resource、Value等) -> Aware回调(BeanNameAware、BeanFactoryAware等) -> BeanPostProcessor.postProcessBeforeInitialization -> InitializingBean.afterPropertiesSet / @PostConstruct / init-method -> BeanPostProcessor.postProcessAfterInitialization -> 放入单例池,Bean已就绪

这里有几个细节很关键。第一个是Aware回调的位置,它在属性填充之后、初始化之前执行。也就是说你写了一个BeanNameAware接口,重写setBeanName()时,这个Bean的依赖已经注入完成了,你可以在setBeanName里放心使用这些依赖。第二个是init-method的执行顺序,它和InitializingBean、@PostConstruct的先后关系:@PostConstruct最先、InitializingBean其次、init-method最后。如果在同一个Bean里同时用了这三种初始化方式,执行顺序就是这个顺序。

3.2 三级缓存与循环依赖:生命周期中最烧脑的一环

谈到Bean生命周期,绕不开循环依赖和三级缓存。很多面试题喜欢问"Spring怎么解决循环依赖",但实际工作中更重要的是理解为什么三级缓存能解决循环依赖,以及哪些情况下解决不了。

三级缓存指的是三个Map:

// 一级缓存:存放完整可用的单例Bean Map<String, Object> singletonObjects = new HashMap<>(); // 二级缓存:存放早期暴露的原始对象引用(还没完成属性填充或初始化) Map<String, Object> earlySingletonObjects = new HashMap<>(); // 三级缓存:存放ObjectFactory,用于在需要时生成代理对象 Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();

循环依赖的场景是:A依赖B,B依赖A。单例模式下,Spring先创建A,发现A需要注入B,于是去创建B;B创建时发现它需要注入A,但A还没创建完——这时候如果直接报错就完蛋了。Spring的做法是:A在实例化之后、属性填充之前,就把自己的一个ObjectFactory放进三级缓存。这个ObjectFactory的能力是:返回A的早期引用(必要时生成代理)。B注入A时,从三级缓存拿到早期引用,完成自己的创建;A接着从B那里拿到完整对象,最终完成创建。

但这里有一个大坑:构造器注入的循环依赖无法解决。原因很好理解,三级缓存暴露的是"实例化之后"的对象引用,如果A是通过构造器注入B的,那么A在构造阶段就需要B,而此时A还没有完成实例化,根本没机会放入三级缓存。同理,@Async注解创建的代理对象也有类似的问题,因为代理生成被延后了。

我记得之前线上出过一次故障:两个Service通过构造器互相依赖,SpringBoot 2.6直接把循环依赖禁止了,项目启动直接报错。当时排查半天才反应过来——SpringBoot 2.6版本开始默认禁止循环依赖(spring.main.allow-circular-references=false),如果你的代码里历史遗留下了循环依赖,升级SpringBoot后必须显式开启这个配置,或者赶紧重构掉。

3.3 一个能打印全过程的演示Bean

理论讲多了容易飘,我写了一个简单的胖Bean,把生命周期里的所有阶段全打印出来,照着跑一遍就能对生命周期有直观感受:

@Component public class LifecycleDemoBean implements InitializingBean, BeanNameAware, BeanFactoryAware { private String name; @Autowired private AnotherBean anotherBean; public LifecycleDemoBean() { System.out.println("1. 构造函数执行:实例化"); } @Autowired public void setName(String name) { System.out.println("2. 属性填充-方法注入:setName"); this.name = name; } @PostConstruct public void postConstruct() { System.out.println("4. @PostConstruct执行"); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("3. Aware回调:BeanFactoryAware"); } @Override public void setBeanName(String name) { System.out.println("3. Aware回调:BeanNameAware"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("5. InitializingBean.afterPropertiesSet执行"); } @PreDestroy public void preDestroy() { System.out.println("销毁阶段:@PreDestroy执行"); } }

同时注册一个BeanPostProcessor来观察前后置处理:

@Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("BeanPostProcessor.beforeInitialization:" + beanName); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("BeanPostProcessor.afterInitialization:" + beanName); return bean; } }

跑这个Demo时你会发现,打印顺序中构造函数和属性填充并不是紧挨着的,中间可能有其他Bean的创建过程穿插。这种穿插正是容器管理复杂性的体现——Spring不是一个个Bean单独走完流程,而是按依赖关系递归创建,因此日志顺序一片混乱很正常。真正想看单个Bean的生命周期,建议给目标Bean单独打断点看调用栈。

4. 注解配置的底层逻辑:@Configuration与@ComponentScan只是入口

4.1 ConfigurationClassPostProcessor:注解配置的"总开关"

你有没有想过这样一个问题:@Configuration作用于类上的时候,Spring是怎么知道要去解析它的?我用一个例子来说明——@ComponentScan里配置了扫描包路径,但Spring为什么要去解析@ComponentScan这个注解本身?

答案是ConfigurationClassPostProcessor,一个BeanDefinitionRegistryPostProcessor。它的优先级在所有BeanPostProcessor之前,容器启动时最先执行。它会扫描容器里所有的配置类(包括用@Configuration标注的类),然后做两件事:解析@ComponentScan、解析@Bean方法。

我这里有一个实际调试经历:某次项目里所有@Autowired注入全部失败,报错说找不到Bean。查了半天,发现是配置类写成了@Component而不是@Configuration,而且没有显式加@ComponentScan。因为@Component标注的类虽然也能被扫描到,但它的子类不会经过ConfigurationClassPostProcessor的完整链路处理,很多针对配置类的增强逻辑就丢失了。换句话说,@Configuration不只是个语义标记,它决定了你的配置类能不能被CGLIB代理增强。

4.2 @Bean方法之间的调用:为什么同一个实例会被拦截

在@Configuration类里,你可能会这样写:

@Configuration public class AppConfig { @Bean public ServiceA serviceA() { return new ServiceA(serviceB()); } @Bean public ServiceB serviceB() { return new ServiceB(); } }

你想当然地以为serviceA()内部调用serviceB()会直接调用方法new一个ServiceB出来,如果这样,每次调用serviceB()都会创建新实例,但Spring容器里serviceB这个Bean是单例。那serviceA里到底注入了哪个实例?

答案藏在CGLIB代理里。@Configuration类默认会被CGLIB动态代理,当你调用serviceB()方法时,代理会拦截这个方法,检查容器里是否已经有serviceB这个Bean,有就直接从单例池返回,不会再执行方法体。这就是为什么@Configuration下的@Bean方法互相调用能保证拿到同一实例。

但如果你把@Configuration替换成@Component或@Configuration(proxyBeanMethods = false),CGLIB代理就失效了,serviceB()会真正执行方法体,产生一个spring容器之外的全新实例。这种模式叫"Lite模式"。SpringBoot自动配置里大量使用lite模式提升启动速度,但代价就是@Bean方法之间的依赖不能靠方法调用维护。遇到这种情况,最稳的写法是方法参数里声明依赖:

@Bean public ServiceA serviceA(ServiceB serviceB) { return new ServiceA(serviceB); }

这样的写法无论配置类是Full模式还是Lite模式,都能正确拿到容器里注册的ServiceB实例。这个经验是我在优化项目启动时间时积累下来的——把大配置类全部改成proxyBeanMethods=false之后,启动快了不少,但顺带引入了好几个方法调用级别的Bug。

4.3 @Import、@Conditional与自动配置的联动

注解配置除了@ComponentScan和@Bean,还有几个"隐藏入口"值得注意。比如@Import可以在配置类里快速引入其他配置类或普通类到容器;@Conditional系列可以按条件判断要不要注入某个Bean。

在SpringBoot生态里,自动配置起始就是AutoConfigurationImportSelector通过@Import方式引入的,它根据classpath里有没有对应的类,来决定创建哪些Bean。这正是工厂类和注解配置联动的高光时刻——配置类负责声明"什么样的条件成立时创建什么样的Bean",工厂负责按声明去生产。

我建议有时间可以把SpringBoot的spring.factories机制和@ConditionalOnClass、@ConditionalOnMissingBean这些注解逐一过一遍。这些东西初看只是"条件开关",本质上是把工厂策略模式搬到了注解层面,让"创建谁、不创建谁"可以由外部条件动态决定。理解了这层,看那些第三方Starter的源码就很有条理了。

5. 实战视角:怎么用生命周期钩子做正经事

5.1 根据生命周期阶段修改Bean属性

网上搜"根据Bean的生命周期修改属性值"这个需求的人不少,其实Spring提供了非常清晰的切入点。如果你需要在初始化之前修改Bean的某个属性,那就实现BeanPostProcessor,在postProcessBeforeInitialization里做修改。

举一个我在项目里的真实案例:微服务里每个服务都要往Redis写一个注册信息,注册信息的key包含当前应用名和实例ID。代码里不想让每个Service都自己去拿实例ID,所以我搞了一个统一的BeanPostProcessor,凡是实现了某个自定义接口的Bean,自动注入实例ID属性:

public interface InstanceAware { void setInstanceId(String instanceId); } @Component public class InstanceIdInjector implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof InstanceAware) { String instanceId = generateInstanceId(); ((InstanceAware) bean).setInstanceId(instanceId); } return bean; } }

这样的好处是:业务代码完全不用感知实例ID从哪来,只负责实现InstanceAware接口接收值。而且这个修改发生在属性填充之后、业务初始化之前,所以在@PostConstruct里就能直接用注入好的实例ID。

需要注意的一点是:在postProcessBeforeInitialization里修改属性,前提是这个属性在属性填充阶段没有被覆盖。如果你同时用了@Value给这个字段赋值,那么赋值发生在属性填充阶段,早于BeanPostProcessor,此时后者的修改会覆盖@Value的值,这个先后顺序要心里有数。

5.2 用BeanPostProcessor做AOP的入口理解

Spring的AOP实现,核心其实就藏在BeanPostProcessor里。AnnotationAwareAspectJAutoProxyCreator就是一个BeanPostProcessor,它的postProcessAfterInitialization阶段检查这个Bean需不需要被代理,如果需要就生成一个代理对象替换原来的Bean。

顺着这个逻辑回头看,为什么@Transactional事务注解有时候失效?原因之一是你的Bean被代理了,但你调用事务方法时是通过this调用而不是通过代理调用——事务切面压根没机会拦截。这和循环依赖里三级缓存为什么要暴露ObjectFactory也有关系:如果Bean已经被代理了,早期暴露的引用Earl要是不经过三级缓存适配,所有依赖它的Bean拿到的都是普通原始对象,代理就白做了。

我的体会是:当你理解BeanPostProcessor是所有自动增强机制的共同底座后,Spring里大量"为什么这里要这样设计"的疑问都会消散。它就像一个流水线上的固定工位,每个经过的Bean都得在这个工位安检一遍,然后决定是放行、篡改、还是换个替身。

5.3 自定义一个生命周期日志切面

工程上还有一种非常常见的需求:监控Bean初始化耗时。在Bean密集、启动时间敏感的场景下,排查哪个Bean创建最慢,可以自己挂一个BeanPostProcessor来计时:

@Component public class BeanInitTimeMonitor implements BeanPostProcessor { private final Map<String, Long> initTimeMap = new ConcurrentHashMap<>(); @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { initTimeMap.put(beanName, System.currentTimeMillis()); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Long start = initTimeMap.get(beanName); if (start != null) { long cost = System.currentTimeMillis() - start; if (cost > 200) { System.out.println("慢初始化Bean: " + beanName + " 耗时 " + cost + "ms"); } } return bean; } }

这个监控实现非常简单,但实际使用起来效果很好。我第一次用在生产环境里,立刻就揪出了一个初始化耗时超过2秒的Bean——它在一个@PostConstruct里同步拉取远程配置,导致整个应用启动时间被拉长。虽然直接用Actuator也能看到部分信息,但这么搞非常直观,可以精确到每个Bean。

6. 后置处理器和Bean工厂共同构建的扩展基石

6.1 手写一段简化版BeanFactory感受Spring的设计

我一直认为,学Spring源码最有效的路径之一是手写一个简化版。下面这段代码实现了最基本的按名字获取单例Bean的流程,虽然只有三十来行,但已经把三级缓存的核心思想体现了一部分:

public class MySimplifiedBeanFactory { private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(); private final List<BeanPostProcessor> postProcessors = new ArrayList<>(); public void registerBeanDefinition(String beanName, BeanDefinition definition) { beanDefinitionMap.put(beanName, definition); } public void registerPostProcessor(BeanPostProcessor processor) { postProcessors.add(processor); } public Object getBean(String beanName) { Object singleton = singletonObjects.get(beanName); if (singleton != null) { return singleton; } BeanDefinition definition = beanDefinitionMap.get(beanName); if (definition == null) { throw new RuntimeException("No bean named " + beanName); } singleton = createBean(beanName, definition); singletonObjects.put(beanName, singleton); return singleton; } private Object createBean(String beanName, BeanDefinition definition) { // 1. 实例化:这里简化成通过反射创建 Object bean = instantiate(definition); // 2. 属性填充:这里简化成按属性名赋值 populate(bean, definition); // 3. 初始化前后置处理 for (BeanPostProcessor processor : postProcessors) { bean = processor.postProcessBeforeInitialization(bean, beanName); } // 4. 初始化回调,这里省略具体实现 for (BeanPostProcessor processor : postProcessors) { bean = processor.postProcessAfterInitialization(bean, beanName); } return bean; } }

不考虑代理、循环依赖、作用域这些复杂情况,这个简易工厂把最核心的模板方法流程做出来了。Spring真正的DefaultSingletonBeanRegistry里,getSingleton的代码复杂得多,但主干就是这么几件事。我自己在写这个简化版的过程中,对这个模板的理解比看十篇源码解析都深。

6.2 为什么建议多看几个BeanPostProcessor的实现

Spring内置了大量的BeanPostProcessor实现,每一类背后都对应一个功能点:

BeanPostProcessor实现类负责的功能触发的时机
AutowiredAnnotationBeanPostProcessor@Autowired、@Value注入解析属性填充阶段
CommonAnnotationBeanPostProcessor@Resource、@PostConstruct、@PreDestroy属性填充/初始化/销毁
AnnotationAwareAspectJAutoProxyCreatorAOP代理生成初始化后阶段
ScheduledAnnotationBeanPostProcessor@Scheduled定时任务注册初始化后阶段
ApplicationListenerDetector识别ApplicationListener并注册初始化后阶段

带着这张表去看Spring源码,你会发现所有功能都被"安装"到了生命周期流水线的特定工位上。比如定时任务为什么在Bean初始化完成后才能生效?因为ScheduledAnnotationBeanPostProcessor在postProcessAfterInitialization里拿到的是已经完成所有准备工作的Bean,这时候解析@Scheduled才安全。

这种设计思路对普通人做架构也是很有启发的。我们做自己的框架时,同样可以预留几个"工位"——定义一系列类似BeanPostProcessor的Spare接口,让用户可以在对象创建的各个阶段插入自定义逻辑,这是Spring扩展性的核心魔法,也是它能统治Java生态这么多年的重要原因之一。

7. 容易被忽略的Bean生命周期细节与排错经验

7.1 各种初始化方式的优先级与混用

实际项目中,一个Bean里同时出现@PostConstruct、InitializingBean和init-method的情况不算少见。执行顺序前面提过:@PostConstruct -> afterPropertiesSet -> init-method。这个顺序有一个现实影响:如果你想在Bean初始化最早期放一些标记或统计逻辑,@PostConstruct是最合适的;如果想确保它发生在所有自定义初始化之后,init-method最靠后。

我遇到过的一个真实问题:某个Bean在@PostConstruct里去查数据库,结果报出空指针。排查半天,发现数据源实例是在afterPropertiesSet里初始化的,而afterPropertiesSet晚于@PostConstruct,所以查数据库时连接池还没建好。后来把数据库初始化移到了@PostConstruct之前,问题才解决。这提醒我们:多阶段初始化虽然灵活,但也意味着你必须清楚每段逻辑应该放在哪一层,否则很容易出现初始化顺序导致的依赖失败。

7.2 单例与原型作用域下生命周期执行的差异

Bean实际创建次数和生命周期钩子的执行次数,取决于scope。默认singleton模式下,每个Bean只创建一次,所有钩子也只执行一次;prototype模式下每次getBean都会创建新实例,并且容器关闭时不会调用销毁回调。

这个差异的实际影响是:如果你在单例Bean里注入了一个prototype Bean(比如直接@Autowired),那注入的实际是一个固定实例,那个Bean的生命周期已经被"冻结"在单例宿主里了。要做成每次获取都是新实例,通常要使用ObjectProvider、@Lookup或者ScopedProxyMode。我之前的文章里详细写过这块,这里简单提一句,核心还是别把prototype当"每次new"用,它必须配合正确的获取方式才是真正的每次新建。

7.3 排查Bean创建异常的通用套路

最后分享一个排错方法。遇到"BeanCreationException"这类问题时,我的排查路径大概是这样:

第一,看异常栈里是否有循环依赖提示。Spring的报错信息很明确,如果是"The dependencies of some of the beans in the application context form a cycle",直接看循环链路在哪个类。

第二,看是不是构造器注入导致的循环依赖无法解决。如上文所述,构造器循环依赖是死结,必须重构。

第三,排查是否有@PostConstruct抛出异常。initializeBean阶段任何异常都会被包装成BeanCreationException,里层cause会告诉你具体是哪一步炸的。

第四,检查BeanDefinition是否注册成功。有时候你在配置类里定义Bean时写错了方法签名,比如@Bean方法方法名写错导致返回类型不匹配,容器根本注册不了这个Bean,但报错信息可能不如你想的那么直观。

这套排查链路我几乎每次都能用上。说白了,Spring的运行时诊断信息已经很完善了,剩下的就是你脑子里有没有完整的生命周期模型去解读这些信息。希望这篇文章能帮你把这条链路建立起来——下次再看到异常,不要再停留在"网上去搜中文翻译"这一步了。

返回列表