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

Uma ·
更新时间:2024-09-20
· 847 次阅读

你可能是被标题吸引过来的,但这确实是一次真实的产线事件。日志级别导致Dubbo出现空指针异常,这个是在查看源码排查问题之后,反推得到的结论。

文章所用Dubbo版本为2.6.3

问题复现 Consumer A服务依赖Provider B服务。 Consumer A先启动了,此时注册中心无B服务实现,而后Provider B启动。 两个系统均正常启动,但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的调用使用了@Reference注解,类似如下代码

@Service public class AuthServiceImpl implements AuthService { @Reference private UserService userService; @Override public String getNameById(String id) { return userService.getNameById(id); } }

这里我们注意到此处Dubbo服务引用使用的是@Reference注解形式,且未指定check参数(默认为true)。按照常理来说:

如果A依赖的服务B没有启动的话,A服务启动就应当报错,但是启动并没有 Dubbo服务上线后,应当可以自动负载均衡调用,不应当出现空指针异常

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

场景测试

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

若A服务依赖Dubbo服务B,当A服务先于B服务启动,根据配置方式的不同,会有以下几种场景
"); return buf.toString(); } catch (Throwable t) { logger.warn(t.getMessage(), t); return super.toString(); } }

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

现在我们回头看看check字段被解析的方法,它的入口在com.alibaba.dubbo.config.ReferenceConfig#init,该方法对类变量initialized进行了校验。
第一次执行此方法的时候initialized为false,可以进入下面check的解析与服务可用性校验的逻辑。
第二次执行时,initialized为true,导致方法执行直接被中断,无法执行到check字段被解析的地方了。

private void init() { if (initialized) { return; } initialized = true; ...... }

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

验证

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

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

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

修改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配置,那么在注解处加一个check=false应该是可以的吧
如果依然不加的话,那么你只能通过运维保证,按照服务依赖的顺序来发布了(新加的服务才会有影响)

debug中的坑

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

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

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

IDEA debug去除toString预览

番外篇

从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号
Github问题号
根据这个2657号,去GitHub上搜索issue,追踪溯源,你能看到原始的问题:Dubbo use diferent behavior when log’s level greater than info

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


作者:somehow1002



空指针 异常 dubbo 指针

需要 登录 后方可回复, 如果你还没有账号请 注册新账号