Создайте прокси Dynami c для существующего объекта Serializable без доступного конструктора - PullRequest
1 голос
/ 17 апреля 2020

У меня есть экземпляр объекта, для которого мне нужно создать прокси для перехвата одного из методов:

  • Объект реализует интерфейс, но мне нужно прокси полного типа, а не просто реализовать интерфейс.
  • Я не знаю точный тип объекта, только его интерфейсный класс.
  • Нет доступных конструкторов publi c.
  • Объект Serializable.
  • У меня есть полная доступность для чтения кода библиотеки, но нет возможности изменить какой-либо из них.

Так что мне нужно сделать что-то вроде:

 TheObject obj = library.getObject();
 TheObject proxy = createProxyObject(obj);
 library.doSomethingWith(proxy);

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

Обратите внимание на следующее: я пытался используя cglib, но я не привязан к этому вообще. Если это возможно в asm, javaassist или любой другой библиотеке, которая подойдет.

Что я имею до сих пор с cglib, так это то, что я могу проксировать простой объект с помощью конструктора publi c:

public class SimpleObject {
  private String name;
  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return name;
  }
  // return a random number
  public int getRandom() {
    return (int)(Math.random() * 100);
  }
}

public void testCglibEnhancer() throws Exception {
  SimpleObject object = new SimpleObject();
  object.setName("object 1");
  System.out.println(object.getName() + " -> " + object.getRandom());

  Enhancer enhancer = new Enhancer();
  enhancer.setSuperclass(object.getClass());

  // intercept getRandom and always return 32
  ProxyRefDispatcher passthrough = proxy -> object;
  MethodInterceptor fixedRandom = (obj, method, args, proxy) -> 32;
  enhancer.setCallbacks(new Callback[]{passthrough, fixedRandom});
  enhancer.setCallbackFilter(method -> method.getName().equals("getRandom") ? 1 : 0);

  SimpleObject proxy = (SimpleObject)enhancer.create();
  System.out.println(proxy.getName() + " -> " + proxy.getRandom()); // always 32
}

Но мне не удалось воспроизвести это, используя объект без конструктора publi c:

public static class ComplexObject implements Serializable {
  public static ComplexObject create() {
    return new ComplexObject();
  }
  private String name;
  private ComplexObject() {
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return name;
  }
  public int getRandom() {
    return (int)(Math.random() * 100);
  }
}

ComplexObject proxy = (ComplexObject)enhancer.create();
// throws IllegalArgumentException: No visible constructors

Поскольку объект является сериализуемым, я могу его клонировать:

public static <T extends Serializable> T cloneViaSerialization(T source) throws IOException, ClassNotFoundException {
  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  ObjectOutputStream out = new ObjectOutputStream(bos);
  out.writeObject(source);

  ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
  ObjectInputStream in = new ObjectInputStream(bis);
  return (T)in.readObject();
}

public void testClone() throws Exception {
  ComplexObject object1 = ComplexObject.create();
  object1.setName("object 1");

  ComplexObject object2 = cloneViaSerialization(object1);
  object2.setName("object 2");

  System.out.println(object1.getName() + " -> " + object1.getRandom());
  System.out.println(object2.getName() + " -> " + object2.getRandom());
}

Так есть ли способ заставить cglib (или любую библиотеку) использовать этот подход?

ComplexObject object = library.getObject();
ObjectInputStream in = ... // serialised version of object

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(object.getClass());
// add callbacks etc.

// note createFromSerialized is not a existing method of
// Enhancer - it is what I'm trying to synthesise somehow
ComplexObject proxy = (ComplexObject)enhancer.createFromSerialized(in);

Спасибо

1 Ответ

1 голос
/ 22 апреля 2020

Работает:

  1. Создано определение производного класса с использованием ASM.
  2. Добавлено это определение класса в тот же загрузчик классов, что и целевой класс.
  3. Сериализовал существующий объект в байтовый массив.
  4. Внедрил имя производного класса в сериализованный байтовый массив.
  5. Десериализовал результирующие байты, используя стандартный ObjectInputStream.readObject.

Для (1) я не смог заставить cglib или byte-buddy создать нужный мне класс, поэтому я переключился на ASM.

Для (2) я использовал загрузчик пользовательских классов для загрузки всего jar-файл, содержащий целевой класс.

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

Для взлома сериализации:

Я создал несколько примеров минимальных классов, сериализовал их на диск и сравнил полученные двоичные данные.

Чтобы ввести имя класса:

  1. Серийный т Полный родительский объект для байтового массива.
  2. Созданный вручную байтовый массив для заголовка производного класса (см. ниже).

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

Итак, просто создал поток всех (2) сопровождаемый всеми (1) за исключением первых 6 байтов.

Чтобы создать массив байтов для производного класса, я просто посмотрел на поставленный источник Java и придумал следующее:

/*
 * Returned array contains:
 * - stream header
 * - class header
 * - end block / class desc markers for next class
 */
private byte[] derivedClass(Class<?> clss) throws IOException {
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  ObjectOutputStream oos = new ObjectOutputStream(baos);

  ObjectStreamClass osc = ObjectStreamClass.lookup(clss);
  oos.writeUTF(osc.getName());
  oos.writeLong(osc.getSerialVersionUID());
  oos.write(SC_SERIALIZABLE);      // flags
  oos.writeShort(0);               // field count
  oos.writeByte(TC_ENDBLOCKDATA);
  oos.writeByte(TC_CLASSDESC);
  oos.flush();

  // header appears to write 0x77 (TC_BLOCKDATA) and 0x54 (???) for bytes 5 & 6
  // samples streamed from other files use 0x73 (TC_OBJECT) and 0x72 (TC_CLASSDESC)
  byte[] bytes = baos.toByteArray();
  bytes[4] = TC_OBJECT;
  bytes[5] = TC_CLASSDESC;

  return bytes;
}

Не проверял это как общий подход, но, похоже, отлично работает для моего случая.

...