Dubbo先启动客户端再启动服务端,线上收银系统崩了

开发 项目管理
晚上的时候,我负责的系统和收银系统同时上线一波(用的是Dubbo)。然后很神奇的事情发生了,收银系统用@Reference注解注入我的接口,然后这个接口的实现类居然为空。

[[405161]]

线上发生事故了

前天晚上上线一波,发生了一个挺有意思的事,昨天复盘了一下,今天分享一下。

晚上的时候,我负责的系统和收银系统同时上线一波(用的是Dubbo)。然后很神奇的事情发生了,收银系统用@Reference注解注入我的接口,然后这个接口的实现类居然为空。

其实我们当时没排查出来是什么原因?

「重启了一下就好了,毕竟重启大法好。」 但本着不能给用户充钱的路上造成阻碍,还是要排查一波这个代理对象为空是如何造成的。

「线上dubbo的版本为2.8.9,注意包名是(com.alibaba)」

为了方便大家理解我说的内容,简单说一下RPC框架的执行流程。

  1. Server将服务信息注册到Registry,Client从Registry拉取Server的信息。
  2. Client通过代理对象(Client Stub)发送发送网络请求,Server通过代理对象(Server Stub)执行本地方法
  3. 网络传输过程中有编解码和序列化的过程

「在Dubbo中Client Stub和Server Stub都是Invoker对象」

