Небольшое неправильное использование FastJson может привести к StackOverflow

Java Fastjson

GitHub 9.4k Star Путь Java-инженера к тому, чтобы стать богом, разве вы не хотите узнать об этом?

GitHub 9.4k Star Путь Java-инженера к тому, чтобы стать богом, разве вы не хотите узнать об этом?

Путь Java-инженера GitHub 9.4k Star к становлению богом, вы действительно уверены, что не хотите об этом узнать?

Для большинства разработчиков FastJson должен быть всем знаком.

FastJson (GitHub.com/alibaba/happening…) — это библиотека синтаксического анализа JSON с открытым исходным кодом от Alibaba, которая может анализировать строки в формате JSON, поддерживать сериализацию Java Beans в строки JSON и десериализацию из строк JSON в JavaBeans.

Он быстрый, широко используемый, хорошо протестированный и простой в использовании. Однако, несмотря на то, что у него так много преимуществ, это не означает, что вы можете использовать его небрежно, потому что, если вы используете его неправильно, это может вызвать StackOverflowError. И StackOverflowError, несомненно, является катастрофой для программы.

С этой ситуацией автор столкнулся один раз в процессе использования FastJson, а затем благодаря глубокому анализу исходного кода я понял принцип, лежащий в основе этого. Эта статья написана с точки зрения воспроизведения сцены и показывает, где находится яма и как ее избежать.

причина

FastJson может помочь разработчикам конвертировать между Java Beans и строками JSON, поэтому этот способ сериализации часто используется.

Во многих случаях нам нужно сохранить некоторые избыточные поля в таблице в базе данных, и эти поля обычно сохраняются в виде строк JSON. Например, нам нужно сделать избыточной некоторую базовую информацию о покупателях в таблице заказов, такую ​​как содержимое JSON:

{
    "buyerName":"Hollis",
    "buyerWechat":"hollischuang",
    "buyerAgender":"male"
}

Поскольку эти поля избыточны, должно быть место для чтения значений этих полей. Поэтому для удобства использования обычно определяется соответствующий объект.

Здесь мы рекомендуем подключаемый модуль IDEA — JsonFormat, который может генерировать JavaBean из строки JSON одним щелчком мыши. Получаем следующие бобы:

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 Beans друг в друга и друг из друга. Например, следующий код:

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 выбрасывается при выполнении кода.

Из аномального стека на снимке экрана выше видно, что он в основном возникает после выполнения метода getJsonString в BuyerInfo.

Итак, почему возникает эта проблема? Это связано с принципом реализации FastJson.

Принцип реализации FastJson

Вы можете обратиться к основам сериализации и десериализацииСериализация и десериализация объектов Java, которые здесь повторяться не будут.

Процесс сериализации FastJson заключается в преобразовании Java Bean в памяти в строку JSON, и после получения строки ее можно сохранить через базу данных и другими способами.

Итак, как FastJson преобразует Java Bean в строку?В Java Bean много атрибутов и методов.Какие атрибуты следует сохранить, а какие удалить?Каких принципов следует придерживаться?

Фактически, для платформы JSON, если вы хотите преобразовать объект Java в строку, у вас есть два варианта:

  • 1. На основе атрибутов.
  • 2. На основе сеттера/геттера

Определение методов получения/установки в Java Bean на самом деле четко определено, см.JavaBeans(TM) Specification

В нашей широко используемой среде сериализации JSON, когда FastJson и jackson сериализуют объекты в строки JSON, они обходят все методы получения в классе. Gson этого не делает, он проходит через рефлексию по всем свойствам в классе и сериализует их значения в json.

Разные фреймворки имеют разное мышление для разных вариантов.Если вам это интересно, вы можете представить это в последующем тексте.

Затем давайте покопаемся в исходном коде, чтобы проверить, так ли это.

При анализе проблемы лучше всего читать по стеку информацию об исключении. Вернемся назад и посмотрим на стек предыдущего исключения:

