buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

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 #至此,凉凉。
View Code

 

 

如何解决循环依赖?

首先要理清各类的职责,尤其是我们的分层架构中各层级的职责————相同层级不要相互调用,调用下级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);
    }
}
View Code

当然,上面的循环依赖,单从技术层面,加@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;
    }
View Code

 

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()));
    }

}
View Code

 

 

本文多次提到spring的interface ApplicationContext。我们窥豹一斑diagram。

 

 

 

感谢阅读!本文写作整理用时两晚近4h。

posted on 2023-07-25 20:20  buguge  阅读(364)  评论(0)    收藏  举报