SpringBean之间的循环依赖,你头疼了吗?
循环依赖---circular dependency & circular reference
java库之间的循环依赖,即java包循环依赖,指的是当两个或多个jar文件直接或间接地相互依赖(circular dependency)。循环依赖关系可能会在应用程序的编译或运行时导致问题,并且很难解决。
类之间的循环依赖指的的A依赖B,同时,B也依赖A。spring容器中当两个或多个bean之间存在相互依赖(相互注入对方,circular reference)时,就会发生循环依赖。例如A→B同时B→A,或例如间接的A→C→B同时B→A,两种情况均会产生A与B的相互依赖。
本文讲后者,即java bean的循环依赖
程序一旦出现循环依赖,则在服务启动时,会概率性报错:Spring尝试创建bean时,它会陷入无限递归中,进而无法完成应用程序的刷新,服务启动失败。报错信息如下:
- Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'payUserSignBizService': Bean with name 'payUserSignBizService' has been injected into other beans [payMerchantAgreementApiImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
百度翻译:-上下文初始化期间遇到异常-取消刷新尝试:org.springframework.beans.factory.BeanCurrentlyInCreationException:创建名为“payUserSignBizService”的bean时出错:名为“PayUserSignBizService”的bean已作为循环引用的一部分以其原始版本注入到其他bean[payMerchantAgreementApiImpl]中,但最终已被包装。这意味着所述其他bean不使用bean的最终版本。这通常是过度渴望类型匹配的结果——例如,考虑在关闭“allowEagleInit”标志的情况下使用“getBeanNameOfType”。
应用启动log如下(信息量大,并附加注释,折叠):

1 #服务启动日志---> 2 ... 3 2023-07-20 22:04:57.427 [TID:N/A] [] [main] INFO c.c.f.a.spring.annotation.SpringValueProcessor:120 - Monitoring key: levy.public.key, beanName: levyCommunicationUtil, method: com.emax.channel.provider.modules.serviceprovider.util.LevyCommunicationUtil.setPlatPublicKey 4 #前面都是INFO,下面出现WARN(检测到循环依赖) 5 2023-07-20 22:04:58.023 [TID:N/A] [] [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext:557 - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'payUserSignBizService': Bean with name 'payUserSignBizService' has been injected into other beans [payMerchantAgreementApiImpl] in its raw version aspart of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example. 6 #程序相关的各种组件开始shutdown/destroy ,如:nacos/dubbo /线程池 /hikari数据库连接 7 2023-07-20 22:04:58.028 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:147 - com.alibaba.nacos.client.naming.beat.BeatReactor do shutdown begin 8 2023-07-20 22:04:58.030 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:149 - com.alibaba.nacos.client.naming.beat.BeatReactor do shutdown stop 9 2023-07-20 22:04:58.030 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:149 - com.alibaba.nacos.client.naming.core.EventDispatcher do shutdown begin 10 2023-07-20 22:05:01.031 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:152 - com.alibaba.nacos.client.naming.core.EventDispatcher do shutdown stop 11 2023-07-20 22:05:01.032 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:370 - com.alibaba.nacos.client.naming.core.HostReactor do shutdown begin 12 2023-07-20 22:05:01.032 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:118 - com.alibaba.nacos.client.naming.core.PushReceiver do shutdownbegin 13 2023-07-20 22:05:04.033 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:122 - com.alibaba.nacos.client.naming.core.PushReceiver do shutdownstop 14 2023-07-20 22:05:04.033 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:132 - com.alibaba.nacos.client.naming.backups.FailoverReactor do shutdown begin 15 2023-07-20 22:05:04.033 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:134 - com.alibaba.nacos.client.naming.backups.FailoverReactor do shutdown stop 16 2023-07-20 22:05:04.034 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:374 - com.alibaba.nacos.client.naming.core.HostReactor do shutdown stop 17 2023-07-20 22:05:04.034 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:715 - com.alibaba.nacos.client.naming.net.NamingProxy do shutdown begin 18 2023-07-20 22:05:04.034 [TID:N/A] [] [main] WARN com.alibaba.nacos.client.naming:72 - [NamingHttpClientManager] Start destroying NacosRestTemplate 19 2023-07-20 22:05:04.035 [TID:N/A] [] [main] WARN com.alibaba.nacos.client.naming:79 - [NamingHttpClientManager] Destruction of the end 20 2023-07-20 22:05:04.035 [TID:N/A] [] [main] INFO com.alibaba.nacos.client.naming:718 - com.alibaba.nacos.client.naming.net.NamingProxy do shutdown stop 21 2023-07-20 22:05:04.062 [TID:N/A] [] [main] INFO o.s.scheduling.concurrent.ThreadPoolTaskExecutor:208 - Shutting down ExecutorService 22 2023-07-20 22:05:04.072 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy17@157ff8f8 was destroying! 23 2023-07-20 22:05:04.072 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy20@4a1abba1 was destroying! 24 2023-07-20 22:05:04.072 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy26@67263db7 was destroying! 25 2023-07-20 22:05:04.072 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy27@4a2bc71f was destroying! 26 2023-07-20 22:05:04.073 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy10@20f94e9a was destroying! 27 2023-07-20 22:05:04.073 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy12@3f213e97 was destroying! 28 2023-07-20 22:05:04.073 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy18@693c7741 was destroying! 29 2023-07-20 22:05:04.073 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy2@7d3b4646 was destroying! 30 2023-07-20 22:05:04.073 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy1@175c4ae5 was destroying! 31 2023-07-20 22:05:04.073 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy14@21bf1b1f was destroying! 32 2023-07-20 22:05:04.073 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy6@2b33e616 was destroying! 33 2023-07-20 22:05:04.074 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy22@c1d9c40 was destroying! 34 2023-07-20 22:05:04.075 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy28@79e3f444 was destroying! 35 2023-07-20 22:05:04.075 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy8@205d6f84 was destroying! 36 2023-07-20 22:05:04.075 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy6@2b33e616 was destroying! 37 2023-07-20 22:05:04.076 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy25@74f54f8e was destroying! 38 2023-07-20 22:05:04.076 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy3@1252d480 was destroying! 39 2023-07-20 22:05:04.076 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy5@59b8a801 was destroying! 40 2023-07-20 22:05:04.076 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy21@1c61f9bf was destroying! 41 2023-07-20 22:05:04.076 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy8@205d6f84 was destroying! 42 2023-07-20 22:05:04.076 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy5@59b8a801 was destroying! 43 2023-07-20 22:05:04.110 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy29@173c0722 was destroying! 44 2023-07-20 22:05:04.110 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy4@12e3cf1c was destroying! 45 2023-07-20 22:05:04.131 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy30@7c1adb8a was destroying! 46 2023-07-20 22:05:04.131 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:293 - org.apache.dubbo.common.bytecode.proxy0@72364a40 was destroying! 47 2023-07-20 22:05:04.178 [TID:N/A] [] [main] INFO o.a.d.c.s.b.f.a.ReferenceAnnotationBeanPostProcessor:305 - class org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor was destroying! 48 2023-07-20 22:05:04.178 [TID:N/A] [] [main] INFO com.zaxxer.hikari.HikariDataSource:350 - HikariPool-1 - Shutdown initiated... 49 2023-07-20 22:05:04.201 [TID:N/A] [] [main] INFO com.zaxxer.hikari.HikariDataSource:352 - HikariPool-1 - Shutdown completed. 50 2023-07-20 22:05:04.204 [TID:N/A] [] [main] INFO org.apache.catalina.core.StandardService:173 - Stopping service [Tomcat] 51 2023-07-20 22:05:04.230 [TID:N/A] [] [main] INFO o.s.b.a.l.ConditionEvaluationReportLoggingListener:142 - 52 53 Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 54 #停dubbo服务 55 2023-07-20 22:05:04.231 [TID:N/A] [] [main] INFO o.a.d.s.b.c.e.AwaitingNonWebApplicationListener:162 - [Dubbo] Current Spring Boot Application is about to shutdown... 56 #应用服务中止。显示是循环依赖所致,并打印异常详细stacktrace 57 2023-07-20 22:05:04.262 [TID:N/A] [] [main] ERROR org.springframework.boot.SpringApplication:858 - Application run failedorg.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'payUserSignBizService': Bean with name 'payUserSignBizService' has been injected into other beans [payMerchantAgreementApiImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example. 58 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622) 59 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515) 60 at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) 61 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) 62 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) 63 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) 64 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:849) 65 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) 66 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) 67 at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142) 68 at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) 69 at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) 70 at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) 71 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) 72 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) 73 at com.emax.channel.provider.ChannelServerApplication.main(ChannelServerApplication.java:38) 74 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 75 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 76 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 77 at java.lang.reflect.Method.invoke(Method.java:498) 78 at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48) 79 at org.springframework.boot.loader.Launcher.launch(Launcher.java:87) 80 at org.springframework.boot.loader.Launcher.launch(Launcher.java:50) 81 at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51) 82 #至此,凉凉。
如何解决循环依赖?
首先要理清各类的职责,尤其是我们的分层架构中各层级的职责————相同层级不要相互调用,调用下级API。
下面是职责不清晰导致的循环依赖。解决方案是,消除同级的UserService和LoginAccountService两个服务的相互依赖,他们在调用对方的数据时,应调用其下层的方法。例如:UserService#selectUserInfo在获取LoginAccount数据时,改为调用loginAccountMapper#selectByUserId。

// ------------------------- UserService ------------------------------------ @Service public class UserService { @Resource private UserMapper userMapper; @Resource private LoginAccountService loginAccountService; public UserVO selectUserInfo(int userId) { User user = userMapper.selectByUserId(userId); if (user == null) { throw new ResponseException("用户不存在"); } UserVO userVO = BeanMapper.map(user, UserVO.class); LoginAccount loginAccount= loginAccountService.selectByUserId(userId); if (loginAccount!=null){ userVO.setLoginAccount(loginAccount.getLoginAccount()); } return userVO; } public void validUser(int userId) { User user = userMapper.selectByUserId(userId); if (user == null) { throw new ResponseException("用户不存在"); } if (!"NORMAL".equalsIgnoreCase(user.getStatus())) { throw new ResponseException("用户已禁用"); } } } // --------------------------- LoginAccountService ---------------------------------- @Service public class LoginAccountService { @Resource private UserService userService; @Resource private LoginAccountMapper loginAccountMapper; public LoginAccount login(String loginAccount, String password) { // 获取登陆账号 LoginAccount entity = loginAccountMapper.selectByAccount(loginAccount); if (entity == null) { throw new ResponseException("账号不存在"); } if (!password.equals(entity.getPassword())) { throw new ResponseException("用户密码错误"); } userService.validUser(entity.getUserId()); return entity; } public LoginAccount selectByUserId(int userId) { return loginAccountMapper.selectByUserId(userId); } }
当然,上面的循环依赖,单从技术层面,加@Lasy等手段是可以解决的,但正确的姿势还是应基于类的职责来重新设计你的程序。要知道职责不清晰会带来诸多问题,绝不仅仅是重复依赖的问题。
业务回调场景中的循环依赖
在我们的企业应用开发中,有一种的场景是,一个业务完成后,回调通知另一个业务,这种“回调”的业务场景在程序实现时,也可能出现循环依赖。我们正好碰到了这个比较典型的情况,跟大家分享一下。
我们的中台channel服务,对接了数家银行通道。其中的一个YiLian通道有个特殊需求,在付款到个人之前需要对收款人进行报备,报备完成才可付款。见下图示意图(设计文稿),YiLianPayStrategy 注入了上层的PayUserSignService,PayUserSignService有注入YiLianSignStrategy,YiLianSignStrategy有注入YiLianPayStrategy,产生YiLianPayStrategy 与YiLianSignStrategy的相互循环依赖。
那么,如果解决“回调”业务场景下的循环依赖呢?
解决方案有二:
1. 不使用bean注入的方式,改为需要回调时才获取bean。
实现方式1)业务bean注入ApplicationContext对象,在回调代码里利用ApplicationContext#getBean来获取目标对象,然后调用目标对象的回调处理方法。
实现方式2)我们的系统里封装了一个实现了ApplicationContextAware接口的SpringContextUtils工具类,这个类提供了基于ApplicationContext#getBean的静态方法getBean。业务bean的回调代码里调用SpringContextUtils#getBean即可获取目标对象,然后调用目标对象的回调处理方法。
上面案例中,YiLianSignStrategy的回调方法利用SpringContextUtils可以改为这样

/** * 封装结算人员报备成功后的相关处理逻辑 * * @param batchNo 批次号 * @param details 批次明细 * @return */ @VisibleForTesting public int batchUpdateQueryResult(String batchNo, List<PayUserSign> details) { if (CollectionUtils.isEmpty(details)) return 0; // 将银行返回的结果,持久化更新报备数据记录 int count = payUserSignManager.batchUpdateQueryResult(details); if (count > 0 && details.stream().anyMatch(p -> PayUserSignStatusEnum.isFinalState(p.getStatus()))) { // 报备完成,回调支付下发业务,继续下发 details.removeIf(p -> PayUserSignStatusEnum.SUCCESS != p.getStatus()); if (CollectionUtils.isNotEmpty(details)) { ThreadPoolUtil.execute(() -> SpringContextUtils.getBean(YiLianPaymentStrategy.class).signOkCallBack(details)); } } return count; }
2. 借助spring的事件监听器,通过其发布订阅模式,实现类之间的解耦。
业务bean的回调代码里,利用ApplicationContext#publishEvent发布一个应用事件;目标对象的回调处理方法添加@EventListener来接收事件通知。注意事件的发布与监听处理是同步的,需要考虑异步实现。
上面案例中我定义了一个Holder,封装了事件的发布和订阅。

import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.event.EventListener; /** * 使用事件监听器来消除类之间的循环依赖 * @author zhangguozhan * 2023-7-24 18:46 */ @Slf4j @Component public class PayUserSignCallbackEventHolder { @Resource private ApplicationContext applicationContext; @Getter private static final class MyEvent extends ApplicationEvent { private final List<PayUserSignDTO> userSignList; /** * Create a new ApplicationEvent. * * @param source the object on which the event initially occurred (never {@code null}) */ public MyEvent(List<PayUserSignDTO> source) { super(source); this.userSignList = source; } } public void publish(List<PayUserSignDTO> payUserSignList) { applicationContext.publishEvent(new MyEvent(payUserSignList)); } @EventListener(classes = MyEvent.class) public void consume(MyEvent event) { ThreadPoolUtil.getThreadPoolExecutor().execute(() -> applicationContext.getBean(YiLianPaymentStrategy.class).signOkCallBack(event.getUserSignList())); } }
本文多次提到spring的interface ApplicationContext。我们窥豹一斑diagram。
感谢阅读!本文写作整理用时两晚近4h。
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/17578264.html