Упрощаем, можем получить следующую цепочку вызовов:

BuyerInfo.getJsonString 
    -> JSON.toJSONString
        -> JSONSerializer.write
            -> ASMSerializer_1_BuyerInfo.write
                -> BuyerInfo.getJsonString

Это связано с тем, что когда FastJson преобразует объекты Java в строки, возникает бесконечный цикл, что приводит к StackOverflowError.

ASMSerializer_1_BuyerInfo в цепочке вызовов на самом деле является Serializer, сгенерированным FastJson для BuyerInfo с использованием ASM, и этот Serializer, по сути, является JavaBeanSerizlier, встроенным в FastJson.

Читатели могут проверить это сами, например, выполнив degbug следующим образом, вы можете обнаружить, что ASMSerializer_1_BuyerInfo на самом деле является JavaBeanSerizlier.

Причина, по которой используется технология ASM, заключается в том, что FastJson хочет избежать накладных расходов на отражение во время повторного выполнения за счет динамического создания классов. Однако в FastJson две реализации сериализации сосуществуют, и не во всех случаях требуется генерировать динамический класс через ASM. Читатели могут попробовать использовать BuyerInfo в качестве внутреннего класса, повторно запустить приведенную выше демонстрацию, а затем посмотреть на стек исключений, вы найдете JavaBeanSerizlier.

Итак, поскольку StackOverflowError вызван вызовом цикла, мы сосредоточимся на том, почему происходит вызов цикла.

Принцип сериализации JavaBeanSerizlier

Мы уже знаем, что в процессе сериализации FastJson будет использоваться JavaBeanSerizlier, поэтому давайте посмотрим, что делает JavaBeanSerizlier и как он помогает сериализации FastJson.

В процессе сериализации FastJson вызовет метод записи JavaBeanSerizlier, давайте посмотрим на содержимое этого метода:

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) {
            // 省略部分代码
        }
        // 省略部分代码
    }
}

В приведенном выше коде, после того как мы опустили большую часть кода, мы можем видеть, что логика относительно проста: сначала получить все методы получения объекта, который нужно сериализовать, затем пройти методы для выполнения, и представление получает значение соответствующий атрибут через метод получения.

Однако, когда вызывается определенный нами метод getJsonString, а затем вызывается JSON.toJSONString(this), запись JavaBeanSerizlier вызывается снова. Таким образом образуется бесконечный цикл и возникает StackOverflowError.

Итак, если вы определяете объект Java, определяете метод getXXX и вызываете метод JSON.toJSONString в этом методе, произойдет StackOverflowError!

Как избежать StackOverflowError

Изучив исходный код FastJson, мы в основном обнаружили проблему, так как же избежать этой проблемы?

Начнем с исходного кода, так как метод записи JavaBeanSerizlier попытается получить все методы-геттеры объекта, давайте посмотрим, как он получит методы-геттеры, и какие методы будут распознаны им как «геттеры», а затем мы назначит правильное лекарство..

В методе записи JavaBeanSerizlier геттеры получаются следующим образом:

final FieldSerializer[] getters;

if (out.sortField) {
    getters = this.sortedGetters;
} else {
    getters = this.getters;
}

Можно увидеть, что и this.sortedGetters, и this.getters являются свойствами в JavaBeanSerizlier, затем продолжайте искать, чтобы увидеть, как инициализируется JavaBeanSerizlier.

Отслеживая источник стека вызовов, мы можем обнаружить, что JavaBeanSerizlier получен в сериализаторах переменных-членов SerializeConfig, поэтому, если мы углубимся, нам нужно увидеть, как инициализируется SerializeConfig, то есть как JavaBeanSerizlier, соответствующий BuyerInfo, вставлены в сериализаторы.

Через отношения вызова мы обнаружили, что SerializeConfig.serializers подставляется в значение через метод SerializeConfig.putInternal:

