Reputation: 2836
I have an instance of an object for which I need to create proxy to intercept one of the methods:
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
Upvotes: 2
Views: 846
Reputation: 2836
Got it working:
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:
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.
Upvotes: 1