Серіалізація в Java як заглянути всередину чорної скриньки, Блог розробників phpBB
Споконвіку в Java є дивовижний механізм серіалізації, який дозволяє, не докладаючи спеціальних розумових зусиль, зберігати у вигляді послідовності байт як бажано важкі графи об'єктів. Формат зберігання добре документований, є купа прикладів, серіалізовані об'єкти «важать» абсолютно собі трошки, пересилаються по мережі на раз, є купа ймовірностей для кастомізації ... Все це звучить чудово, але тільки до тих пір, поки ви не залишитеся віч-на-віч. або багатомегабайтним бінарним файлом, що містить дуже дорогі і необхідні саме тепер дані. Як голими руками забратися в даний файл, і усвідомити, що ж зберігається всередині цього величезного серіалізованого графа об'єктів, не маючи початкового коду? На ці та багато інших питань може відповісти Serialysis - бібліотека, яка дозволить вам детально проаналізувати серіалізовані java-об'єкти (серіалізовані об'єкти- це мій варіант перекладу виразу serial forms, вирішив не йти далеко від оригіналу). Таким чином можна отримати інформацію про об'єкт, яка не доступна через його публічний API. Бібліотека також є придатним інструментом для тестування серіалізації ваших власних класів.
Від перекладача:Субота. Вечір. Ніщо не віщувало роботу в цей день, але раптово я пригадую, що добре б перевірити, як поживають наші джоби на hadoop-кластері, легко так, для заспокоєння совісті — чай завдання було вирішено.
В останні кілька днів досить багато завдань стали завершуватися з OutOfMemoryError на нашому hadoop-кластері в production-і, нарощувати обсяг пам'яті, що виділяється, ймовірності величезної не було, і ми з IT-відділом витратили достатню кількістьчасу на спроби виявити причину. Закінчилося це тим, що наш заокеанський співробітник роздумливо подивився на конфіги, виправив кілька рядків, і сказав, що завдання вирішене. І справді, у п'ятницю все стало чудово, і ми наступного разу пораділи наявності Cloudera Certified Developer-а в команді.
Але не тут було! Хадуп показував, що жодне завдання цієї злощасної суботи не виконалося. Привід падінь дещо відрізнявся від колишньої: task tracker не міг запустити завдання, тому що йому не вистачало пам'яті, щоб завантажити xml-файл конфігурації завдання.
Мені, звичайно, відразу стало цікаво, що ж за монструозні зміни там зберігаються? На жаль, більшу частину займав серіалізований блоб на півсотні мегабайт. Блоб складається з графа об'єктів десятка різних класів, для яких у мене, безумовно, немає вихідників.
Що ж можна зробити з цим багатомегабайтним бінарником увечері суботи за допомогою одних лише підручних коштів?
Тут на сцену виходить мій рятівник: Serialysis. Пара рядків коду — і я маю на руках повний дамп нутрощів серіалізованого об'єкта, з іменами класів і полів. Маючи на руках повний дамп, знаходжу завдання, включаю компресію gzip для словника рядків, патчу класи з підтримкою JBE. Вуаля – завдання вирішене!
Це хак, безперечно, але зрідка без хаків — нікуди.
Коли публічного API недостатньо
Привід написання бібліотеки Serialysis у тому, що я зіткнувся з деякими завданнями, коли мені була потрібна інформація про об'єкт, яку я не зумів отримати через публічний API, проте вона була доступна через серіалізовану форму.
2-й приклад взятий із JMX API. Запити на сервер MBean представлені інтерфейсом QueryExp. Приклади QueryExp зведені на застосуванні способівQuery class. Якщо ваш об'єкт належить QueryExp, як ви дізнаєтеся, які запити він виконує? JMX API не пропонує ніякого шляху це довідатися. Інформація повинна бути представлена в серіалізованій формі так, щоб, коли замовник запитує до віддаленого сервера, її можна було відновити на сервері. Якщо ми можемо побачити серіалізовану форму, ми зуміємо та визначити, який був запит.
2-й приклад якраз і спонукав мене написати цю бібліотеку. Існуючі типові JMX-конектори ґрунтуються на Java-серіалізації, тому в них не потрібно будь-яким спеціальним чином обробляти QueryExps. Але в новому Web Services Connector введеному в JSR 262 застосовується XML для серіалізації. Як вивчити QueryExp, щоб після цього перетворити його на XML? Результат примітивний: WS-конектор використовує версію цієї бібліотеки, щоб заглянути всередину серіалізованих QueryExp.
Всі ці приклади поєднує одне: вони показують прогалини у відповідних інтерфейсах API. Отже, необхідні методи, дозволяють витягувати інформацію з RMI-заглушки. Так само як необхідний спосіб перетворити QueryExp назад до початкового методу Query, який його породив. (Достатньо навіть стандартного підсумку toString(), придатного до розбору). Але таких способів тепер немає, і якщо ми хочемо код, який працюватиме з цими API в їх поточному вигляді, нам необхідний інший підхід.
Проникаємо у приватні поля об'єктів
Якщо у вас є початковий код класів, що хвилюють вас, то величезна спокуса легко влізти і взяти бажані дані. У прикладі із заглушкою RMI, ми шляхом експерименту можемо дізнатися, що спосіб заглушки getRef()повертає sun.rmi.server.UnicastRef, і вивчивши вихідники JDK, ми дізнаємося, що даний клас містить поле ref типу sun.rmi.transport.LiveRef, як раз з тієюінформацією, яка нам потрібна. Так що отримаємо приблизно такий код (але скажу заздалегідь, не варто цього робити):
Припустимо, результат вас абсолютно влаштує, але, повторюю, робити так не раджу - цей код нікуди не годиться. По-перше, ніколи не використовуйте пов'язаність від класів sun.*, тому що ніхто не може гарантувати, що вони не зміняться до невпізнання при будь-якому оновленні JDK, до того ж, ваш код однозначно буде нелегко портувати на інші платформи JDK. По-друге, коли ви бачите щось як Field.setAccessible, то вам слід сприймати це, як знак стоп. Це означає, що ваш код залежить від недокументованих полів, які можуть змінюватися від релізу до релізу або того поганого, які можуть зберегтися, але зі зміненою семантикою.
(Цей код був написаний для JDK 5. Як виявилося, в JDK 6 LiveRef купив публічний спосіб getPort(), тому вам більше не потрібний Field.setAccessible. Але в будь-якому випадку, не варто залежати від sun.* класів.)
Безумовно, зрідка не вдасться виявити кращого рішення. Але якщо ті класи, якими ви серйозно зацікавилися, виявилися серіалізованими, абсолютно допустимо, вам це вдасться. Справа в тому, що серіалізована форма класу є частиною його договору. Якщо API не зовсім зниклий, його зовнішній контракт буде сумісний з його попередніми версіями. Це дуже головна умова, зокрема для платформи JDK.
Так що якщо необхідна інформація і не доступна через громадські методи класів, але принаймні вона є частиною документованої серіалізованої форми, тоді можна вірити, що вона і надалі буде залишена незмінною в серіалізованій формі.
Виклад серіалізованої форми включається в Javadoc у розділі «See Also» для будь-якого сереалізованогокtOutputStream, успадкованим від DataOutput (writeInt, writeUTF тощо) представляється як SBlockData. Серіалізований потік не дозволяє виділити окремі елементи усередині цього шматка; ця інформація – угода між письменником та читачем, задокументована у тезі @serialData.
Базуючись на документації ArrayList, ми можемо отримати розмір масиву таким чином:
Як Serialysis вирішує мої тестові завдання
Спускаючи повний початковий код, наведу лише нарис рішення до завдання QueryExp, про яке я говорив на початку. Уявимо, у мене QueryExp будується так:
Це означає: «дай мені MBean-и з ознакою Version більше п'яти або ознакою SupportsSpume, рівним true». toString() для цього запиту в JDK виглядає так:
А так виглядає підсумок SerialScan.examine:
Легко уявити код, який занурюється в цю конструкцію, створюючи XML-еквівалент. Від будь-якої сумісної реалізації JMX API потрібно створювати точно таку ж серіалізовану форму, тому аналізуючий її код гарантовано буде працювати де бажано.
Зараз код, який вирішує проблемуномера порту в RMI заглушці:
Щоб розібратися в ньому, погляньте на виклад серіалізованої форми RemoteObject.
Цей код, безумовно, складний, але він легко портуємо і перспективний у застосуванні. Думаю, немає сенсу пояснювати, як витягувати з RMI-заглушок решту даних — використовуйте цей же метод.
Завершення
Швидше за все, ви не захочете копатися в серіалізованих формах до тих пір, поки не виникне серйозна потреба. Але якщо без цього не обійтися, Serialysis може значно спростити вам завдання.
Крім того, це чудовий метод перевірити, що ваші особисті класи серіалізуються саме так, як витого чекаєте.