И есть вызов putInternal в getObjectWriter:

putInternal(clazz, createJavaBeanSerializer(clazz));

А вот и JavaBeanSerializer, о котором мы упоминали ранее.Мы знаем, как createJavaBeanSerializer создает JavaBeanSerializer и как устанавливать сеттеры.

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 вызывает calculateGetters, разверните этот метод, чтобы увидеть, как идентифицируются установщики. Часть кода выглядит следующим образом:

for (Method method : clazz.getMethods()) {
    if (methodName.startsWith("get")) {
            if (methodName.length() < 4) {
                continue;
            }

            if (methodName.equals("getClass")) {
                continue;
            }

            ....
    }
}

Этот метод очень длинный, и приведенное выше является лишь его частью.Вышеприведенное является просто простым суждением, чтобы определить, начинается ли метод с «получить», а затем, является ли длина меньше 3, при оценке того, является ли имя метода есть getClass и т.п. Ждите серию мнений. . .

Ниже я просто рисую картинку и перечисляю основную логику суждения:

Затем из приведенного выше рисунка видно, что метод calculateGetters имеет определенную логику при фильтрации методов-геттеров, и пока мы находим способ использовать эту логику, мы можем избежать появления StackOverflowError.

Здесь следует упомянуть, что несколько методов, которые будут представлены ниже, пытаются предотвратить участие целевого метода в сериализации, поэтому ему следует уделить особое внимание. Но опять же, кто будет сериализовать toJSONString JavaBean?

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.Эта аннотация может быть применена к методу.Если для его параметра сериализации установлено значение false, то этот метод не будет распознается как метод получения. , он не будет участвовать в сериализации.

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, которая используется для оформления классов и может указывать игнорирование и включение. Как и в приведенном ниже примере, StackOverflowError также можно избежать, если BuyerInfo определяется с помощью @JSONType(ignores = "jsonString").

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 Beans.

Но обратите особое внимание при его использовании, не вызывайте метод JSON.toJSONString в методе getXXX Java Bean, иначе это вызовет StackOverflowError.

Причина в том, что при сериализации FastJson получает все методы-геттеры в объекте в соответствии с рядом правил, а затем выполняет их последовательно.

Если вам необходимо определить метод для вызова JSON.toJSONString, чтобы избежать этой проблемы, вы можете использовать следующие методы:

  • 1. Имя метода не начинается с get
  • 2. Используйте @JSONField(serialize = false) для украшения целевого метода.
  • 3. Используйте @JSONType для украшения bean-компонента и игнорируйте имя атрибута, соответствующее методу (getXxx -> xxx)

Наконец, причина, по которой автор написал эту статью, заключается в том, что он действительно столкнулся с этой проблемой в своей работе.

Когда возникла проблема, я сразу подумал об изменении имени метода, и замена getJsonString на toJsonString решила проблему. Потому что я уже видел простые принципы работы с FastJson.

Позже я подумал, что раз FastJson предназначен для сериализации через геттер, то он должен обеспечивать открытие, чтобы разработчики могли указать, что некоторые методы, начинающиеся с get, не участвуют в сериализации.

Впервые я подумал, что такого рода дыры вообще реализуются через аннотации, поэтому открыл исходный код FastJson и нашел соответствующие аннотации.

Затем, воспользовавшись выходными, я внимательно изучил исходный код FastJson и досконально разобрался в его реальном принципе.

Это яВыявление проблемы -> анализ проблемы -> решение проблемы -> сублимация проблемыВесь процесс, я надеюсь помочь вам.

Благодаря этому автор осознал истину:

Я прочитал слишком много спецификаций разработки, но все равно пишу ошибки!

Я надеюсь, что с помощью такой небольшой статьи вы сможете получить общее представление об этой проблеме.Если вы однажды столкнетесь с подобной проблемой, вы можете сразу подумать, что Холлис, похоже, написал такую ​​​​статью. Достаточно!