FastJson稍微使用不当就会致使StackOverflow

GitHub 9.4k Star 的Java工程师成神之路 ,不来了解一下吗?java

GitHub 9.4k Star 的Java工程师成神之路 ,真的不来了解一下吗?git

GitHub 9.4k Star 的Java工程师成神之路 ,真的肯定不来了解一下吗?github

对于广大的开发人员来讲,FastJson你们必定都不陌生。数据库

FastJson(github.com/alibaba/fas… )是阿里巴巴的开源JSON解析库,它能够解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也能够从JSON字符串反序列化到JavaBean。json

它具备速度快、使用普遍、测试完备以及使用简单等特色。可是,虽然有这么多优势,可是不表明着就能够随便使用,由于若是使用的方式不正确的话,就可能致使StackOverflowError。而StackOverflowError对于程序来讲是无疑是一种灾难。markdown

笔者在一次使用FastJson的过程当中就遇到了这种状况,后来通过深刻源码分析,了解这背后的原理。本文就来从情景再现看是抽丝剥茧,带你们看看坑在哪以及如何避坑。oracle

原因

FastJson能够帮助开发在Java Bean和JSON字符串之间互相转换,因此是序列化常用的一种方式。框架

有不少时候,咱们须要在数据库的某张表中保存一些冗余字段,而这些字段通常会经过JSON字符串的形式保存。好比咱们须要在订单表中冗余一些买家的基本信息,如JSON内容:oop

{
    "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的实现原理有关了。

FastJson的实现原理

关于序列化和反序列化的基础知识你们能够参考Java对象的序列化与反序列化,这里再也不赘述。

FastJson的序列化过程,就是把一个内存中的Java Bean转换成JSON字符串,获得字符串以后就能够经过数据库等方式进行持久化了。

那么,FastJson是如何把一个Java Bean转换成字符串的呢,一个Java Bean中有不少属性和方法,哪些属性要保留,哪些要剔除呢,到底遵循什么样的原则呢?

其实,对于JSON框架来讲,想要把一个Java对象转换成字符串,能够有两种选择:

  • 一、基于属性。
  • 二、基于setter/getter

关于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,咱们接下来就将重点放在为何会出现循环调用上。

JavaBeanSerizlier序列化原理

咱们已经知道,在FastJson序列化的过程当中,会使用JavaBeanSerizlier进行,那么就来看下 JavaBeanSerizlier到底作了什么,他是如何帮助FastJson进行序列化的。

FastJson在序列化的过程当中,会调用JavaBeanSerizlier的write方法进行,咱们看一下这个方法的内容:

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!

如何避免StackOverflowError

经过查看FastJson的源码,咱们已经基本定位到问题了,那么如何避免这个问题呢?

仍是从源码入手,既然JavaBeanSerizlier的write方法会尝试获取对象的全部getter方法,那么咱们就来看下他究竟是怎么获取getter方法的,到底哪些方法会被他识别为"getter",而后咱们再对症下药。

在JavaBeanSerizlier的write方法中,getters的获取方式以下:

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进行序列化呢?

一、修改方法名

首先咱们能够经过修改方法名的方式解决这个问题,咱们把getJsonString方法的名字改一下,只要不以get开头就能够了。

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);
    }
}
复制代码

二、使用JSONField注解

除了修改方法名之外,FastJson还提供了两个注解可让咱们使用,首先介绍JSONField注解,这个注解能够做用在方法上,若是其参数serialize设置成false,那么这个方法就不会被识别为getter方法,就不会参加序列化。

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);
    }
}
复制代码

三、使用JSONType注解

FastJson还提供了另一个注解——JSONType,这个注解用于修饰类,能够指定ignores和includes。以下面的例子,若是使用@JSONType(ignores = "jsonString")定义BuyerInfo,则也可避免StackOverflowError。

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);
    }
}
复制代码

总结

FastJson是使用很是普遍的序列化框架,能够在JSON字符串和Java Bean之间进行互相转换。

可是在使用时要尤为注意,不要在Java Bean的getXXX方法中调用JSON.toJSONString方法,不然会致使StackOverflowError。

缘由是由于FastJson在序列化的时候,会根据一系列规则获取一个对象中的全部getter方法,而后依次执行。

若是必定要定义一个方法,调用JSON.toJSONString的话,想要避免这个问题,能够采用如下方法:

  • 一、方法名不以get开头
  • 二、使用@JSONField(serialize = false)修饰目标方法
  • 三、使用@JSONType修饰该Bean,并ignore掉方法对应的属性名(getXxx -> xxx)

最后,做者之因此写这篇文章,是由于在工做中真的实实在在的碰到了这个问题。

发生问题的时候,我马上想到改个方法名,把getJsonString改为了toJsonString解决了这个问题。由于我以前看到过关于FastJson的简单原理。

后来想着,既然FastJson设计成经过getter来进行序列化,那么他必定提供了一个口子,让开发者能够指定某些以get开头的方法不参与序列化。

第一时间想到通常这种口子都是经过注解来实现的,因而打开FastJson的源代码,找到了对应的注解。

而后,趁着周末的时间,好好的翻了一下FastJson的源代码,完全弄清楚了其底层的真正原理。

以上就是我 发现问题——>分析问题——>解决问题——>问题的升华 的全过程,但愿对你有帮助。

经过这件事,笔者悟出了一个道理:

看过了太多的开发规范,却依然仍是会写BUG!

但愿经过这样一篇小文章,可让你对这个问题有个基本的印象,万一某一天遇到相似的问题,你能够立刻想到Hollis好像写过这样一篇文章。足矣!

相关文章
相关标签/搜索