ARTICLE DETAIL

资讯详情

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

Spring Bean作用域与生命周期:prototype注入失效到三级缓存全解析

Spring Bean作用域与生命周期:prototype注入失效到三级缓存全解析 先问你一个平时最容易踩中、但看文档又总是被一笔带过的问题在Spring Boot项目里我把一个业务Bean的作用域改成了prototype然后在单例的Service里注入它。结果不管调用多少次拿到的都是同一个实例跟预期完全相反。这个问题我在代码评审和线上问题排查里遇到过不下十次我自己的第一个Spring项目也因此踩过坑。说到底它牵扯的正是Spring Bean作用域与生命周期的协同机制作用域决定了Bean什么时候被创建、能存在多久而依赖注入的时机又决定了单例持有的是那个Bean的哪一个“版本”。这篇内容我会把这两件事彻底撕开。从五种内置作用域的实际表现到实例化、属性填充、初始化、销毁的每个环节再延伸到很多人面试背过但没真做过的三级缓存。适合准备面试的Java开发也适合正在被“单例里注入了却怎么也拿不到新对象”这种问题逼疯的排查者。所有结论都来自实际项目验证不是背文档。1. 一个经典事故prototype被注入单例后为什么还是同一个1.1 事故现场与排查过程当时的情况很简单一个定时任务线程池里每次执行任务都需要一个全新的TaskContext对象用来隔离任务之间的临时状态。代码里是这样写的Component public class TaskExecutor { Autowired private TaskContext taskContext; public void execute() { taskContext.reset(); taskContext.doWork(); } }TaskContext上标了Scope(prototype)从定义上来说每次从容器获取都应该得到一个新实例。但线上日志里使用System.identityHashCode打印出来的实例地址却始终一样任务A写进去的数据能被任务B读到典型的线程串数据事故。排查的时候我第一反应是“是不是注解没生效”检查了半天发现作用域配置没问题直接Autowired确实也能注入。问题到底出在哪1.2 原因注入时机比作用域更早这里就要把Bean的生命周期和作用域放在一起看了。Spring容器在启动时创建单例Bean也就是TaskExecutor这个单例。创建单例的过程中容器要处理它的依赖于是触发了一次getBean(taskContext)从容器里取了一个prototype实例注入进去。关键就在这里prototype的作用是“每次获取时创建一个新实例”但依赖注入这个“获取动作”只发生了一次发生在容器启动阶段。之后整个应用生命周期里你拿到的都是TaskExecutor对象里保存的那个固定引用。这也是为什么“日志里每过几秒调用一次地址还是一样的”——不是prototype失效了而是容器在启动时只取了一次后面你自己根本没有再向容器索取过。1.3 这件事说明的两条主线这个事故透露出两个必须搞清楚的机制。作用域决定的是“什么时候创建”和“存活多久”它依赖容器的获取动作才能体现出来。生命周期决定的是“一个Bean从创建到销毁会走哪些标准流程”它跟Bean内部逻辑相关和注入方的关系更大。很多人把这个事故简单归因为“Spring的prototype是假的”但实际是没理解作用域和生命周期的配合。我在后面的章节里会反复用这个场景举例因为它是理解所有后续概念的最好入口。2. 五种内置作用域使用边界与容易踩的坑2.1 从默认的singleton说起Spring默认的Bean作用域是singleton但这里要强调一个细节它指的是“每个Spring容器内只有一个实例”不是Java世界里那种全局单例。你如果在一个进程里起了两个Spring容器比如多个父子上下文的应用同一个类在两个容器里分别有一个实例这并不违反singleton的定义。singleton的实现核心是容器里那张缓存表。更具体一点DefaultSingletonBeanRegistry维护了一个ConcurrentHashMapString, Object也就是我们常说的“一级缓存”。普通单例在容器启动阶段就会被创建并放进缓存之后每次getBean都直接从缓存取。所以单例Bean适合无状态场景比如Service、DAO、配置类它们本身不保存业务数据放多少份都没有意义。Component public class OrderService { // 默认就是单例不用显式加 Scope }如果你的单例Bean是有状态的要小心并发问题。比如一个实体对象被注册成singleton在高并发下它的字段就会变成共享变量这在业务代码里几乎是一种灾难。2.2 prototype有状态短生命周期的正确用法prototype的作用域语义是每次从容器获取都创建全新实例。Spring在doGetBean里对prototype的处理逻辑很简单创建完对象后不会放入缓存直接返回给调用方。销毁管理与singleton完全不同容器不持有prototype实例的引用所以destroy回调不会触发PreDestroy、DisposableBean在prototype Bean上都不会起作用。实际开发里prototype适合那些“每次使用都要重新初始化、且用完就丢”的对象。比如上面提到的任务上下文再比如一个复杂的查询过滤器、离线导出任务参数对象。需要注意的是虽然容器不缓存prototype实例但它仍然会走完整的实例化和初始化流程也就是说Autowired字段注入、PostConstruct初始化方法都会执行。这一点很多人搞混以为prototype就是裸的new对象完全不是。Component Scope(prototype) public class ReportContext { Autowired private ReportConfig config; PostConstruct public void init() { System.out.println(prototype 实例也会执行初始化); } }2.3 request、session、application与websocket这几个作用域必须在Web环境中才有效。它们背后的实现都依赖一个核心接口Scope本质上是通过特定容器对象比如HttpServletRequest、HttpSession、ServletContext作为存储载体把Bean实例存进去。平时不需要手动存Spring已经封装好了。request每个HTTP请求一个实例请求结束就销毁。适合放请求级状态比如某个请求内的临时数据聚合对象。session同一个用户会话共享一个实例会话失效时销毁。适合放登录态、用户偏好类的数据。application整个ServletContext范围内共享一个实例相当于Web应用级的单例。websocketSpring 4.2开始支持每个WebSocket会话一个实例。这几个作用域有一个共同的大坑如果你在单例Bean里直接注入它们就会遇到和prototype一样的“拿不到新对象”问题。而且因为request作用域的Bean背后绑定的是当前线程对应的请求对象一旦注入到了单例里这个请求结束之后对象就被销毁了你再调用它的方法很可能拿到一个已经失效的状态。解决办法和prototype场景类似我在第5章会集中展开。2.4 自定义Scope一个很少被用到但很实用的能力Spring允许你自己实现Scope接口把Bean实例存储到自定义载体上。比如我想做一个“基于ThreadLocal的作用域”让同一个线程内拿到同一个Bean实例不同线程之间完全隔离又比如我想把某个Bean实例绑定到数据库事务或慢请求链路上。做法是写一个实现类注册到ConfigurableBeanFactory然后在Bean定义上用Scope(scopeName myThread, proxyMode ...)指定。public class MyThreadScope implements Scope { private final ThreadLocalMapString, Object threadLocal ThreadLocal.withInitial(HashMap::new); Override public Object get(String name, ObjectFactory? objectFactory) { return threadLocal.get().computeIfAbsent(name, k - objectFactory.getObject()); } // 省略 remove、registerDestructionCallback、getConversationId 等方法 }自定义Scope的场景虽然不常见但一旦遇到“状态按线程隔离”或者“状态按事务隔离”的需求它是比ThreadLocal手动管理优雅得多的方案而且能天然和Spring的代理机制配合。如果你所在的公司有中间件开发需求这个能力迟早会用到。3. Bean生命周期全景图从实例化到销毁的每个关键节点3.1 一张表和一条主线很多文章讲生命周期喜欢从PostConstruct讲起但真实情况要早得多。从一个Bean被容器认识到最终被销毁完整链路如下关键阶段触发节点主要回调与处理BeanDefinition准备容器解析配置类/XML/注解生成BeanDefinition合并属性实例化前AbstractAutowireCapableBeanFactoryInstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation实例化构造器推断默认无参构造/工厂方法/Autowired构造器实例化后属性填充之前postProcessAfterInstantiation可控制是否继续填充属性属性填充populateBeanAutowired、Resource、Value在这里注入Aware回调初始化前BeanNameAware、BeanFactoryAware、ApplicationContextAware初始化前initializeBeanBeanPostProcessor.postProcessBeforeInitializationPostConstruct在这里初始化invokeInitMethodsInitializingBean.afterPropertiesSet、自定义init-method初始化后applyBeanPostProcessorsAfterInitializationAOP代理通常在这里生成使用容器持有引用实际业务调用销毁前容器关闭时PreDestroy销毁destroyBeansDisposableBean.destroy、自定义destroy-method3.2 实例化阶段比想象中更早的干预点大多数人以为Spring就是通过反射newInstance()构造Bean但实例化之前Spring给了你一个干预机会InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation。这个方法如果返回了一个非空对象Spring就会直接用这个对象作为最终的Bean不再走后续的反射构造流程。很多框架级功能就是靠这一点实现的比如MyBatis的Mapper接口代理它会在这个环节直接返回JDK动态代理对象根本不需要真实的实现类。Component public class MyInstantiationProcessor implements InstantiationAwareBeanPostProcessor { Override public Object postProcessBeforeInstantiation(Class? beanClass, String beanName) { if (beanName.equals(specialBean)) { return new ProxyFactory(beanClass).getProxy(); } return null; } }然后才是真正的实例化。Spring会做构造器推断优先使用Autowired标注的构造器没有标注就按“有无参构造用无参构造有多参构造选能匹配参数的”。这一步还要处理循环引用的问题我把这部分放到第6章单独讲。3.3 属性填充依赖注入真正发生的地方实例化完成之后populateBean方法开始执行之前标注在字段上的Autowired、Required、Value都会在这一步解析并完成赋值。这里的核心处理器是AutowiredAnnotationBeanPostProcessor它扫描Bean类中的Autowired和Value注解然后调用getBean逐个解析依赖。这里有个值得注意的细节属性填充阶段也受InstantiationAwareBeanPostProcessor影响。如果你在postProcessAfterInstantiation里返回falseSpring会跳过整个属性填充阶段不再自动注入任何字段。这在某些特殊场景里有价值比如你想完全接管一个Bean的初始化不想要Spring插手普通字段注入。属性填充完毕Aware回调就开始执行了。如果Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware等接口容器会把对应的名称、工厂或上下文传给Bean。这个过程发生在初始化前意味着你在PostConstruct方法里已经可以安全使用这些容器资源。3.4 初始化阶段三种方式的执行顺序初始化阶段是生命周期里最容易被面试官追问细节的地方。它的顺序是固定的如果你想用一个Bean同时验证可以像这样打标记Component public class LifecycleDemoBean implements InitializingBean, BeanNameAware { public LifecycleDemoBean() { System.out.println(1. 构造器); } Override public void setBeanName(String name) { System.out.println(2. setBeanName Aware); } PostConstruct public void postConstruct() { System.out.println(3. PostConstruct); } Override public void afterPropertiesSet() { System.out.println(4. afterPropertiesSet); } public void customInit() { System.out.println(5. init-method); } }如果XML或配置类里把customInit配成init-method输出顺序一定是构造器、Aware、PostConstruct、afterPropertiesSet、init-method。这套顺序不是谁拍脑袋定的而是AbstractAutowireCapableBeanFactory.initializeBean方法里的硬编码逻辑。3.5 销毁阶段容器关闭时的逆向操作销毁阶段发生在容器关闭时。Spring会遍历所有已注册的单例Bean执行销毁回调顺序和初始化完全相反先PreDestroy然后DisposableBean.destroy最后自定义destroy-method。这一点很符合直觉就像折返跑先初始化后销毁后初始化的先销毁力保资源释放顺序可控。但需要重点提一句这个流程无法覆盖prototype Bean。因为容器根本不持有prototype实例的缓存引用它在返回实例的那一刻就已经放弃管理了。所以如果你的prototype Bean占用了连接资源得自己想办法回收比如用工厂方法模式在业务代码里手动调用close()。这一点我在第7章还会再次强调。4. 多个初始化/销毁回调的优先级与冲突源码顺序说了算4.1 PostConstruct是如何被触发的很多人的误区是认为PostConstruct是Spring的“亲儿子”实际上它只是借用了一下BeanPostProcessor的机制。JDK自己就提供了PostConstruct和PreDestroy注解Spring的CommonAnnotationBeanPostProcessor把这些注解翻译成了生命周期回调。具体来说postProcessBeforeInitialization方法会在initializeBean过程中被调用此时它发现Bean方法上有PostConstruct就通过反射执行。也就是说PostConstruct实际是“初始化前BeanPostProcessor里的执行动作”它天然排在InitializingBean.afterPropertiesSet之前。同理PreDestroy是通过postProcessBeforeDestruction实现的。了解这个底层关系你就知道为什么网上说的“PostConstruct先于afterPropertiesSet”永远是对的。4.2 同时存在多个回调时谁生效还是都会执行都有。这一点必须说清楚一个Bean可以同时实现InitializingBean、在方法上标PostConstruct、又在配置里写了init-method三个回调都会执行而且顺序固定。不是“二选一”或“后面的覆盖前面”是全部执行。这个设定本身没什么问题但它会带来维护隐患。我曾经在一个老项目里见到一个Bean同时用了三种初始化方式里面还夹杂了AOP子类代理排查问题时完全分不清“哪份状态是哪一层初始化写入的”。所以我强烈建议团队统一规范要么全部用PostConstruct要么全部用InitializingBean。前者适合现代Spring Boot项目因为不需要让业务类实现Spring接口后者适合需要明确语义、又不想依赖注解扫描的底层框架代码。4.3 回调一旦异常会怎么样初始化回调如果抛出异常这个Bean会被判定为创建失败。如果PostConstruct里抛异常容器会抛出BeanCreationException把这个Bean相关的早期引用一并清理。这一点经常被忽略很多人在PostConstruct里做数据库连接或远程调用一旦失败整个应用启动就会中断。这不能算Spring的问题它本来就是一种保护不让你在一个“初始化失败”的状态下正常对外提供服务。销毁回调抛异常更要小心。容器在关闭时会继续执行其他Bean的销毁但日志中可能只会看到一个异常堆栈掩盖了真正的资源泄漏问题。我在线上排障时习惯在PreDestroy里用try-finally包住资源关闭逻辑保证即使关闭动作本身出了问题也能把关键数据先打出来。4.4 BeanPostProcessor对prototype Bean也生效吗生效。这点和第2章提到的“prototype不缓存”很容易混淆。prototype Bean虽然不缓存实例但在它被创建的那一次仍然会走完整的initializeBean流程所以BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization都会执行。换句话说实例化和初始化回调对prototype生效销毁回调不生效。如果在BeanPostProcessor.postProcessAfterInitialization里返回nullSpring会结束当前Bean的处理流程不再执行后续的初始化后逻辑。很多AOP框架判断“当前Bean不需要代理”时就是通过返回null来短路处理的。但要注意这个短路只是针对“后置处理”前面的属性填充和初始化已经完成了。5. 作用域失效的经典修复代理模式与延迟查找的真相5.1 为什么单例注入原型一定失效第1章已经说明了此事的原因。现在从生命周期角度再确认一次单例Bean A在创建时需要完成属性注入所以Spring会执行getBean(B)B虽然是prototype但一次获取动作只会得到一个实例这个实例随后被固定在A的字段里。之后A的整个生命周期中字段都指向最初那个固定实例。每次调用A.b得到同一个实例是生命周期和依赖注入顺序共同导致的结果不是作用域语义出错。明白原理之后解决方案就很清晰了要么让A每次去容器里主动获取B要么让A持有的不是一个B实例而是一个“能够每次取到新B实例”的代理对象。5.2 方案一ObjectProvider延迟查找Spring 4.3以后提供了ObjectProvider这是现代化项目里最推荐的做法。把注入点改成ObjectProviderB相当于注入了一个“工厂入口”。每次调用getObject()都会重新触发容器的getBean逻辑从而拿到新的prototype实例。Component public class SingletonA { private final ObjectProviderPrototypeB prototypeProvider; public SingletonA(ObjectProviderPrototypeB prototypeProvider) { this.prototypeProvider prototypeProvider; } public void doWork() { PrototypeB b prototypeProvider.getObject(); b.run(); } }这个方案的好处是显式、可控。你一眼就能看出来“这里不是在用同一个B而是每次拿新的”。它的缺点是调用方需要感知注入类型的变化不再是透明的字段注入。如果你的团队希望业务代码清爽那就要考虑代理方案。5.3 方案二Lookup方法注入Lookup是Spring提供的方法级注入方式通常配合抽象方法使用。Spring会通过CGLIB为当前Bean生成一个子类在子类里覆盖这个方法方法里执行getBean获取最新实例。Component public class SingletonA { public void doWork() { PrototypeB b getPrototypeB(); b.run(); } Lookup protected PrototypeB getPrototypeB() { return null; } }注意两个使用边界方法不能是private类不能是final。因为Spring需要生成子类来覆盖方法private和final都会破坏CGLIB子类化。这个方案适合不想引入ObjectProvider字段、又想动态获取场景Bean的情况。5.4 方案三作用域代理 ScopedProxyMode另一种常见方式是给prototype加一个代理属性。Component Scope(value prototype, proxyMode ScopedProxyMode.TARGET_CLASS) public class PrototypeB { public void run() {} }这样注入到其他单例中的其实是一个CGLIB代理对象。这个代理被模拟成B的类型但内部逻辑是“每次调用代理方法先从容器获取一个真实的B实例再把方法调用转发给它”。效果上调用方完全感受不到变化像在操作一个普通Bean。但这个方案也埋了几个坑。代理对象不等于真实对象。你用a instanceof PrototypeB可能是true但a.getClass()返回的是CGLIB子类。this调用内部方法时不会经过代理。在实际开发里你用B自己的方法内部调用另一个方法另一个方法不会触发代理逻辑。final方法和final类无法被CGLIB代理。序列化时非常容易踩坑代理对象要实现writeReplace才有机会被正确序列化。调试时idea看到的是奇怪的CGLIB类名初次接触的人会以为是框架bug。5.5 五种方案对比与选型建议方案使用难度对调用方是否透明适用场景ObjectProvider低否业务代码获取新实例Lookup方法注入中基本透明需要无参方法获取新实例ScopedProxyMode中是request/session作用域跨Bean注入ApplicationContext手动getBean低否非容器管理的特殊入口构造器里直接new低否不影响Spring生命周期管理的纯临时对象我的个人建议是业务系统优先用ObjectProvider因为显式、好排查框架或组件开发优先用Lookup因为可以在抽象方法上定义语义只有request/session作用域才优先考虑ScopedProxyMode因为那些场景几乎必须让单例透明地获取请求级状态。6. 生命周期与三级缓存循环依赖为何只在单例下成立6.1 循环依赖的本质与默认结局假如有A和B两个单例BeanA依赖BB依赖A。Spring容器在创建A时执行属性填充发现需要B于是去创建B创建B时执行属性填充发现需要A又回头找A。如果没有额外的缓存机制这会形成死循环直到栈溢出。Spring解决这个问题的核心思路是“提前暴露”在A的实例化完成之后、属性填充开始之前先让A暴露一个早期引用使得B在创建时可以拿到这个尚未初始化完成的A。等下那三级缓存到底是怎么配合的6.2 三级缓存的完整协作Spring容器里维护了三张Map也就是“三级缓存”。缓存名称存储内容何时放入singletonObjects 一级缓存完整Bean实例创建完成后earlySingletonObjects 二级缓存早期暴露的Bean实例从三级缓存解析后singletonFactories 三级缓存ObjectFactory对象实例化完成后立即放入创建A的过程遇到循环依赖时实例化A对象但尚未属性填充。把A对应的ObjectFactory放入三级缓存。这个工厂的作用是返回一个“早期引用”。开始属性填充发现需要B调用getBean(b)去创建B。B实例化完成属性填充时发现需要A调用getBean(a)。一级缓存里没有A二级缓存里也没有A但三级缓存里有A的ObjectFactory。把A早期引用从工厂里取出来放入二级缓存然后交给B注入。B的依赖注入完成B进入完整初始化最终放入一级缓存。回到A的创建流程因为A已经通过早期引用被B持有所以A此时可以继续完成属性填充、初始化和最终放入一级缓存。有一个细节很容易忽略早期引用进入二级缓存时如果此时A需要AOP代理那么从三级缓存的ObjectFactory里取出来的就不是A原始对象而是A的代理对象。这一步保证了B最终依赖到的是符合AOP增强规则的版本不会出现“B里拿到的是没代理的裸A”。6.3 为什么prototype循环依赖一定失败现在回到prototype。Spring在处理prototype时只负责创建实例并返回根本不会放入三级缓存也不会维护早期引用。当prototype的A依赖prototype的B、B又依赖A时容器每创建一个新实例都找不到对方缓存于是循环下去最终抛BeanCurrentlyInCreationException。这其实是一道很不错的面试判断题循环依赖能不能解决不取决于Bean内部写法而取决于作用域有没有使用三级缓存机制。singleton可以prototype直接报错。6.4 三级缓存救不了的几种场景三级缓存也非万能下面这些情况仍然会报循环依赖错误。构造器注入循环依赖因为构造器在实例化阶段就需要依赖参数此时Bean还没完成实例化无法提前暴露“早期引用”。面对这种问题换成Autowired字段注入或setter注入通常就能解决。Async标注的Bean异步处理会通过AsyncAnnotationBeanPostProcessor生成代理这个代理是在初始化后创建的而三级缓存暴露的是早期引用。如果依赖提前发生就可能拿到未增强的原始对象结果A和AOP代理对不上于是Spring为了安全起见直接拒绝。Configuration类中有特殊增强逻辑的Bean配置类是CGLIB增强对象三级缓存对它也有类似的兼容约束。处理构造器注入循环依赖时最简单的方案是加Lazy。Lazy会生成一个懒加载代理对象注入进去代理延迟到真正调用时才触发获取Bean相当于把“启动期的循环”推迟到了“运行期的正常获取”也能绕开问题。注意这跟作用域没有关系Lazy不是把Bean改成懒加载作用域只是把依赖的获取时机后移。7. 高频盲点与实操建议面试、评审和排障视角7.1 关于作用域与生命周期的几个高频误区我在面试别人和整理团队training材料时发现下面这些误区反复出现它们也值得成为你自查的清单。误区事实prototype Bean也会执行destroy方法不会。容器创建时执行初始化回调但不缓存销毁回调不触发singleton Bean是JVM单例不是。每个Spring容器一份多个上下文就是多个实例Lazy就是把Bean改成prototype不是。Lazy只是给依赖生成代理延后获取作用域没变PostConstruct是Spring独有不是。它是JDK标准注解Spring只是利用BeanPostProcessor机制触发request作用域Bean能直接注入单例不能。需要proxyMode或ObjectProvidersingleton Bean构造器里做了远程调用可以有但不推荐。一旦失败整个容器启动失败资源也容易泄漏7.2 代码评审视角什么时候要考虑作用域日常业务系统里90%以上的Bean都应该是无状态单例。需要认真考虑作用域的场景通常有信号可言。一个Bean内部持有可变的业务状态又不希望线程之间共享优先考虑prototype或者把状态移到方法参数上。一个Bean需要绑定到HTTP请求链路比如当前用户的租户信息、请求追踪ID优先考虑request作用域配合代理注入。一个Bean是重量级资源封装比如线程池、连接池、缓存管理器必须是singleton绝不能用prototype否则每次获取都新建池子系统性能直接被拖垮。一个Bean需要按用户会话隔离session作用域但我一般建议尽量少用避免会话内长期持有大对象造成内存压力。评审时如果看到有人拿prototype当“并发隔离”方案我会特别警惕。因为prototype只是保证“每次获取新对象”但如果Bean内部依赖了某个单例共享状态新对象依然会读取旧数据。正确做法是先问到底要隔离的是“实例本身”还是“实例内部依赖的共享状态”。7.3 生命周期相关问题的快速定位方法排查生命周期问题我的土办法一直是“在关键节点埋点日志”。不必上来就翻源码可以在项目中加一个全局的BeanPostProcessor把所有BeanName和当前阶段打出来Component public class LifecycleLoggingProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) { System.out.println([before init] beanName - bean.getClass().getSimpleName()); return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) { System.out.println([after init] beanName - bean.getClass().getSimpleName()); return bean; } }启动应用后你就能看到每个Bean初始化的实际顺序。遇到“某个字段明明标注了Autowired却还是null”很多情况都是因为该Bean在某个BeanPostProcessor里被提前替换了比如MyBatis的MapperFactoryBean、或者你项目里的自定义后处理器。顺着日志看它是在beforeInit阶段还是afterInit阶段被替换的能省下一大半排查时间。再补充一个跟生命周期相关的排查技巧当你在PostConstruct或者InitializingBean.afterPropertiesSet里做初始化但是初始化依赖另一个Bean而这个Bean的初始化尚未完成时就会抛出BeanCurrentlyInCreationException。这说明依赖方向反了应该把初始化逻辑挪到依赖方的字段注入完成之后或者改用ApplicationListenerContextRefreshedEvent再处理。7.4 资源清理prototype Bean谁能帮我close前文反复强调prototype Bean不触发销毁回调那它占用的连接、文件句柄怎么释放三个方向。工厂模式创造一个工厂类由它负责创建prototype Bean并在业务代码显式调用close。包装成内部资源不直接让prototype Bean承载资源而是让它持有某个可关闭的组件业务代码结束后调用组件close。使用Transactional等切面切面可以在方法结束后做统一清理但前提是方法边界清晰。我个人更推荐第一个方向因为它是显式的。如果一个Bean被标记为prototype团队所有人看到时就要默认“它的销毁由使用方负责”。这比把销毁逻辑藏在“以为会执行但实际不执行”的PreDestroy里可靠得多。尾声我处理这个问题时的一个习惯前阵子帮一个同事排查线上问题看到他把PreDestroy写在prototype Bean上觉得容器关闭时会自动清理连接池。实际上这个Bean根本不会被容器管理销毁结果就是每次任务结束连接都不释放最终数据库连接被打满。这个例子让我更加确定一个习惯每次给Bean选作用域时我会顺手把它的销毁责任写在注释里比如“本Bean为prototype调用方负责close”。刚开始团队觉得我啰嗦后来线上少了好几个连接泄漏问题就没人反对了。如果你对Spring的作用域和生命周期还有其他困惑我的建议很简单别只停留在“背结论”层面把生命周期里每个回调都打印出来看一遍把三级缓存代码断点走一遍亲眼看一次prototype不缓存实例的结果这些信息量比任何文档都有效。Spring的机制并不神秘它只是把“什么时候创建、什么时候注入、什么时候销毁”这几件事用一套可预测的流程固定了下来理解到这一层你就不会再被那些“莫名奇妙的反常现象”骗到了。
返回列表