你遇到过Dubbo调用报空指针异常吗?

下面我要介绍的,是一次真实的产线事件。由于各种因素,导致Dubbo多打印了一行日志后,出现空指针异常。希望能帮到苦苦寻找答案的你!

备注:文章所用Dubbo版本为2.6.3

问题复现

  1. Consumer A服务依赖Provider B服务。
  2. Consumer A服务先启动了,此时注册中心无B服务实现,而后Provider B启动。
  3. 两个系统均正常启动,但A服务调用B服务时,报空指针异常
    报错如下:
[dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause
java.lang.NullPointerException: null
…

其中,消费者A的Dubbo注入使用了@Reference注解,类似如下代码

@Service
public class AuthServiceImpl implements AuthService {@Referenceprivate UserService userService;@Overridepublic String getNameById(String id) {return userService.getNameById(id);}
}

出现问题后,我们仔细排查。注意到此处Dubbo服务引用使用的是@Reference注解形式,且未指定check参数(默认为true)。按照常理来说:

  • 如果A依赖的B服务没有启动的话,A服务启动就应当报错,但是启动并没有报错
  • B服务上线后,按照Dubbo的功能说明,应当可以做到服务自动注入与发现,A服务调用时不应当出现空指针异常

据此推测,可能是@Reference注解+check参数配置原因,导致Dubbo代理没有生成,此时注入的服务为null,因此Dubbo的那一套都失效了。

场景测试

为了判断到底是不是@Reference注解+check配置的原因,我特意做了几个场景测试。包括xml配置和注解配置方式。总结如下:

若A服务依赖Dubbo服务B,当A服务先于B服务启动,根据配置方式的不同,会有以下几种场景

通过测试结果,可以看到,只有使用注解@Reference进行配置,且check=true场景,不符合我们的预期。
按照官方说明,配置check=true,服务在启动时进行服务可用性的校验。如果服务不可用,应当无法启动才对。

顺着这种异常的场景代码,进入Dubbo的源码进行debug,经历了一系列坑之后,我终于找到产生的原因。下面从Dubbo源码的角度来分析一下产生的原因。

源码分析

首先先明确一下,Dubbo中@Reference注解解析的相关代码在ReferenceConfig类中。
调用关系是
com.alibaba.dubbo.config.ReferenceConfig#init --> com.alibaba.dubbo.config.ReferenceConfig#createProxy

在createProxy方法中,有关check参数解析的代码如下:

Boolean c = check;
if (c == null && consumer != null) {c = consumer.isCheck();
}
if (c == null) {c = true; // default true
}
if (c && !invoker.isAvailable()) {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());
}
if (logger.isInfoEnabled()) {logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);

可以看到,check参数默认为true。如果服务不可用,会抛出IllegalStateException异常!如果check设置为false,则会跳过校验逻辑,会继续下面的生成代理逻辑。

但是为啥没有调用到呢?

