专栏名称: HollisChuang
Developer
目录
相关文章推荐
51好读  ›  专栏  ›  HollisChuang

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

HollisChuang  · 掘金  ·  · 2019-11-11 03:06

正文

阅读 234

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

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

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

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

对于广大的开发人员来说,FastJson大家一定都不陌生。

FastJson( github.com/alibaba/fas… )是阿里巴巴的开源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 : [email protected]
     */
    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,我们接下来就将重点放在为什么会出现循环调用上。







请到「今天看啥」查看全文