最近在维护一个“祖传”的 SSM + JSP 老项目,本来以为顺着老代码的葫芦画瓢就是分分钟的事,结果在“循环依赖”和“AOP 代理”这两个老演员身上,我结结实实地卡了半天。
今天就把这段从“疯狂报错”到“底层顿悟”的真实踩坑记录写下来,希望能帮到同样在维护老项目的小伙伴们!
🚨 第一回合:AOP 代理引发的血案
项目的基本情况是这样的:XML 配置了 default-autowire="byName" 且 default-lazy-init="false",另外通过 <aop:config> 给 *ServiceImpl 配置了事务代理。
这天启动项目,核弹来了: 控制台直接炸出 BeanCurrentlyInCreationException!💥
❓ 为什么翻车了?
我顺着堆栈一查,发现了一个完美的循环依赖链 + 代理冲突:
起点触发:Spring 启动,开始创建
bpmCommonService。连环注入:因为
byName自动注入,它触发了属性bpmPowerService的创建。形成闭环:而
BpmPowerServiceSpringImpl里恰好又有一个bpmCommonService属性!提前暴露:为了打破循环,Spring 提前把尚未完成 AOP 代理的“原始版本”的
bpmCommonService注入给了bpmPowerService。💥 冲突爆发:等
bpmCommonService走完全部生命周期,最终被 AOP 包装成“代理版本”时,Spring 回头一查,发现bpmPowerService手里拿着的还是那个“原始版本”!Spring 大怒:“你引用的对象跟我最终生成的对象不一致!”,直接抛异常罢工。
🤔 第二回合:天真的 lazy-init 标签
痛定思痛,我想到了最经典的解法:打破循环链!
只要让被 AOP 包装的那个 bean 延迟初始化,让 Spring 先完整走完别人的流程就行了。于是我微微一笑,直接去 XML 里给 bpmCommonService 加上了 lazy-init="true":
XML
<bean id="bpmCommonService"
class="cn.com.cis.undwrt.bpm.service.spring.BpmCommonServiceSpringImpl" lazy-init="true">
</bean>
我当时在心里疯狂推演:只要延迟加载,就不会在启动时互相等待,完美!
然而,重启后再次翻车,异常依旧!💥
❓ 为什么又失败了?
因为延迟加载是会被传染和破坏的。
整个项目的 XML 配置了 default-autowire="byName",如果有一个非 lazy 的 Bean(比如系统启动必需的其他 Service)在启动时需要注入 bpmCommonService,Spring 为了满足它,还是会被迫提前去实例化我们的 bpmCommonService。这就导致 lazy-init 标签彻底形同虚设,循环依赖依然在启动期爆发。
💡 终极回合:顿悟!让 ApplicationContext 拯救世界
盯着屏幕抓头发的我,突然想到一个绝妙的思路:
既然启动期不管怎么配都会被自动注入强行拉起,那我能不能彻底切断启动期的属性注入,把获取 Bean 的时机推迟到运行时?
我决定抛弃依靠 XML byName 自动注入的做法,改为降维打击:直接引入 ApplicationContext 容器本身!
按照这个思路重新推演:
启动期解耦:在
BpmPowerServiceSpringImpl中,我不再保留bpmCommonService属性让 Spring 去自动注入,而是直接注入ApplicationContext容器。完美包装:因为切断了循环链,Spring 在启动时可以安安稳稳地把
bpmCommonService实例化,并从容地走完 AOP 代理流程,最终把那个“代理版本”放进单例池。避开死锁:到了运行期,真正的业务代码执行时,我再通过
applicationContext.getBean("bpmCommonService")拿到它。此时拿到的,就是 100% 经历过完整 AOP 包装的最终代理对象,没有任何冲突死角!
💻 落地实战:优雅的代码实现
思路理顺了,下面放上我项目里改造的真实核心逻辑。代码改动极小,效果立竿见影。
核心操作:实现 ApplicationContextAware 接口
Java
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.beans.BeansException;
public class BpmPowerServiceSpringImpl implements ApplicationContextAware {
// 灵魂属性:存放容器上下文
private ApplicationContext applicationContext;
// 骚操作:利用接口回调,让 Spring 在启动时把容器自己送过来
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 具体的业务方法
*/
public void doPowerCheck() {
// 划重点!!!
// 运行时按需获取,彻底绕开启动期的循环依赖与 AOP 代理冲突!
BpmCommonServiceSpringImpl bpmCommonService =
(BpmCommonServiceSpringImpl) applicationContext.getBean("bpmCommonService");
// 接下来就可以白嫖代理对象的方法了,事务完全生效
bpmCommonService.doSomething();
}
}
把导致循环依赖的旧属性删掉,整个世界都清净了。
📝 写在最后
做完这个模块最大的感触就是:开发中如果顺着一条路走死了(比如死磕 lazy-init 标签),不妨跳出固有的思维框架,利用时间差进行降维打击。
将“启动时依赖”转化为“运行时获取”,这套 getBean() 的解法完美避开了 AOP 早期引用与最终代理不一致的死结。虽然从重构的角度看,彻底梳理业务解耦才是最终之道,但在维护历史包袱沉重的老系统时,这绝对是风险最小、最立竿见影的救火神技。