起因

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

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

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

java 装换json对象 java jsonstring转对象_java


本人也喜欢在一些场景下将对象转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()来说事吧。

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。

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

java 装换json对象 java jsonstring转对象_java 装换json对象_02

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);
}
...

java 装换json对象 java jsonstring转对象_RocketMQ_03


每解析一个get方法,都会从ByteBuffer中读取相应数量的字节数据,当ByteBuffer的remaining长度小于要获取的字节数时就会抛BufferUnderflowException,是不是有点眼熟呢,没错,在最开始贴出的异常日志就出现了它的身影

java 装换json对象 java jsonstring转对象_RocketMQ_04


举个更直观的例子,假设你的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对象。