Search code examples
javajava-11java-bytecode-asmbyte-buddycglib

Create dynamic proxy for existing Serializable object with no available constructor


I have an instance of an object for which I need to create proxy to intercept one of the methods:

  • The object implements an interface, but I need to proxy the full type not just implement the interface.
  • I don't know the exact type of the object, only its interface class.
  • There are no accessible public constructors.
  • The object is Serializable.
  • I have full accessibility to read the library code, but no ability to change any of it.

So what I need to do is something like:

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

It seems to me that theoretically this should be possible as the object is Serializable, but I can't find any way of using that.

Note on the following: I've been trying using cglib but I'm not tied to that at all. If it is possible in asm, javaassist, or any other library that would be fine.

What I have so far with cglib is I can proxy a simple object with a public constructor:

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
}

But I've been unable to replicate this using an object with no public constructor:

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

As the object is Serializable, I can clone it:

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());
}

So is there any way I can get cglib (or any library) to use this approach?

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);

Thanks


Solution

  • Got it working:

    1. Created a derived class definition using ASM.
    2. Added this class definition to the same class loader as the target class.
    3. Serialised the existing object to a byte array.
    4. Injected the derived class name into the serialised byte array.
    5. Deserialised the resulting bytes using standard ObjectInputStream.readObject.

    For (1), I couldn't get cglib or byte-buddy to create the class I needed, so I switched to ASM.

    For (2), I used a custom class loader to load the whole jar file containing the target class.

    This does mean I end up with a clone of the original object not a proxy as per the question, but that works fine for what I need.

    For the serialisation hack:

    I created a few example minimal classes, serialized them to disk and compared the resulting binary data.

    To inject the class name:

    1. Serialised the full parent object to a byte array.
    2. Manually created a byte array for the derived class header (see below).

    Both byte arrays contain the stream header data (magic, version and initial object type), so I took that from (2) for simplicity, although I did have to tweak a couple of bytes.

    So then just created a stream of all of (2) followed by all of (1) except the first 6 bytes.

    To create the byte array for the derived class, I just looked at the delivered Java source and came up with the following:

    /*
     * 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;
    }
    

    Have not tested this as a general approach, but appears to work fine for my case.