В Java с незапамятных времен имеется замечательный механизм сериализации, позволяющий без особых умственных усилий сохранять сколь угодно сложные графы объектов в виде последовательности байтов.
Формат хранения хорошо документирован, примеров много, сериализованные объекты «весят» совсем немного, пересылаются по сети за один раз, есть масса возможностей для кастомизации… Звучит все это здорово, но только пока вы каким-то образом не останетесь одни.
какой-нибудь многомегабайтный бинарный файл, содержащий очень и очень ценные данные, которые нужны прямо сейчас.
Как можно залезть в этот файл голыми руками и понять, что хранится внутри этого огромного сериализованного объектного графа, не имея при этом исходного кода? На эти и многие другие вопросы можно получить ответы Сериализ — библиотека, которая позволит детально анализировать сериализованные Java-объекты ( сериализованная форма - это мой вариант перевода выражения серийные формы, я решил не уходить далеко от оригинала).
Таким образом, вы можете получить информацию об объекте, которая недоступна через его публичный API. Библиотека также является полезным инструментом при тестировании сериализации ваших собственных классов.
От переводчика: Суббота.
Вечер.
Ничего не предвещало работы в тот день, но вдруг вспоминаю, что было бы неплохо проверить, как обстоят дела на Hadoop-кластере, просто для облегчения совести — ведь проблема решена… В последние несколько дней довольно много задач стало заканчиваться с OutOfMemoryError на нашем Hadoop-кластере в продакшене, увеличить объем выделяемой памяти стало невозможно, и мы с ИТ-отделом потратили изрядное количество времени, пытаясь это исправить.
найди причину.
Закончилось все тем, что наш американский коллега вдумчиво посмотрел конфиги, поправил пару строк и сказал, что проблема решена.
И действительно, в пятницу все стало хорошо, и мы еще раз порадовались Сертифицированный разработчик Cloudera в команде.
Но не тут то было! Хадуп показал, что в эту злополучную субботу не было выполнено ни одно задание.
Причина сбоев была несколько иной, чем раньше: трекер задач не мог запустить задачу, поскольку ему не хватало памяти для загрузки XML-файла конфигурации задачи.
Я, конечно, сразу заинтересовался, что за чудовищные конфигурации там хранятся? Увы, большую его часть занимал сериализованный блоб размером в пятьдесят мегабайт. Блоб состоит из графа объектов десятка разных классов, исходников которых у меня, конечно, нет. Что можно сделать с этим многомегабайтным бинарником субботним вечером, используя только доступные инструменты? И здесь на сцену выходит мой спаситель: серийный анализ.
Пара строчек кода — и у меня полный дамп внутренностей сериализованного объекта, с названиями классов и полей.
Имея на руках полный дамп, нахожу проблему, включаю сжатие gzip для строкового словаря, патчю классы с помощью Дж.
Б.
Е.
.
Вуаля – проблема решена! Это, конечно, хак, но иногда без хаков ничего не сделаешь.
P.S. Библиотека старая, но на данный момент она пригодилась.
Честно говоря, некоторые применения, которые нашел для библиотеки автор, мне кажутся очень странными.
Ради бога, мне не дали узнать порт, а значит, он и не особо нужен! По моему мнению, лучшее применение этой технологии — отладка и устранение всех видов неполадок; в этой области ему действительно нет равных.
Собственно, статья:
Когда публичного API недостаточно
Причина написания библиотеки сериализации заключается в том, что я столкнулся с некоторыми проблемами, когда мне требовалась информация об объекте, которую я не мог получить через общедоступный API, но она была доступна через сериализованную форму.Например, у вас есть заглушка для удаленного объекта RMI, и вы хотите знать, к какому адресу или порту он будет подключаться или к какому Фабрика сокетов RMI (RMIClientSocketFactory) буду использовать.
Стандартный API RMI не предоставляет возможности извлечения информации из заглушки.
Чтобы заглушка функционировала после десериализации, эта информация должна присутствовать в сериализованной форме.
Следовательно, мы могли бы получить нужную нам информацию, если бы смогли каким-то образом разобрать сериализованную заглушку.
Второй пример взят из JMX API .
Запросы к серверу MBean представлены интерфейсом QueryExp .
Примеры QueryExp создаются с использованием методов Класс запроса .
Если ваш объект принадлежит QueryExp, как узнать, какие запросы он выполняет? JMX API не предлагает никакого способа это выяснить.
Информация должна быть представлена в сериализованном виде, чтобы, когда клиент сделает запрос к удаленному серверу, ее можно было восстановить на сервере.
Если мы увидим сериализованную форму, мы сможем определить, каким был запрос.
Второй пример — это то, что побудило меня написать эту библиотеку.
Существующие стандартные соединители JMX основаны на сериализации Java, поэтому им не требуется обрабатывать QueryExps каким-либо особым образом.
Но в новых веб-службах Разъем введен в ДжСР 262 XML используется для сериализации.
Как проанализировать QueryExp, а затем преобразовать его в XML? Ответ прост: коннектор WS использует версию этой библиотеки для просмотра сериализованного QueryExp. У всех этих примеров есть одна общая черта: они демонстрируют пробелы в соответствующих API. Это означает, что нам нужны методы, позволяющие извлекать информацию из заглушки RMI. Точно так же, как нам нужен способ преобразовать QueryExp обратно в исходный метод Query, который его породил.
(Достаточно даже стандартного синтаксического вывода toString().
) Но сейчас таких методов нет, и если нам нужен код, который будет работать с этими API в их текущей форме, нам нужен другой подход.
Проникновение в приватные поля объектов
Если у вас есть исходный код интересующих вас классов, то возникает соблазн просто подключиться и взять нужные данные.В примере заглушки RMI мы можем экспериментально узнать, что метод заглушки получитьСсылку() возвращает sun.rmi.server.UnicastRef, и после изучения исходников JDK мы обнаруживаем, что этот класс содержит поле ref типа sun.rmi.transport.LiveRef с именно той информацией, которая нам нужна.
Получаем примерно такой код (но заранее скажу, этого делать не стоит):
Возможно, результат вас устроит, но, повторюсь, я не рекомендую этого делать — этот код никуда не годится.import sun.rmi.server.*; import sun.rmi.transport.*; import java.rmi.*; import java.rmi.server.*; public class StubDigger { public static getPort(RemoteStub stub) throws Exception { RemoteRef ref = stub.getRef(); UnicastRef uref = (UnicastRef) ref; Field refField = UnicastRef.class.getDeclaredField("ref"); refField.setAccessible(true); LiveRef lref = (LiveRef) refField.get(uref); return lref.getPort(); } }
Во-первых, никогда не полагайтесь на классы sun.*, потому что никто не может гарантировать, что они не изменятся до неузнаваемости при любом обновлении JDK, и ваш код точно не будет легко перенесен на другие платформы JDK. Во-вторых, когда вы видите что-то вроде Field.setAccessible , то вам следует воспринимать это как знак остановки.
Это означает, что ваш код зависит от недокументированных полей, которые могут меняться от версии к версии или, что еще хуже, могут сохраняться, но с измененной семантикой.
(Этот код был написан для JDK 5. Как оказалось, в JDK 6 LiveRef приобрел публичный метод getPort(), поэтому Field.setAccessible вам больше не нужен.
Но в любом случае вам не следует зависеть от солнца.
* занятия.
) Конечно, иногда лучшего решения не найти.
Но если те классы, которые вас серьезно интересуют, окажутся сериализуемыми, то вполне возможно, что у вас все получится.
Дело в том, что сериализованная форма класса является частью его контракта.
Если API не потерян полностью, то его внешний контракт будет совместим с предыдущими версиями.
Это очень важное условие, в частности для платформы JDK. Так что, если требуемая информация недоступна через общедоступные методы классов, но, по крайней мере, является частью документированной сериализованной формы, то, будем надеяться, она и дальше останется неизменной в сериализованной форме.
Описание сериализованной формы включено в документацию Javadoc в разделе «См.
также» для каждого сериализованного класса.
Вы можете найти сериализованные формы всех общедоступных классов JDK. Здесь , на одной огромной странице.
Привет, Сериализ!
Моя библиотека для получения метаданных сериализованных объектов называется Сериализ, это сочетание слов «последовательный анализ».
Позвольте мне привести вам простой пример того, как это работает. Этот код. SEntity sint = SerialScan.examine(new Integer(5));
System.out.println(sint);
.
выведет это.
SObject(java.lang.Integer){
value = Prim(int){5}
}
Это означает, что объект типа java.lang.Integer, который мы передали в SerialScan.examine, сериализуется как объект с одним полем типа int внутри.
Если мы проверим документированная сериализованная форма java.lang.Integer , то мы увидим, что это именно то, что и ожидалось.
Если вы посмотрите исходный код java.lang.Integer, вы увидите, что сам класс также имеет одно поле значения типа int: /**
* The value of the <code>Integer</code>.
*
* @serial
*/
private final int value;
Но частные поля — это детали реализации.
В обновлении поле можно переименовать или заменить новым, унаследованным от родительского класса.
java.lang.Номер , или кто-нибудь еще.
И нет никакой гарантии, что этого не произойдет, но есть гарантия, что сериализованная форма останется неизменной.
Сериализация обеспечивает механизм сохранения сериализованной формы в исходном виде, даже если поля класса изменились.
Вот более сложный пример.
Допустим, мы по какой-то причине хотим знать, насколько велик массив внутри.
API не дает нам необходимой информации, хотя и позволяет нам принудительно выделить массив не меньше указанного.
Если мы посмотрим сериализованная форма ArrayList , мы увидим, что он содержит искомую информацию.
Он определяет сериализованное поле size, которое представляет собой количество элементов в списке, но это не то, что нам нужно.
А вот двоичные данные в методе WriteObject содержат именно то, что нужно: Серийные данные: содержит длину внутреннего ArrayList, за которым следуют все элементы (каждый как объект) в заданном порядке.
Если мы запустим этот код. List<Integer> list = new ArrayList<Integer>();
list.add(5);
SObject slist = (SObject) SerialScan.examine(list);
System.out.println(slist);
.
тогда мы получаем следующий вывод. SObject(java.util.ArrayList){
size = SPrim(int){1}
-- data written by class's writeObject:
SBlockData(blockdata){4 bytes of binary data}
SObject(java.lang.Integer){
value = SPrim(int){5}
}
}
Здесь мы попадаем в темные дебри сериализации.
В дополнение к сериализации полей объекта или вместо нее класс может иметь метод writeObject(ObjectOutputStream), который записывает произвольные данные в поток, используя такие методы, как ObjectOutputStream.writeInt .
Класс также должен содержать соответствующий метод readObject, который считывает те же данные и с помощью тега @serialData Вам следует документировать то, что записывает метод WriteObject, так же, как вы это делаете с ArrayList. Данные WriteObject в сериале можно получить с помощью метода SObject.getAnnotations(), который возвращает список.
Каждый объект, написанный с использованием метода ObjectOutputStream.writeObject(Объект) появляется в этом списке как SObject. Каждый фрагмент данных, записанный одним или несколькими последовательными вызовами методов ObjectOutputStream, унаследованных от Вывод данных ( записьInt , записьUTF и так далее) представлен как SBlockData. Сериализованный поток не позволяет выбирать отдельные элементы внутри этого фрагмента; эта информация является соглашением между автором и читателем, задокументированным в теге @serialData.
Основываясь на документации ArrayList, мы можем получить размер массива следующим образом: SObject slist = (SObject) SerialScan.examine(list);
List<SEntity> writeObjectData = slist.getAnnotations();
SBlockData data = (SBlockData) writeObjectData.get(0);
DataInputStream din = data.getDataInputStream();
int alen = din.readInt();
System.out.println("Array length: " + alen);
Как сериализация решает мои тестовые проблемы
Опуская полный исходный код, я приведу лишь набросок решения проблемы QueryExp, о которой я говорил вначале.
Предположим, мой QueryExp построен следующим образом: QueryExp query =
Query.or(Query.gt(Query.attr("Version"), Query.value(5)),
Query.eq(Query.attr("SupportsSpume"), Query.value(true)));
Это означает: «дайте мне MBeans с атрибутом Version больше 5 или атрибутом SupportsSpume, равным true».
toString() для этого запроса в JDK выглядит следующим образом: ((Version) > (5)) or ((SupportsSpume) = (true))
А вот как выглядит результат SerialScan.examine: SObject(javax.management.OrQueryExp){
exp1 = SObject(javax.management.BinaryRelQueryExp){
relOp = SPrim(int){0}
exp1 = SObject(javax.management.AttributeValueExp){
attr = SString(String){"version"}
}
exp2 = SObject(javax.management.NumericValueExp){
val = SObject(java.lang.Long){
value = SPrim(long){5}
}
}
}
exp2 = SObject(javax.management.BinaryRelQueryExp){
relOp = SPrim(int){4}
exp1 = SObject(javax.management.AttributeValueExp){
attr = SString(String){"supportsSpume"}
}
exp2 = SObject(javax.management.BooleanValueExp){
val = SPrim(boolean){true}
}
}
}
Легко представить код, который погружается в эту структуру и создает эквивалент XML. Каждая совместимая реализация JMX API должна создавать одну и ту же сериализованную форму, поэтому код, который ее анализирует, гарантированно будет работать где угодно.
Теперь код, который решает проблему номера портов в разъеме RMI : public static int getPort(RemoteStub stub) throws IOException {
SObject sstub = (SObject) SerialScan.examine(stub);
List<SEntity> writeObjectData = sstub.getAnnotations();
SBlockData sdata = (SBlockData) writeObjectData.get(0);
DataInputStream din = sdata.getDataInputStream();
String type = din.readUTF();
if (type.equals("UnicastRef"))
return getPortUnicastRef(din);
else if (type.equals("UnicastRef2"))
return getPortUnicastRef2(din);
else
throw new IOException("Can't handle ref type " + type);
}
private static int getPortUnicastRef(DataInputStream din) throws IOException {
String host = din.readUTF();
return din.readInt();
}
private static int getPortUnicastRef2(DataInputStream din) throws IOException {
byte hasCSF = din.readByte();
String host = din.readUTF();
return din.readInt();
}
Чтобы понять это, взгляните на описание сериализованная форма RemoteObject .
Этот код, конечно, сложен, но он легко переносим и перспективен в использовании.
Думаю, нет смысла объяснять, как извлечь все остальные данные из заглушек RMI — используйте тот же метод.
Заключение
Вероятно, вам не захочется возиться с сериализованными формами, если только в этом нет крайней необходимости.Но если вы не можете обойтись без этого, Seriaанализ может значительно упростить вам задачу.
Это также хороший способ проверить, что ваши собственные классы сериализованы так, как вы ожидаете.
Вы можете скачать библиотеку Seriaанализа здесь: http://weblogs.java.net/blog/emcmanus/serialysis.zip .
Теги: #сериализация #java #maxifier #с открытым исходным кодом #java
-
Шпионаж Или Мониторинг
19 Oct, 24 -
Планшет Android – Важность Наличия Sd-Карты
19 Oct, 24 -
Обзор Мини-Квадрокоптера Byrobot
19 Oct, 24 -
Картонная Коробка. Цифровой
19 Oct, 24 -
Вебикс 2.3. Весеннее Обновление
19 Oct, 24