它具有速度快、使用广泛、测试完备以及使用简单等特点。但是,虽然有这么多优点,但是不代表着就可以随便使用,因为如果使用的方式不正确的话,就可能导致StackOverflowError。而StackOverflowError对于程序来说是无疑是一种灾难。 笔者在一次使用FastJson的过程中就遇到了这种情况,后来经过深入源码分析,了解这背后的原理。本文就来从情景再现看是抽丝剥茧,带大家看看坑在哪以及如何避坑。
{
"buyerName":"Hollis",
"buyerWechat":"hollischuang",
"buyerAgender":"male"
}
因为这些字段被冗余下来,必定要有地方需要读取这些字段的值。所以,为了方便使用,一般也对定义一个对应的对象。
这里推荐一个IDEA插件——JsonFormat,可以一键通过JSON字符串生成一个JavaBean。我们得到以下Bean:
public class BuyerInfo {
/**
* buyerAgender : male
* buyerName : Hollis
* buyerWechat : hollischuang@qq.com
*/
private String buyerAgender;
private String buyerName;
private String buyerWechat;
public void setBuyerAgender(String buyerAgender) { this.buyerAgender = buyerAgender;}
public void setBuyerName(String buyerName) { this.buyerName = buyerName;}
public void setBuyerWechat(String buyerWechat) { this.buyerWechat = buyerWechat;}
public String getBuyerAgender() { return buyerAgender;}
public String getBuyerName() { return buyerName;}
public String getBuyerWechat() { return buyerWechat;}
}
然后在代码中,就可以使用FastJson把JSON字符串和Java Bean进行互相转换了。如以下代码:
Order order = orderDao.getOrder();
// 把JSON串转成Java Bean
BuyerInfo buyerInfo = JSON.parseObject(order.getAttribute(),BuyerInfo.class);
buyerInfo.setBuyerName("Hollis");
// 把Java Bean转成JSON串
order.setAttribute(JSON.toJSONString(buyerInfo));
orderDao.update(order);
有的时候,如果有多个地方都需要这样互相转换,我们会尝试在BuyerInfo中封装一个方法,专门将对象转换成JSON字符串,如:
public class BuyerInfo {
public String getJsonString(){
return JSON.toJSONString(this);
}
}
但是,如果我们定义了这样的方法后,我们再尝试将BuyerInfo转换成JSON字符串的时候就会有问题,如以下测试代码:
public static void main(String[] args) {
BuyerInfo buyerInfo = new BuyerInfo();
buyerInfo.setBuyerName("Hollis");
JSON.toJSONString(buyerInfo);
}
运行结果:
可以看到,运行以上测试代码后,代码执行时,抛出了StackOverflow。 从以上截图中异常的堆栈我们可以看到,主要是在执行到BuyerInfo的getJsonString方法后导致的。 那么,为什么会发生这样的问题呢?这就和FastJson的实现原理有关了。
关于Java Bean中的getter/setter方法的定义其实是有明确的规定的,参考JavaBeans(TM) Specification而我们所常用的JSON序列化框架中,FastJson和jackson在把对象序列化成json字符串的时候,是通过遍历出该类中的所有getter方法进行的。Gson并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成json。 不同的框架进行不同的选择是有着不同的思考的,这个大家如果感兴趣,后续文字可以专门介绍下。 那么,我们接下来深入一下源码,验证下到底是不是这么回事。 分析问题的时候,最好的办法就是沿着异常的堆栈信息,一点点看下去。我们再来回头看看之前异常的堆栈:
我们简化下,可以得到以下调用链:
BuyerInfo.getJsonString
-> JSON.toJSONString
-> JSONSerializer.write
-> ASMSerializer_1_BuyerInfo.write
-> BuyerInfo.getJsonString
是因为在FastJson将Java对象转换成字符串的时候,出现了死循环,所以导致了StackOverflowError。
调用链中的ASMSerializer_1_BuyerInfo,其实是FastJson利用ASM为BuyerInfo生成的一个Serializer,而这个Serializer本质上还是FastJson中内置的JavaBeanSerizlier。
读者可以自己试验一下,比如通过如下方式进行degbug,就可以发现ASMSerializer_1_BuyerInfo其实就是JavaBeanSerizlier。 
之所以使用ASM技术,主要是FastJson想通过动态生成类来避免重复执行时的反射开销。但是,在FastJson中,两种序列化实现是并存的,并不是所有情况都需要通过ASM生成一个动态类。读者可以尝试将BuyerInfo作为一个内部类,重新运行以上Demo,再看异常堆栈,就会发现JavaBeanSerizlier的身影。那么,既然是因为出现了循环调用导致了StackOverflowError,我们接下来就将重点放在为什么会出现循环调用上。
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
SerializeWriter out = serializer.out;
// 省略部分代码
final FieldSerializer[] getters = this.getters;//获取bean的所有getter方法
// 省略部分代码
for (int i = 0; i < getters.length; ++i) {//遍历getter方法
FieldSerializer fieldSerializer = getters[i];
// 省略部分代码
Object propertyValue;
// 省略部分代码
try {
//调用getter方法,获取字段值
propertyValue = fieldSerializer.getPropertyValue(object);
} catch (InvocationTargetException ex) {
// 省略部分代码
}
// 省略部分代码
}
}
以上代码,我们省略了大部分代码之后,可以看到逻辑相对简单:就是先获取要序列化的对象的所有getter方法,然后遍历方法进行执行,视图通过getter方法获得对应的属性的值。
但是,当调用到我们定义的getJsonString方法的时候,进而会调用到JSON.toJSONString(this),就会再次调用到JavaBeanSerizlier的write。如此往复,形成死循环,进而发生StackOverflowError。
所以,如果你定义了一个Java对象,定一个了一个getXXX方法,并且在该方法中调用了JSON.toJSONString方法,那么就会发生StackOverflowError!
final FieldSerializer[] getters;
if (out.sortField) {
getters = this.sortedGetters;
} else {
getters = this.getters;
}
可见,无论是this.sortedGetters还是this.getters,都是JavaBeanSerizlier中的属性,那么就继续往上找,看看JavaBeanSerizlier是如何被初始化的。
通过调用栈追根溯源,我们可以发现,JavaBeanSerizlier是在SerializeConfig的成员变量serializers中获取到的,那么继续深入,就要看SerializeConfig是如何被初始化的,即BuyerInfo对应的JavaBeanSerizlier是如何被塞进serializers的。
通过调用关系,我们发现,SerializeConfig.serializers是通过SerializeConfig.putInternal方法塞值的:
而getObjectWriter中有关于putInternal的调用:
putInternal(clazz, createJavaBeanSerializer(clazz));
这里面就到了我们前面提到的JavaBeanSerializer,我们知道createJavaBeanSerializer是如何创建JavaBeanSerializer的,并且如何设置其中的setters的就可以了。
private final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy);
if (beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz)) {
return MiscCodec.instance;
}
return createJavaBeanSerializer(beanInfo);
}
重点来了,TypeUtils.buildBeanInfo就是重点,这里面就到了我们要找的内容。
buildBeanInfo调用了 computeGetters,深入这个方法,看一下setters是如何识别出来的。部分代码如下:
for (Method method : clazz.getMethods()) {
if (methodName.startsWith("get")) {
if (methodName.length() < 4) {
continue;
}
if (methodName.equals("getClass")) {
continue;
}
....
}
}
这个方法很长很长,以上只是截取了其中的一部分,以上只是做了个简单的判断,判断方法是不是以'get'开头,然后长度是不是小于3,在判断方法名是不是getClass,等等一系列判断。。。
下面我简单画了一张图,列出了其中的核心判断逻辑:
那么,通过上图,我们可以看到computeGetters方法在过滤getter方法的时候,是有一定的逻辑的,只要我们想办法利用这些逻辑,就可以避免发生StackOverflowError。 这里要提一句,下面将要介绍的几种方法,都是想办法使目标方法不参与序列化的,所以要特别注意下。但是话又说回来,谁会让一个JavaBean的toJSONString进行序列化呢?
public class Main {
public static void main(String[] args) {
BuyerInfo buyerInfo = new BuyerInfo();
buyerInfo.setBuyerName("Hollis");
JSON.toJSONString(buyerInfo);
}
}
class BuyerInfo {
private String buyerAgender;
private String buyerName;
private String buyerWechat;
//省略setter/getter
public String toJsonString(){
return JSON.toJSONString(this);
}
}
public class Main {
public static void main(String[] args) {
BuyerInfo buyerInfo = new BuyerInfo();
buyerInfo.setBuyerName("Hollis");
JSON.toJSONString(buyerInfo);
}
}
class BuyerInfo {
private String buyerAgender;
private String buyerName;
private String buyerWechat;
//省略setter/getter
@JSONField(serialize = false)
public String getJsonString(){
return JSON.toJSONString(this);
}
}
public class Main {
public static void main(String[] args) {
BuyerInfo buyerInfo = new BuyerInfo();
buyerInfo.setBuyerName("Hollis");
JSON.toJSONString(buyerInfo);
}
}
@JSONType(ignores = "jsonString")
class BuyerInfo {
private String buyerAgender;
private String buyerName;
private String buyerWechat;
//省略setter/getter
public String getJsonString(){
return JSON.toJSONString(this);
}
}