顺着debug调用链路往下看,在Spring启动时,Dubbo注解相关的工作执行代码调用链如下:

  • com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor#postProcessPropertyValues
  • org.springframework.beans.factory.annotation.InjectionMetadata#inject
  • com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor.ReferenceFieldElement#inject
  • com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor#buildReferenceBean
  • com.alibaba.dubbo.config.spring.beans.factory.annotation.AbstractAnnotationConfigBeanBuilder#build
  • com.alibaba.dubbo.config.AbstractConfig#toString
  • com.alibaba.dubbo.config.spring.ReferenceBean#getObject
  • com.alibaba.dubbo.config.ReferenceConfig#get
  • com.alibaba.dubbo.config.ReferenceConfig#init
  • com.alibaba.dubbo.config.ReferenceConfig#createProxy
  1. 首先,使用了@Reference注解的Bean对象,在Spring启动注入时,会执行com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor#postProcessPropertyValues方法,进行@Reference字段的注入

  2. 然后,执行进入com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor.ReferenceFieldElement#inject方法,进行referenceBean对象的构建,构建完成后注入到指定的@Reference字段中

  @Overrideprotected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {Class<?> referenceClass = field.getType();// 构建reference对象referenceBean = buildReferenceBean(reference, referenceClass);ReflectionUtils.makeAccessible(field);// 反射注入值field.set(bean, referenceBean.getObject());}

注意在上述反射注入值时,调用了referenceBean的getObject()方法,此方法会依次调用以下链路

  • com.alibaba.dubbo.config.spring.ReferenceBean#getObject
  • com.alibaba.dubbo.config.ReferenceConfig#get
  • com.alibaba.dubbo.config.ReferenceConfig#init
  • com.alibaba.dubbo.config.ReferenceConfig#createProxy

从而完成ReferenceBean的代理生成和返回,理论上这里应该是check发挥作用(抛异常)的地方,一次正常的调用链路应当如此。

但是回过头来查看我们debug的调用链路,可以发现,referenceBean的getObject()方法好像在奇怪的地方被调用了!!!

  • com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor#buildReferenceBean
  • com.alibaba.dubbo.config.spring.beans.factory.annotation.AbstractAnnotationConfigBeanBuilder#build
  • com.alibaba.dubbo.config.AbstractConfig#toString

这是什么鬼?!
在构建ReferenceBean的时候(buildReferenceBean),referenceBean的getObject()居然被提前执行了?

先来看一下调用点,com.alibaba.dubbo.config.spring.beans.factory.annotation.AbstractAnnotationConfigBeanBuilder#build

 public final B build() throws Exception {checkDependencies();B bean = doBuild();configureBean(bean);// 日志级别达到info,就会打印下面的bean,就会调用AbstractConfig的toString方法if (logger.isInfoEnabled()) {logger.info(bean + " has been built.");}return bean;}

这个方法看起来平平无奇,为啥会调用到referenceBean的getObject方法呢?
答案就在这里的日志打印上!打印此处的bean(为ReferenceBean类型),会调用到这个bean基类(AbstractConfig)的toString方法

我们进到AbstractConfig的toString方法里看一下:

public String toString() {try {StringBuilder buf = new StringBuilder();buf.append("<dubbo:");buf.append(getTagName(getClass()));Method[] methods = getClass().getMethods();for (Method method : methods) {try {String name = method.getName();if ((name.startsWith("get") || name.startsWith("is"))&& !"getClass".equals(name) && !"get".equals(name) && !"is".equals(name)&& Modifier.isPublic(method.getModifiers())&& method.getParameterTypes().length == 0&& isPrimitive(method.getReturnType())) {int i = name.startsWith("get") ? 3 : 2;String key = name.substring(i, i + 1).toLowerCase() + name.substring(i + 1);// 此处调用了一些类方法Object value = method.invoke(this);if (value != null) {buf.append(" ");buf.append(key);buf.append("=\"");buf.append(value);buf.append("\"");}}} catch (Exception e) {logger.warn(e.getMessage(), e);}}buf.append(" />");return buf.toString();} catch (Throwable t) {logger.warn(t.getMessage(), t);return super.toString();}}

哦!看到这里就恍然大悟了。

可能是为了打印日志更详细,AbstractConfig的toString方法里利用反射调用了其相关的一些方法来获取值。而我们的referenceBean是继承自这个AbstractConfig的toString方法的,其getObject方法又正好满足这里的条件,所以就提前被调用了。(注意这里因为check=true,服务不可用而抛出的异常被catch给吃掉了

按道理说这里提前调用了,应该也不影响后续的调用生成代理逻辑呀!
但是我在debug中,发现后续的调用居然直接被跳过了!
它是如何跳过的呢?

现在我们回头看看check字段被解析的方法,它的入口在com.alibaba.dubbo.config.ReferenceConfig#init,该方法对类变量initialized进行了校验。

  • 第一次执行此方法的时候initialized为false,可以进入下面check的解析与服务可用性校验的逻辑
  • 第二次执行时,initialized为true,导致方法执行直接被中断,无法执行到check字段被解析的地方了
private void init() {if (initialized) {return;}initialized = true;......
}

也就是说,上述的toString方法里先调用了这个方法,所以到真正需要调用这个方法的地方,反而因为已经initialized了,被直接拦截了!真让人哭笑不得~

综合以上因素,可以总结如下:

  1. 由于打印日志的需要,提前调用了ReferenceBean的初始化方法,理论上检测服务可用性而抛出的异常,被日志打印的catch捕获了!
  2. 由于ReferenceBean初始化进行了次数校验,只有第一次可以进入执行。所以第二次真正应当调用的地方,反而被拦截了

这样,因为一行日志打印,就出现null指针异常了。。。

验证

上面已经说明了,因为打印了ReferenceBean这个对象,导致调用父类toString方法,提前完成了init(异常又被toString方法捕获处理了),从而无法正确生成代理,且又能够正常启动。
我们看到,调用生成代码的逻辑中加了判断

if (logger.isInfoEnabled()) {logger.info(bean + " has been built.");
}

因此只有日志级别达到了info级别,才会打印这个对象。那么如果我调整一下日志,不打印日志,或者打印日志级别为error,会发生什么情况呢?

 <logger name="com.alibaba.dubbo" level="ERROR"></logger>

修改log4j2.xml中的日志级别为error,重新跑一下上述异常的测试场景,我们发现@Reference注解配置Dubbo的应用已经无法启动了,报错与XML配置场景一致。
这说明我们的猜测是正确的!日志级别(小于等于info)居然真的导致Dubbo出现空指针异常!

其他场景说明

  • 为啥XML配置不会出现空指针?

因为XML配置走的调用路径与注解不一样啊,XML配置不会走到方法com.alibaba.dubbo.config.spring.beans.factory.annotation.AbstractAnnotationConfigBeanBuilder#build中哦

  • 为啥注解配置 + check=false 能够正确生成代理?

因为check=false不会检查服务状态,所以toString方法调用时,仍然可以正确完成初始化,也就是说不会影响后面代理的生成逻辑

一些建议

这个bug在2.6.5版本已经修复了,但是许多公司产线上跑着的Dubbo版本,依然低于此版本,所以仍然有出现这个问题的可能。

  • 最好的修复建议是升版本,版本的选择可以参考
    dubbo个版本总结与升级建议

  • 如果无法升版本,那么尽量使用xml配置吧

  • 如果依然无法使用xml配置,那么在@Reference注解处加一个 “check=false” 应该是可以的吧

  • 如果依然不行的话,那么你只能通过运维保证,按照服务依赖的顺序来发布了

测试代码

本文所用测试代码 点击下载

debug中的坑

下面分享一下在调试过程中遇到的一些坑吧!

在排查这个问题过程中,我发现IDEA调试时,默认是开启toString预览的!也就是说会默认调用对象的toString方法。这对于Dubbo空指针问题的调试,无疑是雪上加霜!调试着,突然就跳去了一个奇怪的地方,着实让人恼火。

好在IDEA提供了关闭的选项,IDEA debug关闭toString预览方法如下

不要勾选复选框
Build,Execution,Deployment -> Debugger -> Data Views -> Java -> Enable ‘toString()’ object views

番外篇

如果你从GitHub上拉取Dubbo 2.6.5版本的代码,可以看到此处的修复代码如下:

public final B build() throws Exception {checkDependencies();B bean = doBuild();configureBean(bean);if (logger.isInfoEnabled()) {logger.info("The bean[type:" + bean.getClass().getSimpleName() + "] has been built.");}return bean;}

你看,不再直接打印bean了,而是改为直接展示simpleName了。

如果你细心一点,查看一下提交记录,可以看到此问题是 小马哥2018-10-19 修复的。修复的备注包含了Github issue号

根据这个2657号,去GitHub上Dubbo项目下搜索issue,追踪溯源,你能看到原始的问题:Dubbo use diferent behavior when log’s level greater than info

原来早就有人发现了相同的问题了。。。
看来下次看到这类问题,还是需要去官方代码库中看看有没有人问过类似的问题,这样可以减少我们很多的时间和精力!

震惊!日志级别居然可能导致Dubbo出现空指针异常相关推荐

  1. Go 学习笔记(54)— Go 第三方库之 uber-go/zap/lumberjack(记录日志到文件、支持自动分割日志、支持日志级别、打印调用文件、函数和行号)

    1. 简要说明 zap 是 uber 开源的 Go 高性能日志库,支持不同的日志级别, 能够打印基本信息等,但不支持日志的分割,这里我们可以使用 lumberjack 也是 zap 官方推荐用于日志分 ...

  2. log4j的日志级别(ssm中log4j的配置)

    log4j定义了8个级别的log(除去OFF和ALL,可以说分为6个级别),优先级从高到低依次为:OFF.FATAL.ERROR.WARN.INFO.DEBUG.TRACE. ALL. 1. ALL ...

  3. Logback中使用TurboFilter实现日志级别等内容的动态修改

    可能看到这个标题,读者会问:要修改日志的级别,不是直接修改log.xxx就好了吗?为何要搞那么复杂呢? 所以,先说一下场景,为什么要通过TurboFilter去动态的修改日志级别.我们在使用Java开 ...

  4. Logger日志级别说明及设置方法、说明 (zhuan)

    http://blog.csdn.net/rogger_chen/article/details/50587920 ****************************************** ...

  5. linux 内核日志等级,Linux系统中日志级别详情

    日志信息分类 1.等级由低到高:debug 2.区别: debug 级别最低,可以随意的使用于任何觉得有利于在调试时更详细的了解系统运行状态的东东: info  重要,输出信息:用来反馈系统的当前状态 ...

  6. LOG4J日志级别详解

    日志记录器(Logger)是日志处理的核心组件. org.apache.log4j.Level类提供以下级别,但也可以通过Level类的子类自定义级别. Level 描述 ALL 各级包括自定义级别 ...

  7. java日志级别的作用_Java系统日志级别对性能的影响性

    先介绍下java系统的日志 日志框架:是一种日志接口,不负责具体的日志输出形式(有点类似于JDBC),可以灵活的切换日志输出形式.常见的日志框架有slf4j.jcl,只提供Logger.LoggerF ...

  8. 日志级别 debug info warn eirror fatal

    日志级别 debug info warn eirror fatal 软件中总免不了要使用诸如 Log4net, Log4j, Tracer 等东东来写日志,不管用什么,这些东东大多是大同小异的,一般都 ...

  9. 日志级别动态调整——小工具解决大问题

    随着外卖业务的快速发展,业务复杂度不断增加,线上系统环境有任何细小波动,对整个外卖业务都可能产生巨大的影响,甚至形成灾难性的雪崩效应,造成巨大的经济损失.每一次客诉.系统抖动等都是对技术人员的重大考验 ...

最新文章

  1. Linux下用iptables做端口映射
  2. [白领会]在职场高级技能
  3. 这是我见过解释java内部类最详细的一篇文章了
  4. python爬虫外快_我用Python爬虫挣钱的那些事
  5. SCOM 2016 配置报警邮件 (下)
  6. [USACO13FEB]Tractor【二分 + BFS】
  7. Unity3d--AR/MR 技术
  8. PCB设计的工艺流程
  9. CentOS 7 安装 Weadmin(ITOSS)
  10. 【bzoj1406】【AHOI2007】【密码箱】【数论】
  11. 编程程软件测试学院3周年 为你破解入职大厂的终极奥秘
  12. node.js解析word文档
  13. 怎么用python画房子_用python画一个小房子
  14. Java工程师培训课(十七【新的领域】)
  15. linux运行jar的几种方式
  16. meanshift 与 camshift 跟踪算法比较
  17. CMOS图像传感器——Remosaic技术
  18. veiw pad 7寸 android4.2,全国首款Android+Win7双系统平板电脑ViewPad 10登场
  19. html固定页脚布局及样式,html页脚固定在底部的方法
  20. win10家庭版解决VMvare开启虚拟机提示,禁用Device/Credential Guard不兼容问题完整解决方案,亲测!

热门文章

  1. ABAQUS划分网格
  2. 【Python语言基础】——Python 文件写入
  3. 小程序服务器端端接口,微信小程序:后端服务接口(WordPress)
  4. ipad分屏_ipad os正式推送,分屏多任务操作成最大亮点
  5. ATE测试几个中断异常的处理
  6. oracle 注册程序,Oracle 12c New Feature: Listener注册进程LREG
  7. Html table 页 实现点击选中tr行 改变背景颜色
  8. 胡志明市堤岸——全球最大的唐人街
  9. MQ基本使用及重点内容
  10. 插入排序算法(InsertionSort)