前言
大家在开发中有没有遇到过因循环依赖导致项目启动失败?在排查循环依赖的过程中有没困难?如何避免写出循环依赖的代码?
我没写过循环依赖的代码,作为稳定性负责人,我排查过多次。
有些逻辑简单的代码,循环依赖很容易排查。但是,我们的业务超级复杂,绝大多数循环依赖,一整天都查不出来。
起初我们遇到一个循环依赖处理一个,作为稳定性负责人,技术能干的事,不会让人做第二次,为此,我写了一段循环依赖巡检代码,把循环依赖扼杀在测试环境。
下面介绍下场景及处理思路,未必最优,欢迎交流。
背景
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属性的循环依赖是可以的:
示例代码:
@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"; } }
参考 :https://my.oschina.net/tridays/blog/805111
杜绝循环依赖的解决方案
1,查看bean的依赖
2,解析bean及其依赖beans
如上,我们可以查看某个bean的依赖项,由此,我们可以递归查找bean是否存在循环引用。
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 每隔几分钟巡检,有循环依赖就企业微信报警,这里截取一段日志,如下:
最后
水滴保险商城-架构组招java实习生(本人直招)
要求:
1、具备较强的编程基本功,熟练掌握JAVA编程语言,熟悉常用数据结构与算法。
2、了解常用的开源框架和开源服务(Spring,Netty,MySQL,Redis,Tomcat,Nginx 等)。
3、熟悉网络编程、多线程编程、分布式等优先。
4、阅读过spring家族源码优先,有技术博客优先,熟悉spring-cloud技术栈优先。
5、 做事积极主动,有较强的执行能力和和较好的沟通能力。
对水滴感兴趣的同学可找我内推,java 后端和QA 都可以,我找负责人评估合适的话,推给HR联系您。