原创 | FastJson稍微使用不当就会导致StackOverflow

△Hollis, 一个对Coding有着怪异追求的人△

这是Hollis的第 235 篇原创分享

作者 l Hollis
泉源 l Hollis(ID:hollischuang)
对于宽大的开发人员来说,FastJson人人一定都不生疏。

FastJson(https://github.com/alibaba/fastjson)是阿里巴巴的开源JSON剖析库,它可以剖析JSON花样的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

它具有速度快、使用普遍、测试完整以及使用简朴等特点。然则,虽然有这么多优点,然则不代表着就可以随便使用,由于若是使用的方式不正确的话,就可能导致StackOverflowError。而StackOverflowError对于程序来说是无疑是一种灾难。

笔者在一次使用FastJson的历程中就遇到了这种情形,厥后经由深入源码剖析,领会这背后的原理。本文就来从情景再现看是抽丝剥茧,带人人看看坑在哪以及若何避坑。

问题再现

FastJson可以辅助开发在Java Bean和JSON字符串之间相互转换,以是是序列化经常使用的一种方式。

有许多时刻,我们需要在数据库的某张表中保留一些冗余字段,而这些字段一样平常会通过JSON字符串的形式保留。好比我们需要在订单表中冗余一些买家的基本信息,如JSON内容:

{
    "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工具转换成字符串,可以有两种选择:

  • 1、基于属性。
  • 2、基于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",然后我们再有的放矢。

,

以太坊统计网

www.326681.com采用以太坊区块链高度哈希值作为统计数据,联博以太坊统计数据开源、公平、无任何作弊可能性。联博统计免费提供API接口,支持多语言接入。

,

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

1、修改方式名

首先我们可以通过修改方式名的方式解决这个问题,我们把getJsonString方式的名字改一下,只要不以get开头就可以了,如改为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);
    }
}

2、使用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);
    }
}

3、使用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的话,想要制止这个问题,可以接纳以下方式:

  • 1、方式名不以get开头
  • 2、使用@JSONField(serialize = false)修饰目的方式
  • 3、使用@JSONType修饰该Bean,并ignore掉方式对应的属性名(getXxx -> xxx)
    最后,作者之以是写这篇文章,是由于在工作中真的实实在在的碰到了这个问题。

发生问题的时刻,我马上想到改个方式名,把getJsonString改成了toJsonString解决了这个问题。由于我之前看到过关于FastJson的简朴原理。

厥后想着,既然FastJson设计成通过getter来举行序列化,那么他一定提供了一个口子,让开发者可以指定某些以get开头的方式不介入序列化。

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

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

以上就是我 发现问题——>剖析问题——>解决问题——>问题的升华 的全历程,希望对你有辅助。

通过这件事,笔者悟出了一个原理:

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

希望通过这样一篇小文章,可以让你对这个问题有个基本的印象,万一某一天遇到类似的问题,你可以马上想到Hollis似乎写过这样一篇文章。足矣!

迎接人人关注Java之道民众号,也会定期公布原创的Java手艺文章~

  • MORE | 更多精彩文章 -

  • Arthas - Java 线上问题定位处置的最终利器
  • 再有人问你为什么MySQL用B+树做索引,就把这篇文章发给她
  • 双十一,连当当官方都希望你薅的羊毛竟然被我发现了!
  • 看完这篇还不领会Nginx,那我就哭了!

若是你喜欢本文,
请长按二维码,关注 Hollis.

转发至朋友圈,是对我最大的支持。

转发+在看,让更多人瞥见。