我们继续,注入的接口实现类居然能为空?我就看了一下他写的代码,只用了一个@Reference注解,没有设置任何属性。

  1. @Documented 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 
  4. public @interface Reference { 
  5.  
  6.     // 省略其他属性 
  7.     boolean check() default true
  8.  

那么check=true,即没有服务提供者的时候,服务消费者都不能正常启动,因为会抛出IllegalStateException异常。既然能正常启动,那这个代理对象正常创建了啊,不可能为null啊

  1. // 2.8.9版本 
  2. // ReferenceConfig#createProxy 
  3. Boolean c = check
  4. if (c == null && consumer != null) { 
  5.     c = consumer.isCheck(); 
  6. if (c == null) { 
  7.     c = true; // default true 
  8. if (c && !invoker.isAvailable()) { 
  9.     throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion()); 

「然后我同事说有没有可能是客户端先启动,没有服务提供者导致代理对象为空的?」

我说不可能的,客户端先启动,check属性为true,不可能启动成功的!再说每次上线,新服务正常启动后,才会关闭旧服务的,服务提供者一定会有的。

「为什么会发生这种情况,是真心搞不懂,只能google “@Reference 注入对象为null”」

答案基本一致,没有服务提供者导致代理对象为空的,只要把@Reference的check属性设置为false即可,至于原因没一篇文章说过

「接下来就是验证网上的方法了」

  1. 先启动producer,再启动consumer,正常调用
  2. 先启动consumer(check=true),再启动producer,代理对象为空,完美复现
  3. 先启动consumer(check=false),再启动producer,正常调用

「和我的想法不一致,学dubbo的时候没听过必须先启动producer再启动consumer才能正常调用啊?」

我就拿出我学dubbo时用的例子测试了一波,dubbo的版本为2.7.3注意包名是(org.apache)

先启动producer,再启动consumer,正常调用

先启动consumer(check=true),此时没有producer,启动失败

先启动consumer(check=false),再启动producer,正常调用

「这才符合我的想法啊」

揭秘真相

既然@Reference注入的对象为null,那说明Spring Bean的生命周期中属性赋值阶段有问题

再来分析一下@Reference注解的注入逻辑,和@Autowired,@Resource之类的注入逻辑基本差不多。

当你加入Dubbo的spring boot starter时,会往容器中注入ReferenceAnnotationBeanPostProcessor,看一下这个类的继承关系

其中最主要的部分你只需要知道这个类重写了 InstantiationAwareBeanPostProcessor#postProcessPropertyValues(这个方法在后面的版本中被postProcessProperties方法替代),而这个方法正是用来属性赋值的,看上面的Bean生命周期图

  1. public class ReferenceAnnotationBeanPostProcessor { 
  2.  
  3.  // 省略了继承类和方法 
  4.     // 这个方法给@Reference属性赋值 
  5.     @Override 
  6.     public PropertyValues postProcessPropertyValues( 
  7.             PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException { 
  8.  
  9.         InjectionMetadata metadata = findReferenceMetadata(beanName, bean.getClass(), pvs); 
  10.         try { 
  11.             metadata.inject(bean, beanName, pvs); 
  12.         } catch (BeanCreationException ex) { 
  13.             throw ex; 
  14.         } catch (Throwable ex) { 
  15.             throw new BeanCreationException(beanName, "Injection of @Reference dependencies failed", ex); 
  16.         } 
  17.         return pvs; 
  18.     } 
  19.  

接着执行到ReferenceFieldElement#inject方法,@Reference引入的对象会被包转为ReferenceBean

  1. private class ReferenceFieldElement extends InjectionMetadata.InjectedElement { 
  2.  
  3.  
  4.     @Override 
  5.     protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable { 
  6.  
  7.         Class<?> referenceClass = field.getType(); 
  8.  
  9.         // 获取 referenceBean 的逻辑在这 
  10.         referenceBean = buildReferenceBean(reference, referenceClass); 
  11.  
  12.         ReflectionUtils.makeAccessible(field); 
  13.  
  14.   // 通过反射注入对象 
  15.         field.set(bean, referenceBean.getObject()); 
  16.  
  17.     } 
  18.  

经过一系列方法调用执行到如下方法

  1. // AbstractAnnotationConfigBeanBuilder#build 
  2. public final B build() throws Exception { 
  3.  
  4.     checkDependencies(); 
  5.  
  6.     B bean = doBuild(); 
  7.  
  8.     configureBean(bean); 
  9.  
  10.     if (logger.isInfoEnabled()) { 
  11.         logger.info(bean + " has been built."); 
  12.     } 
  13.  
  14.     return bean; 
  15.  

此时日志中会打印ReferenceBean对象,这个对象继承了AbstractConfig,所以会执行AbstractConfig#toString方法

  1. public abstract class AbstractConfig implements Serializable { 
  2.  
  3.     @Override 
  4.     public String toString() { 
  5.         try { 
  6.             StringBuilder buf = new StringBuilder(); 
  7.             buf.append("<dubbo:"); 
  8.             buf.append(getTagName(getClass())); 
  9.             Method[] methods = getClass().getMethods(); 
  10.             for (Method method : methods) { 
  11.                 try { 
  12.                     String name = method.getName(); 
  13.                     if ((name.startsWith("get") || name.startsWith("is")) 
  14.                             && !"getClass".equals(name) && !"get".equals(name) && !"is".equals(name
  15.                             && Modifier.isPublic(method.getModifiers()) 
  16.                             && method.getParameterTypes().length == 0 
  17.                             && isPrimitive(method.getReturnType())) { 
  18.                         int i = name.startsWith("get") ? 3 : 2; 
  19.                         String key = name.substring(i, i + 1).toLowerCase() + name.substring(i + 1); 
  20.                         Object value = method.invoke(this, new Object[0]); 
  21.                         if (value != null) { 
  22.                             buf.append(" "); 
  23.                             buf.append(key); 
  24.                             buf.append("=\""); 
  25.                             buf.append(value); 
  26.                             buf.append("\""); 
  27.                         } 
  28.                     } 
  29.                 } catch (Exception e) { 
  30.                     logger.warn(e.getMessage(), e); 
  31.                 } 
  32.             } 
  33.             buf.append(" />"); 
  34.             return buf.toString(); 
  35.         } catch (Throwable t) { 
  36.             logger.warn(t.getMessage(), t); 
  37.             return super.toString(); 
  38.         } 
  39.     } 
  40.  

「好家伙,打印的时候把get方法全执行了一遍,然后执行ReferenceBean#getObject方法异常了(就是那个没有服务提供者抛出的异常),但是被try Catch了」

因为ReferenceBean是一个FactoryBean,所以需要调用getObject方法才能获取创建的对象

  1. private class ReferenceFieldElement extends InjectionMetadata.InjectedElement { 
  2.  
  3.  
  4.     @Override 
  5.     protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable { 
  6.  
  7.         Class<?> referenceClass = field.getType(); 
  8.  
  9.         // 获取 referenceBean 的逻辑在这 
  10.         referenceBean = buildReferenceBean(reference, referenceClass); 
  11.  
  12.         ReflectionUtils.makeAccessible(field); 
  13.  
  14.   // 通过反射注入对象 
  15.         field.set(bean, referenceBean.getObject()); 
  16.  
  17.     } 
  18.  

「接着调用ReferenceBean#getObject方法,好了,这就是服务导出的逻辑了!」 不细说了,后续单开文章写,会执行到ReferenceConfig#get方法

  1. // ReferenceConfig#get 
  2. public synchronized T get() { 
  3.     if (destroyed) { 
  4.         throw new IllegalStateException("Already destroyed!"); 
  5.     } 
  6.     if (ref == null) { 
  7.         init(); 
  8.     } 
  9.     return ref; 

「此时代理对象为null,执行init方法,initialized默认为false,执行一次变为true(AbstractConfig执行toString方法的时候哈),所以第二次执行,直接return,此时代理对象为null,完事!」

  1. private void init() { 
  2.     if (initialized) { 
  3.         return
  4.     } 
  5.     initialized = true
  6.     // 省略部分代码 

「我学习用的版本为什么能正常工作?」

  1. public final C build() throws Exception { 
  2.  
  3.     checkDependencies(); 
  4.  
  5.     C configBean = doBuild(); 
  6.  
  7.     configureBean(configBean); 
  8.  
  9.     if (logger.isInfoEnabled()) { 
  10.         logger.info("The configBean[type:" + configBean.getClass().getSimpleName() + "] has been built."); 
  11.     } 
  12.  
  13.     return configBean; 
  14.  

就是打印的时候不会执行getObject方法了

「为什么@Reference的check属性设置为false就能正常调用?」

因为第一次调用成功执行完ReferenceBean#getObject方法,ref已经赋值为代理对象了,第二次执行就能将这个代理对象返回

  1. // ReferenceConfig#get 
  2. public synchronized T get() { 
  3.     if (destroyed) { 
  4.         throw new IllegalStateException("Already destroyed!"); 
  5.     } 
  6.     if (ref == null) { 
  7.         init(); 
  8.     } 
  9.     return ref; 

至于我们的线上系统为什么没获取到服务提供者,我估计很大概率是由于网络的原因

解决方案

  • @Reference注解的check属性设置为false(默认为true),因为当你的check属性为true并且没有服务提供者时,不会起任何作用,只会注入一个空对象,后续当有服务提供者可用时,这个对象始终为空。当check为false时,会注入一个代理对象,当有服务提供者时,这个代理对象会刷新,就能正常发起调用
  • 选择能正常执行的版本

本文转载自微信公众号「Java识堂」,可以通过以下二维码关注。转载本文请联系Java识堂公众号。

 

责任编辑:武晓燕 来源: Java识堂
相关推荐

2011-09-09 09:44:23

WCF

2009-08-21 16:14:52

服务端与客户端通信

2009-08-21 15:59:22

服务端与客户端通信

2009-08-21 15:36:41

服务端与客户端

2009-08-21 15:54:40

服务端与客户端

2010-03-18 17:47:07

Java 多客户端通信

2023-03-06 08:01:56

MySQLCtrl + C

2024-03-06 14:58:52

客户端微服务架构

2010-11-19 14:22:04

oracle服务端

2023-04-03 08:13:05

MySQLCtrl + C

2021-04-16 08:54:03

CMS系统redisnode服务器

2023-08-14 08:17:13

Kafka服务端

2015-01-13 10:32:23

RestfulWeb框架

2021-10-19 08:58:48

Java 语言 Java 基础

2018-12-27 13:11:04

爱奇艺APP优化

2010-01-13 18:23:46

2022-09-05 14:36:26

服务端TCP连接

2010-05-28 14:11:37

SVN1.6

2009-03-04 10:27:50

客户端组件桌面虚拟化Xendesktop

2021-09-22 15:46:29

虚拟桌面瘦客户端胖客户端
点赞
收藏

51CTO技术栈公众号