今天帮同事看了个问题,该问题不复杂,只是表现出来没有头绪,在此简单记录。首先发现该问题是一个空指针异常,即调用方通过 Dubbo 调用消费端提供的方法时调用方的异常仅有一个空指针异常,没有其他有价值的信息。调用方的代码简化如下:
1 |
|
第 6 行将抛出空指针异常,异常栈帧如下:
1 | java.lang.NullPointerException |
提供者该方法实现的代码如下:
1 |
|
在提供者方能看到的异常信息如下:
1 | 2021-12-23 22:23:33,289 ERROR me.tianshuang.filter.DubboExceptionFilter:55 - [DUBBO] Got unchecked and undeclared exception which called by 192.168.1.9. service: me.tianshuang.service.TestService, method: queryByQuery, exception: java.lang.NullPointerException: null, dubbo version: 2.7.5, current host: 192.168.1.9 |
为何 TestServiceImpl.java:64
会触发空指针呢?难道 query
对象为空?可是在调用端明显创建了 TestQuery
的实例啊,于是在提供方跟踪了源码,发现与 TestQuery
的构造函数有关,我们的 TestQuery
类的部分源码简化如下:
1 |
|
在第 7 行将触发 IllegalArgumentException
,而该查询类为何创建了这样的构造函数呢,因为我们将 TestQuery
对应的表进行了分库分表,且选用的拆分键为 userId
,所以使用该工具类查询时,我们要求必须带有拆分键 userId
。而通过跟踪源码,我们发现在 JavaDeserializer.java at master 会调用 instantiate()
实例化 TestQuery
类,源码如下:
1 | protected Object instantiate() |
可以看出采用的反射进行实例化,而构造器及构造器的参数在当前 JavaDeserializer
实例化时会确定,代码位于 JavaDeserializer.java at master,核心部分如下:
1 | public JavaDeserializer(Class cl) { |
核心思想为选择一个 cost
最低的构造函数,然后调用 getParamArg
方法初始化每个构造函数参数的值,其中最为关键的为第 60 行,可以看出非原语类型会直接使用 NULL 作为参数的值。对应到以上这个例子,可以看出 userId
被初始化为空,然后反射调用构造函数,根据代码我们知道会触发构造函数中的 IllegalArgumentException
,而为什么调用方收到的是空指针异常呢?我们仔细查看提供方的线程栈帧:
1 | instantiate:311, JavaDeserializer (com.alibaba.com.caucho.hessian.io) |
会发现在 DecodeableRpcInvocation:139
行处理时对异常进行了 catch 操作,并且仅打印了日志,源码位于 DecodeableRpcInvocation.java at dubbo-2.7.5:
1 | args = new Object[pts.length]; |
可以看出,参数解码异常时仅打印了日志,且日志等级为 WARN
,并不是 ERROR
,当我们提供方的日志等级为 ERROR
时在提供方是看不到这个异常信息的,然后 query
参数的值为 NULL 进行提供方的调用,在方法体中触发了空指针异常,这就是消费端收到空指针异常的原因。我把提供方的日志等级调整后看到了以上打印的异常:
1 | 2021-12-23 23:14:20,947 WARN org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation:142 - [DUBBO] Decode argument failed: 'me.tianshuang.query.TestQuery' could not be instantiated, dubbo version: 2.7.5, current host: 192.169.1.21 |
知道原因后,我们将 TestQuery
类构造函数中的 Long userId
调整为了 long userId
,即当 Dubbo 序列化时先默认使用 0 填充,然后再通过反射设置 userId
属性的值,以实现反序列化,这样调整的原因是依然保证了 TestQuery
使用时必须设置拆分键,而不是简单的新增一个无参的构造函数。
在排查过程中,我发现在 Duboo 源码仓库中无法搜索到 JavaDeserializer
类,而发行的 jar
文件中含有该类,猜测是通过将依赖进行 shade
处理后打入了 jar
文件,查询源码后发现果然如此,JavaDeserializer
类源码位于 dubbo-hessian-lite,在 Dubbo 的依赖配置文件 pom.xml at dubbo-2.7.5 中进行了如下配置:
1 | <build> |
Reference
SEC-779: GrantedAuthorityImpl constructor throws IllegalArgumentException during Hessian deserialization · Issue #1038 · spring-projects/spring-security · GitHub
dubbo2.7.0 com.alibaba.com.caucho.hessian.io.HessianProtocolException: ‘com.alibaba.dubbo.common.URL’ could not be instantiated · Issue #3342 · apache/dubbo · GitHub
Optimize argument decode exception handling. by tianshuang · Pull Request #9490 · apache/dubbo · GitHub