掘金 后端 ( ) • 2024-04-29 17:26

背景

  1. 最近同事创建了一个kotlin新项目,新项目在调用dubbo服务失败,消费者异常信息:com.alibaba.com.caucho.hessian.io.HessianProtocolException: expected map/object at java.lang.String ()
  2. 提供者无异常日志
  3. 代码写法和其他项目一致

代码

@NoArgsConstructor 
data class EewForwardDto ( 
    var msgId: String 
) : Serializable 

interface IEndpointApiService { 
    fun doDelivery(eewForwardDto: EewForwardDto) 
} 

fun test() { 
    val dto = EewForwardDto( msgId = "" ) 
    val res = iEndpointApiService.doDelivery(dto) 
    println(res) 
}

异常栈

image.png

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.xin.base.controller.TTTestController$$T.handleRequest(TTTestController$$T.java:128)
    at com.xin.base.controller.HttpServer$$.serve(HttpServer$$.java:45)
    at fi.iki.elonen.NanoHTTPD$HTTPSession.execute(NanoHTTPD.java:945)
    at fi.iki.elonen.NanoHTTPD$ClientHandler.run(NanoHTTPD.java:192)
    at java.lang.Thread.run(Thread.java:750)
Caused by: org.apache.dubbo.rpc.RpcException: Failed to invoke the method doDelivery in the service com.***.eew.ohs.expose.rpc.api.IEndpointApiService. Tried 3 times of the providers 
com.alibaba.com.caucho.hessian.io.HessianProtocolException: expected map/object at java.lang.String ()
    at com.alibaba.com.caucho.hessian.io.AbstractDeserializer.error(AbstractDeserializer.java:131)
    at com.alibaba.com.caucho.hessian.io.AbstractMapDeserializer.readObject(AbstractMapDeserializer.java:70)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2297)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2104)
    at org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject(Hessian2ObjectInput.java:101)
    at org.apache.dubbo.common.serialize.ObjectInput.readAttachments(ObjectInput.java:87)
    at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:165)
    at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:83)
    at org.apache.dubbo.remoting.transport.DecodeHandler.decode(DecodeHandler.java:57)
    at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:44)

排查

  1. 首先确认下提供者和消费者的依赖、配置是否有差异,经确认都是dubbo2.7.9、协议dubbo、序列化使用的是hessian
  2. 虽然提供者没有异常信息,但是通过org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:165)可以判断,消费者的请求已经到达提供者处
  3. 通过Hessian2Input.readObject异常日志可以确认是在反序列化时出了问题
  4. 本地搭建环境,在dubbo服务提供者debug代码
  5. 根据堆栈,debug到com.alibaba.com.caucho.hessian.io.AbstractMapDeserializer#readObject

image.png

图2

如图2所示,只要走到这个方法,不管序列化结果如何,最终都会报错
那为啥会走到AbstractMapDeserializer这个方法?

image.png

图3

image.png

图4

往前debug,从org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)开始

如图3、4所示,getDeserializer方法会返回com.alibaba.com.caucho.hessian.io.MapDeserializer,该类没有覆盖AbstractMapDeserializer的readObject方法,因此一定会报错

image.png

图5

如图5所示,hessian获取有误反序列化器的逻辑已经是com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject(java.lang.Class, java.lang.Class...)方法末尾了,猜测正常流程应该是走上面的case逻辑返回,打开其他项目验证也确实如此

那为啥新项目不会走到case逻辑?

image.png

图6

如图6所示,case是通过tag去匹配的,case_offset < _length 为true,_buffer是hessian的序列化数据,猜测是offset下标不对,导致tag读取有误,hessian应该是有序读取buffer,有可能是前面有步骤有误导致offset计算错误

image.png

图7

往前多打几个断点,如图7所示,发现in.readObject方法居然报错了,而这个方法刚好也会修改offset的值,猜测是这个方法失败,导致offset不符合预期,但由此也发现一个新问题,为啥dubbo异常日志没有输出,问题太多了,先解决readObject报错的问题

image.png

图8

如图8所示,com.alibaba.com.caucho.hessian.io.JavaDeserializer#instantiate使用有参构造函数实例化,但是构造函数传参却是null,EewForwardDto的msgId是不允许为null(kotlin特性),猜测是因为这个 导致实例化失败

验证猜想

image.png

图9

如图9所示,通过idea的debug工具给_constrctorArgs赋值,确实成功实例化了
debug下_constrctorArgs赋值处,看看为啥_constructorArgs参数值为啥为null

image.png

图10

image.png

图11

如图10、11所示,进入com.alibaba.com.caucho.hessian.io.JavaDeserializer#JavaDeserializer的getParamArg方法。
isPrimitive()方法用于判断类是否是基本类型,如果是基本类型则返回默认值,看这个逻辑也没问题,因为我们的 EewForwardDto的msgId参数就是string类型,那为啥一样的项目,其他项目的data class却可以正常实例化?

启动下其他项目,debug看下

@NoArgsConstructor
data class ReportDto(
    var businessKey: String,
    var datas: List<Any>
): Serializable
    

image.png

图12

如图12所示,排查发现其他项目会走无参构造示例化,因此不会报错,通过查询资料发现,kotlin data class默认是没有无参构造函数的,可是我们和其他项目一样,也加了lombok的@NoArgsConstructor注解,为啥效果不一样?

image.png

图13

如图13所示,通过arthus的jad命令反编译看下EewForwardDto(ps:idea的反编译对kotlin代码来说几乎不可用,不知道是不是我使用姿势不对), 发现确实没有无参构造,而其他成功调用的项目是有的

排查总结

那现在问题基本可以确定了,data class没有无参构造,所以导致dubbo反序列化失败

解决方法

通过查询资料发现,kotlin官方提供了一个maven插件,支持在编译时生成无参构造函数,排查其他用data class成功调用dubbo的项目,确实也引了这个插件,复用了lombok的NoArgsConstructor注解,作为插件生成无参构造的标识,因此实际上无参构造并不是lombok生成的

官方无参构造插件

<plugins>
    <plugin>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-plugin</artifactId>
        <version>${kotlin.version}</version>
        <configuration>
            <jvmTarget>1.8</jvmTarget>
            <compilerPlugins>
                <plugin>no-arg</plugin>
            </compilerPlugins>
            <pluginOptions>
                <option>no-arg:annotation=lombok.NoArgsConstructor</option>
            </pluginOptions>
        </configuration>
        <dependencies>
            <dependency>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-noarg</artifactId>
                <version>${kotlin.version}</version>
            </dependency>
        </dependencies>
    </plugin>
</plugins>

ps: dubbo提供者日志没有显示的问题,由于牵扯内容较多,留在其他篇章解决

参考文档

官方无参构造插件
kotlin中文文档
探索kotlin data class如何使用无参构造