Как я могу десериализовать объект, если он был перемещен в другой пакет или переименован? - PullRequest
33 голосов
/ 01 марта 2010

Рассмотрим следующую ситуацию:

Существует файл сериализации, созданный более старой версией приложения. К сожалению, пакет изменился для класса, который был сериализован. И теперь мне нужно загрузить информацию из этого файла в тот же класс, но расположенный в другом пакете. Этот класс определен serialVersionUID и не изменился (то есть совместим).

Вопрос. Можно ли загрузить экземпляры нового класса из этого файла, используя какие-либо приемы (кроме простого копирования класса в старый пакет и затем с использованием логики оболочки десериализации)? Можно использовать readResolve() для восстановления после перемещения / переименования класса? Если нет, объясните почему.

Ответы [ 7 ]

48 голосов
/ 12 октября 2010

Возможно:

class HackedObjectInputStream extends ObjectInputStream {

    public HackedObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
        ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();

        if (resultClassDescriptor.getName().equals("oldpackage.Clazz"))
            resultClassDescriptor = ObjectStreamClass.lookup(newpackage.Clazz.class);

        return resultClassDescriptor;
    }
}

Это также позволяет игнорировать несоответствие serialVersionUID или даже десериализовать класс, если его структура поля была изменена.

6 голосов
/ 31 октября 2011

Если вы используете Cygnus Hex Editor, вы можете вручную изменить имя пакета / класса.

Если новое имя (всегда включающее пакет) имеет одинаковый размер, вы можете просто заменить старое имя новым именем, но если размер изменился, вам необходимо обновить первые 2 символа перед именем с новой новой длиной .

Щелкните правой кнопкой мыши Стандартные типы данных и выберите Big Endian.

Длина - подписанное слово.

Например:

00 0E 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
.  .  p   a  c  k  a  g  e  .  S  a  m  p  l  e

как пакет. Пример написан. 00 0E означает 14, число символов, которое имеет "package.Sample".

Если мы хотим перейти на newpackage.Sample, мы заменим эту строку на:

00 12 6E 65 77 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
.  .  n  e  w  p   a  c  k  a  g  e  .  S  a  m  p  l  e

00 12 означает 18, число символов, которое имеет "newpackage.Sample".

И, конечно, вы можете сделать патчер для автоматического обновления.

5 голосов
/ 02 марта 2010

Вопрос: можно ли загрузить новые экземпляры классов из этого файла используя любые приемы (кроме тривиальных копирование класса в старый пакет и затем с помощью оболочки десериализации логика)

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

Возможно использовать readResolve () для восстановить после перемещения / переименования учебный класс? Если нет, объясните почему.

Нет, потому что механизм десериализации выйдет из строя гораздо раньше, на этапе, когда он пытается найти десериализованный класс - он не может знать, что класс в другом пакете имеет метод readResolve(), который он должен использовать.

5 голосов
/ 01 марта 2010

Вероятно, вам лучше всего воссоздать старый класс (имя, пакет и серийный идентификатор), прочитать в сериализованной форме, затем скопировать данные в экземпляр нового объекта и повторно выполнить его повторно.

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

Другой вариант - воскресить старый класс и реализовать его метод readResolve для возврата экземпляра нового класса (возможно, путем объявления конструктора копирования). Лично я думаю, что я пойду на сценарий изменения схемы и затем удаляю старый класс навсегда.

2 голосов
/ 15 марта 2019

Используйте этот класс вместо ObjectInputStream, если ваши классы перемещены в другое пространство имен.

class SafeObjectInputStream extends ObjectInputStream {
    private final String oldNameSpace;
    private final String newNameSpace;

    public SafeObjectInputStream(InputStream in, String oldNameSpace, String newNameSpace) throws IOException {
        super(in);
        this.oldNameSpace = oldNameSpace;
        this.newNameSpace = newNameSpace;
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
        ObjectStreamClass result = super.readClassDescriptor();
        try {
            if (result.getName().contains(oldNameSpace)) {
                String newClassName = result.getName().replace(oldNameSpace, newNameSpace);
                // Test the class exists
                Class localClass = Class.forName(newClassName);

                Field nameField = ObjectStreamClass.class.getDeclaredField("name");
                nameField.setAccessible(true);
                nameField.set(result, newClassName);

                ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass)
                Field suidField = ObjectStreamClass.class.getDeclaredField("suid");
                suidField.setAccessible(true);
                suidField.set(result, localClassDescriptor.getSerialVersionUID());
        }
        } catch(Exception e) {
            throw new IOException("Exception when trying to replace namespace", e);
        }
        return result;
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        if (desc.getName().contains(oldNameSpace)) {
            String newClassName = desc.getName().replace(oldNameSpace, newNameSpace);
            return Class.forName(newClassName);
        }
        return super.resolveClass(desc);
    }
}

Вы можете использовать его следующим образом:

ObjectInputStream objectStream = new SafeObjectInputStream(inputStream, "org.oldnamespace", "org.newnamespace");
objectStream.readObject();

Это не приведет к ошибке StreamCorruptedException, если некоторые из ваших классов изменятся. Вместо этого он попытается загрузить как можно больше полей. Вы можете выполнить проверку / обновление данных, внедрив в своих классах метод readObject.

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    // Validate read data here
}
2 голосов
/ 02 марта 2010

Не думаю, что можно делать то, что ты хочешь.

Формат файла сериализации содержит имена классов. Подробно он имеет следующую структуру:

AC ED

номер версии протокола

данные объекта

описание класса объекта

Описание класса имеет следующий формат:

полное имя класса

уникальный идентификатор серийной версии (SHA1 из поля и методы подписи)

опции сериализации

дескрипторы полей

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

0 голосов
/ 09 июля 2018

Дополнение к способу редактирования в шестнадцатеричном формате.

Это сработало для меня, и было проще заменить старое имя пакета новым, вместо того чтобы реализовывать замены классов, переопределяя ObjectInputStream. Тем более, что были и анонимные классы.

Вот скрипт, который заменяет старый путь к классу новым путем к классу в двоичном формате.

Вот содержимое моего hexreplace.sh сценария:

#!/bin/bash
set -xue

OLD_STR=$(echo -n $1 | hexdump -ve '1/1 "%.2X"')
NEW_STR=$(echo -n $2 | hexdump -ve '1/1 "%.2X"')
SRC_FILE=$3
DST_FILE=$4

TMP_FILE=$(mktemp /tmp/bin.patched.XXXXXXXXXX)

[ -f $SRC_FILE ]

hexdump -ve '1/1 "%.2X"' "$SRC_FILE" | sed "s/$OLD_STR/$NEW_STR/g" | xxd -r -p > "$TMP_FILE"

mv "$TMP_FILE" "$DST_FILE"

Run

hexreplace.sh old.class.path new.class.path source_file destination_file

Скрипт работает правильно, если исходный и целевой файлы совпадают.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...