SpringCloud BeanCurrentlyInCreationException 异常解决方案

大家在开发中有没有遇到过因循环依赖导致项目启动失败?在排查循环依赖的过程中有没困难?如何避免写出循环依赖的代码?

我没写过循环依赖的代码,作为稳定性负责人,我排查过多次。

有些逻辑简单的代码,循环依赖很容易排查。但是,我们的业务超级复杂,绝大多数循环依赖,一整天都查不出来。

起初我们遇到一个循环依赖处理一个,作为稳定性负责人,技术能干的事,不会让人做第二次,为此,我写了一段循环依赖巡检代码,把循环依赖扼杀在测试环境。

下面介绍下场景及处理思路,未必最优,欢迎交流。

背景

SpringCloud服务在上线时出现BeanCurrentlyInCreationException异常(服务本地启动无异常,测试环境启动也无异常,上线就偶尔异常)。

1,本地模拟如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:

Error creating bean with name 'studentA' : Bean with name 'studentA' has been injected into other beans [studentC] 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.

2,生产场景如下图:

异常排查&模拟

经过生产排查和本地测试,产生此异常的场景如下:

1,如果类方法有@Async注解,则可能出现如上异常

2,如果存在循环依赖,并且类方法上有@Async注解,则必然会出现如上异常。

1,场景演示:

在spring中,基于field属性的循环依赖是可以的:
image

示例代码:

@Service
@Slf4j
public class StudentA {

    @Autowired
    private StudentB studentB;
    public String test(){
        return studentB.test();
    }
    public String test1(){
        return "Studenta1";
    }
}

@Slf4j
@Service
public class StudentB {

    @Autowired
    private StudentC studentC;

    public String test() {
        return "Studentb";
    }

    public String test1() {
        return studentC.test1();
    }
}

@Slf4j
@Service
public class StudentC {

    @Autowired
    private StudentA studentA;

    public String test() {
        return studentA.test();
    }

    public String test1() {
        return "Studentc1";
    }
}

@Autowired
private StudentA studentA ;
@Test
public void testA(){
    String v= studentA.test();
    log.info("testa={}",v);
}

以上代码输出正常

2,异常演示

如果我们的方法加上了@Async 注解。则抛出异常:

@Service
@Slf4j
public class StudentA {

    @Autowired
    private StudentB studentB;
    @Async
    public String test(){
        return studentB.test();
    }
    @Async
    public String test1(){
        return "Studenta1";
    }
}

@Slf4j
@Service
public class StudentB {

    @Autowired
    private StudentC studentC;
    @Async
    public String test() {
        return "Studentb";
    }
    @Async
    public String test1() {
        return studentC.test1();
    }
}

@Slf4j
@Service
public class StudentC {

    @Autowired
    private StudentA studentA;
    @Async
    public String test() {
        return studentA.test();
    }
    @Async
    public String test1() {
        return "Studentc1";
    }
}

@Autowired
private StudentA studentA ;
@Test
public void testA(){
    String v= studentA.test();
    log.info("testa={}",v);
}

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: 
Error creating bean with name 'studentA': Bean with name 'studentA' has been injected into other beans [studentC] 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.

3,解决异常

A,B,C三个类中,至少一个类的field 必须加@Lazy

@Service
@Slf4j
public class StudentA {

    @Autowired
    @Lazy
    private StudentB studentB;
    @Async
    public String test(){
        return studentB.test();
    }
    @Async
    public String test1(){
        return "Studenta1";
    }
}

@Slf4j
@Service
public class StudentB {

    @Autowired
    @Lazy
    private StudentC studentC;
    @Async
    public String test() {
        return "Studentb";
    }
    @Async
    public String test1() {
        return studentC.test1();
    }
}

@Slf4j
@Service
public class StudentC {

    @Autowired
    @Lazy
    private StudentA studentA;
    @Async
    public String test() {
        return studentA.test();
    }
    @Async
    public String test1() {
        return "Studentc1";
    }
}

参考 :Spring @Async 应用于出现循环依赖的 Bean 报错的解决方案 - 临时小驻 - OSCHINA - 中文开源技术交流社区

杜绝循环依赖的解决方案

这里根据我司的服务架构介绍一下如何避免循环依赖,我司用的spring cloud 全家桶,注册中心用的consul,配置中心有config 和apollo。

1,查看bean的依赖

spring-cloud 提供了bean信息查看功能,通过ip:端口+/actuator/beans可查看(如何配置actuator这里不详细讨论),如下图,我们可以看到每个bean依赖的bean

杜绝循环依赖的解决方案

这里根据我司的服务架构介绍一下如何避免循环依赖,我司用的spring cloud 全家桶,注册中心用的consul,配置中心有config 和apollo。

1,查看bean的依赖

spring-cloud 提供了bean信息查看功能,通过ip:端口+/actuator/beans可查看(如何配置actuator这里不详细讨论),如下图,我们可以看到每个bean依赖的bean

