一位同事在接收RocketMQ消息后,为了方便以后排查问题,顺便就用fastjson将消息转成JSONString来打log。模拟代码如下
public ConsumeConcurrentlyStatus consumeMessage(List 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被打上重点嫌疑人便签。
最后直接分析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方法)
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,小于想获取的长度。
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对象。