实战复盘:解决老 SSH 项目的循环依赖与 AOP 代理冲突

实战复盘:解决老 SSH 项目的循环依赖与 AOP 代理冲突

_

最近在维护一个“祖传”的 SSM + JSP 老项目,本来以为顺着老代码的葫芦画瓢就是分分钟的事,结果在“循环依赖”和“AOP 代理”这两个老演员身上,我结结实实地卡了半天。

今天就把这段从“疯狂报错”到“底层顿悟”的真实踩坑记录写下来,希望能帮到同样在维护老项目的小伙伴们!

🚨 第一回合:AOP 代理引发的血案

项目的基本情况是这样的:XML 配置了 default-autowire="byName"default-lazy-init="false",另外通过 <aop:config>*ServiceImpl 配置了事务代理。

这天启动项目,核弹来了: 控制台直接炸出 BeanCurrentlyInCreationException!💥

❓ 为什么翻车了?

我顺着堆栈一查,发现了一个完美的循环依赖链 + 代理冲突

  1. 起点触发:Spring 启动,开始创建 bpmCommonService

  2. 连环注入:因为 byName 自动注入,它触发了属性 bpmPowerService 的创建。

  3. 形成闭环:而 BpmPowerServiceSpringImpl 里恰好又有一个 bpmCommonService 属性!

  4. 提前暴露:为了打破循环,Spring 提前把尚未完成 AOP 代理的“原始版本”的 bpmCommonService 注入给了 bpmPowerService

  5. 💥 冲突爆发:等 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 容器本身!

按照这个思路重新推演:

  1. 启动期解耦:在 BpmPowerServiceSpringImpl 中,我不再保留 bpmCommonService 属性让 Spring 去自动注入,而是直接注入 ApplicationContext 容器。

  2. 完美包装:因为切断了循环链,Spring 在启动时可以安安稳稳地把 bpmCommonService 实例化,并从容地走完 AOP 代理流程,最终把那个“代理版本”放进单例池。

  3. 避开死锁:到了运行期,真正的业务代码执行时,我再通过 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 早期引用与最终代理不一致的死结。虽然从重构的角度看,彻底梳理业务解耦才是最终之道,但在维护历史包袱沉重的老系统时,这绝对是风险最小、最立竿见影的救火神技。

WebLogic 12c 入门安装踩坑记录:解决配置向导闪退与路径解析报错 2026-07-20
SSH练手项目踩坑记录:Struts2接收OkHttp发送的JSON数据报空指针异常 2026-08-02

评论区