2,解析bean及其依赖beans

如上,我们可以查看某个bean的依赖项,由此,我们可以递归查找bean是否存在循环引用。
image
bean 信息模型

@Data
public class ApplicationBeans {

    private Map<String, ContextBeans> contexts;
}



@Data
public class ContextBeans {

    private Map<String, BeanDescriptor> beans;
    private String parentId;
}

3,代码实现

这里检测项目起名alarm-center,检测类为CheckCyclicDependenceService

@Slf4j
@RefreshScope
@Service
public class CheckCyclicDependenceService {
    // 服务发现
    @Autowired
    private DiscoveryClient discoveryClient;
    //默认检测 api,admin两个项目,如果想检测其它项目,可在config配置
    @Value("${check-serviceids-value:api,admin}")
    private String checkServiceIds;
     //入口
    public void checkCyclicDependence() {
        if (Strings.isNullOrEmpty(checkServiceIds)) {
            return;
        }
        List<String> serviceIds = Arrays.asList(checkServiceIds.split(","));
        for (String serviceId : serviceIds) {
            long start = System.currentTimeMillis();
            RestTemplate restTemplate = new RestTemplate();            //根据服务名去consul找一个服务实例
            ServiceInstance instance = discoveryClient.getInstances(serviceId).get(0);
            String url = instance.getUri() + "/actuator/beans";
            //所有的beans信息
            String applicationBeansStr = restTemplate.getForObject(url, String.class);
            ApplicationBeans applicationBeans = JSONObject.parseObject(applicationBeansStr, ApplicationBeans.class);
            long end = System.currentTimeMillis();
            log.info("checkCyclicDependence get applicationBeans end,serviceid={},coust={}", serviceId, (end - start));
            Map<String, ContextBeans> contexts = applicationBeans.getContexts();
            Map<String, BeanDescriptor> qualifiedBeans = new HashMap<>();
            for (Map.Entry<String, ContextBeans> conEntry : contexts.entrySet()) {
                if (!conEntry.getKey().startsWith(serviceId)) {
                    continue;
                }
                ContextBeans contextBeans = conEntry.getValue();
                Map<String, BeanDescriptor> beans = contextBeans.getBeans();

                for (Map.Entry<String, BeanDescriptor> entry1 : beans.entrySet()) {
                    String beanName = entry1.getKey().toLowerCase();
                    BeanDescriptor beanDescriptor = entry1.getValue();
                    if (!beanDescriptor.getType().startsWith("com.shuidihuzhu") || beanName.endsWith("aspect") || beanName.endsWith("controller")
                            || beanName.endsWith("dao") || beanName.endsWith("datasource")
                            || beanName.endsWith("fallback") || beanDescriptor.getDependencies().length == 0) {
                        continue;
                    }
                    qualifiedBeans.put(entry1.getKey(), beanDescriptor);
                }
            }
            StringBuilder sb = new StringBuilder();
            cyclicDependence(qualifiedBeans, sb);
            if (sb.length() > 0) {
                sb.append(System.getProperty("line.separator"));
                sb.append("重注代码质量,尽量做到无循环依赖");
                sb.append(System.getProperty("line.separator"));
                sb.append("所属服务:" + serviceId);
                //alarmClient.sendByUser(Lists.newArrayList("zhangzhi"), sb.toString());
                alarmClient.sendByGroup("cf-server-alarm", sb.toString());
                end = System.currentTimeMillis();
            }
            log.info("checkCyclicDependence end,serviceid={},coust={}", serviceId, (end - start));
        }

    }

    public void cyclicDependence(Map<String, BeanDescriptor> beans, StringBuilder sb) {

        for (Map.Entry<String, BeanDescriptor> bean : beans.entrySet()) {
            String beanName = bean.getKey();
            check(beans, beanName, beanName, 0, sb);
        }
    }


    public void check(Map<String, BeanDescriptor> beans, String beanName, String dependenceName, int depth, StringBuilder sb) {
        if (depth == 4) {//这里可以指定深度,层级过多,容易栈溢出
            return;
        }
        depth++;
        BeanDescriptor bean = beans.get(dependenceName);//依赖项的依赖项
        if (bean != null) {
            String[] deps = bean.getDependencies();//依赖项
            for (String dep : deps) {
                if (dep.equals(beanName)) {
                    sb.append(System.getProperty("line.separator"));
                    String str = String.format("%s和%s存在循环依赖;", beanName, dependenceName);
                    sb.append(str);
                    log.info(str);
                } else {
                    check(beans, beanName, dep, depth, sb);
                }
            }
        }

    }
}

效果

我们在测试环境有个job 每隔几分钟巡检,有循环依赖就企业微信报警,这里截取一段日志,如下:


作者:
原文:http://zhangzhi19861216.cnblogs.com/