目录

数据转JSONString异常,Write javaBean error

起因

一位同事在接收RocketMQ消息后,为了方便以后排查问题,顺便就用fastjson将消息转成JSONString来打log。模拟代码如下

1
2
3
4
public ConsumeConcurrentlyStatus  consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext  consumeConcurrentlyContext) {
   MessageExt msg = msgs.get(0);
   logger.info("收到MQ消息,msg={}", JSON.toJSONString(msg));
   ...

然后就报错了,JSON.toJSONString(msg)异常后,代码就不往下走了,关键是业务逻辑也不能执行了。

本人也喜欢在一些场景下将对象转JSONString打log来作为日后快速排查问题的依据,有时候因为业务需要,甚至将对象转JSONString后存储数据库,而且用的都是fastjson。

所以菊花一紧,觉得有必要排查下这个问题。

猜测

刚开始猜测是不是消息内容有序列化的问题,但是消息能够正常解析和使用,所以排除这种可能。

转而猜测会不会是fastjson的bug,当前使用的fastjson版本是1.2.7,果断升到官方最新版本1.2.44试了下,还是报一样的错。

改用google的gson来解析,成功解析并输出。所以fastjson被打上重点嫌疑人便签。

Do

最后直接分析fastjson源码,最终fastjson表示这个锅它不背。那为什么fastjson不行,而gson可以呢? 这是由于它们各自的解析方式不一样的导致的,fastjson解析的数据是来自对象的get方法,而gson的则来自于对象的属性。

之前的报错恰恰是由于MessageExt对象的get方法引起的。

MessageExt对象中有两个有趣的get方法,getBornHostBytes()和getStoreHostBytes()。

两个方法差不多,就拿getBornHostBytes()来说事吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public ByteBuffer getBornHostBytes() {
   return socketAddress2ByteBuffer(this.bornHost);
 }

public static ByteBuffer socketAddress2ByteBuffer(SocketAddress socketAddress) {
   ByteBuffer byteBuffer = ByteBuffer.allocate(8);
   return socketAddress2ByteBuffer(socketAddress, byteBuffer);
 }

private static ByteBuffer socketAddress2ByteBuffer(SocketAddress socketAddress, ByteBuffer byteBuffer) {
   InetSocketAddress inetSocketAddress = (InetSocketAddress)socketAddress;
   byteBuffer.put(inetSocketAddress.getAddress().getAddress(), 0, 4);
   byteBuffer.putInt(inetSocketAddress.getPort());
   byteBuffer.flip();
   return byteBuffer;
 }

具体方法功能就不多说了,问题就出在方法的返回类型ByteBuffer。fastjson会继续解析这个方法的实际返回类型HeapByteBuffer。

具体方法功能就不多说了,问题就出在方法的返回类型ByteBuffer。fastjson会继续解析这个方法的实际返回类型HeapByteBuffer。

最终的问题就出在HeapByteBuffer的get方法上(ps:fastjson解析的都是无参的get方法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 public char getChar() {
   return Bits.getChar(this, ix(nextGetIndex(2)), bigEndian);
 }

public int getInt() {
   return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
 }

public double getDouble() {
   return Bits.getDouble(this, ix(nextGetIndex(8)), bigEndian);
 }
 ...

每解析一个get方法,都会从ByteBuffer中读取相应数量的字节数据,当ByteBuffer的remaining长度小于要获取的字节数时就会抛BufferUnderflowException,

是不是有点眼熟呢,没错,在最开始贴出的异常日志就出现了它的身影

举个更直观的例子,假设你的ByteBuffer字节长度就是8,第一次你用getInt()获取到了4个字节的数据,第二次你用getDouble()想获取8个字节的数据时就会抛这个异常了,因为remaining这个时候的值是4,小于想获取的长度。

More

fastjson实际上是可以通过设置SerializerFeature规避这个问题的

方法一

JSON.toJSONString(msg, SerializerFeature.IgnoreNonFieldGetter); getBornHostBytes()方法在MessageExt并没有对应的属性bornHostBytes, 设置后,fastjson就会跳过getBornHostBytes()的解析。

方法二

JSON.toJSONString(msg, SerializerFeature.IgnoreErrorGetter); fastjson会忽略有问题的get异常解析,返回其它正常的解析数据。

SerializerFeature.IgnoreErrorGetter 在fastjson 1.2.7版本中没有,1.2.44版本中有。

另外,JSONObject.toJSONString和JSON.toJSONString本质上是相同的。

总结

吃一堑长一智,在我们实际开发中,DTO对象尽量使用失血模型,不要在get方法中做些不必要的操作。还有不要将DTO的get方法返回类型设为ByteBuffer哦。

如果没有特殊情况,RocketMQ的消息就打印body部分(真正的消息内容)就够看了,不要打印MessageExt对象。