Проблемы и решения, вызванные циклическими ссылками

задняя часть тестовое задание
Проблемы и решения, вызванные циклическими ссылками

Существуют проблемы, которые могут быть вызваны циклическими ссылками в возвращаемых результатах.

предисловие

На тестовой платформе компании тестируется только что написанный интерфейс RPC, но обнаруживается, что возвращается исключение, которое невозможно преобразовать в POJO:

image.png

Сначала я думал, что это просто проблема с бизнес-кодом, но оказалось, что проблема не так проста!

Устранение неполадок идей

проблема с бизнес-кодом

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

image.png image.png

Этот результат был для меня неожиданным, поэтому я использовал идею удаленной отладки для дальнейшего подтверждения, и результат вернулся нормально. Таким образом, можно сделать вывод, что с бизнес-кодом проблем нет.

Проблема с RPC-фреймворком

Поскольку это не проблема с бизнес-кодом, возможно ли, что проблема связана с инфраструктурой RPC, используемой моим сервером? И внимательно наблюдайте за информацией об исключении, отображаемой тестовой платформой, вы можете видеть, что исключение генерируется в hsf (инфраструктура RPC компании), а информация об исключении также является исключением преобразования объекта POJO, поэтому я смело предполагаю, что сериализация появился серверный RPC-фреймворк.. вопрос.

image.png

Итак, согласно информации об исключении, я установил точку останова в том месте, где было выброшено исключение для отладки, и результат снова превзошел мои ожидания — точка останова не вошла и выдала исключение. Поэтому проблема сериализации на стороне сервера исключена.

Проблема с сервером тестовой платформы

Поскольку это не проблема сериализации на стороне моего сервера, может ли это быть проблемой сериализации на стороне сервера тестовой платформы? Сервер тестовой платформы должен отображать результат удаленного вызова внешнего интерфейса в виде json, а используемый нами протокол сериализации фреймворка RPC — hessain2, поэтому после того, как сервер тестовой платформы получит возвращаемое значение интерфейс, его также необходимо сериализовать, так как json отображается во внешнем интерфейсе.

Спросив разработчиков RPC-фреймворка, я узнал, что сериализация json тестовой платформы в настоящее время использует jackson, поэтому я использую jackson для сериализации результата локально, и результат вызывает исключение:

image.png

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

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

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

решение

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

Gson

В процессе использования Gson выявляется еще одна проблема этого сложного класса — в классе есть член с тем же именем, что и его член родительского класса, из-за чего Gson генерирует исключение IllegalArgumentException.В настоящее время класс не может быть изменен , поэтому решение Gson исключено.

fastjson

Во-вторых, широко используется открытый исходный код компании fastjson.Первоначально интерфейс toJSONString использовался непосредственно для сериализации, и в результате возникало исключение нулевого указателя. Чтобы выяснить это, я начал копаться в его исходном коде.

Чтение кода com.alibaba.fastjson.serializer.JavaBeanSerializer показывает:

image.png

fastjson получает члены и сериализует их через геттеры, а геттеры здесь читают код com.alibaba.fastjson.util.TypeUtils.computeGetters:

image.png

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

Чтобы решить эту проблему, fastjson поддерживает параметр SerializerFeature.IgnoreNonFieldGetter после 1.2.7, который можно добавить прямо в интерфейсе toJSONString.

До сих пор результат сериализации раскрывался гладко, и проблема была решена. Для нашего сложного типа возвращаемого значения это также обнажает яму, на которую легко наступить при сериализации json.Объекты, которые необходимо сериализовать в будущем, должны максимально следовать нескольким принципам:

  1. избегайте циклических зависимостей;
  2. Подкласс не определяет член с тем же именем, что и суперкласс;
  3. Избегайте определения методов получения/установки для переменных, не являющихся членами.

Принцип разрешения круговой ссылки

Сценарии, в которых циклические ссылки вызывают проблемы, можно разделить на сериализацию и создание объектов.Предполагается, что объекты класса A ссылаются на объекты класса B, а объекты класса B ссылаются на объекты класса A:

  1. Когда сериализуется объект A, одновременно сериализуется объект B. Когда сериализуется объект B, в свою очередь сериализуется объект A. Если это реализовано рекурсивно, это в конечном итоге приведет к переполнению стека. это реализовано в цикле, это приведет к бесконечному циклу;
  2. Когда объект A создан, объект B должен быть загружен, а объект B, в свою очередь, должен быть загружен с A, что в конечном итоге приводит к сбою создания объекта.

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

fastjson/hessian

Вышеупомянутые jackson и Gson не поддерживают сериализацию объектов с циклическими ссылками, а fastjson/hessain — типичное решение для циклических ссылок в сценариях сериализации. Чтение кода com.alibaba.fastjson.serializer.JavaBeanSerializer показывает:

image.png

У него есть метод специально для циклических ссылок.Здесь объект фактически кешируется.Если ссылочный объект находится в кеше, он не будет сериализоваться дальше, а заменится символом ref.Правила следующие:

грамматика описывать
{"ССЫЛКА ":" \”} ссылочный корневой объект
{"$ref":"@"} цитировать себя
{"$ref":".."} ссылка на родительский объект
{"$ref":"../.."} родительский объект, ссылающийся на родительский объект
{"ссылка":\.members[0].reportTo”} ссылки на основе пути

Hessian на самом деле решает проблему циклической ссылки таким же образом, прочтите com.caucho.hessian.io.AbstractSerializer, чтобы увидеть:

image.png

Метод здесь тот же, перед каждой сериализацией можно судить, есть ли она в кеше.

Spring

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

Кэш уровня 1: кеш полностью созданных объектов; Кэш уровня 2: кеш объектов, которые создаются, и некоторые элементы еще не загружены; Кэш уровня 3: Кэш, в котором хранится метод создания объекта (то есть кеш, в котором хранится фабрика, а не сам объект).

Предположим, что объект класса A относится к объекту класса B, а объект класса B относится к объекту класса A. В процессе создания объекта класса A необходимо загрузить объект B. В это время объект B будет искать в кеше первого уровня.Кэш первого уровня ищет.Если его еще нет, то он найдет метод для создания B из кеша третьего уровня, и создаст "голый" бин (бин без объекта-члена), поместите его в кэш второго уровня, а затем загрузите объект в A. При этом метод создания B в кэше третьего уровня будет удален, чтобы предотвратить повторное создание, и, наконец, Объект будет помещен в кеш первого уровня. Когда объект B создан, объект A может быть найден и загружен непосредственно в кэш первого уровня и, наконец, помещен в кэш первого уровня.

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

Суммировать

Подведем некоторые итоги со стороны разработки и со стороны инструментов:

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