35

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

 4 years ago
source link: https://juejin.im/post/5dc8cff451882559593d5274
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2019年11月11日阅读 2914

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。

1

它具有速度快、使用广泛、测试完备以及使用简单等特点。但是,虽然有这么多优点,但是不代表着就可以随便使用,因为如果使用的方式不正确的话,就可能导致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);
}
复制代码

运行结果:

1

可以看到,运行以上测试代码后,代码执行时,抛出了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。

不同的框架进行不同的选择是有着不同的思考的,这个大家如果感兴趣,后续文字可以专门介绍下。

那么,我们接下来深入一下源码,验证下到底是不是这么回事。

分析问题的时候,最好的办法就是沿着异常的堆栈信息,一点点看下去。我们再来回头看看之前异常的堆栈:

1

我们简化下,可以得到以下调用链:

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。

1

之所以使用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方法塞值的:

1

而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,等等一系列判断。。。

下面我简单画了一张图,列出了其中的核心判断逻辑:

1

那么,通过上图,我们可以看到computeGetters方法在过滤getter方法的时候,是有一定的逻辑的,只要我们想办法利用这些逻辑,就可以避免发生StackOverflowError。

这里要提一句,下面将要介绍的几种方法,都是想办法使目标方法不参与序列化的,所以要特别注意下。但是话又说回来,谁会让一个JavaBean的toJSONString进行序列化呢?

1、修改方法名

首先我们可以通过修改方法名的方式解决这个问题,我们把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);
    }
}
复制代码

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好像写过这样一篇文章。足矣!

1

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK