Attach proxy to an existing object?

2019-06-21 20:41发布

My plan is to write a annotation based caching framework which caches the return values of methods. When a method gets called the first time with a specific parameter, then the cache should store the methods return value. When the same method gets called a second time with the same parameter then the method should return the previously calculated result from the cache and not execute its code again. My annotations looks like this:

@Cached(cacheProvider = HashMapCacheProvider.class)
public Product getProduct(String productId){    
    // Scraping the product from a website ...
    return product;
}

At the moment my little framework is working already fine. I'm using Javassist to create proxy objects of classes which are containing annotated methods. In order to create a new cached object I'm using this code:

public static <T> T newCachedInstance(Class<T> clazz)
    throws InstantiationException, IllegalAccessException {

    ProxyFactory factory = new ProxyFactory();
    factory.setSuperclass(clazz);
    factory.setFilter(new MethodFilter() {
        public boolean isHandled(Method m) {
            // ignore finalize()
            return !m.getName().equals("finalize");
        }
    });

    Class<T> c = factory.createClass();

    T proxy = c.newInstance();
    ((ProxyObject) proxy).setHandler(new CachedMethodHandler());
    return proxy;
}

The problem is, that I can just create new cached objects by this method and not by the constructor of their classes. Therefore I'm searching a solution to attach already existing objects to the caching mechanism.

And here is my question: Is it possible to attach a proxy to an existing object? In my understanding this should not be possible without updating all existing references to this object.

My other approach is to use bytecode manipulation in order to manipulate the code of the annotated methods like this:

public class Hello {
    public void say() {
         System.out.println("Hello");
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("Hello");
        CtMethod m = cc.getDeclaredMethod("say");
        m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
        Class c = cc.toClass();
        Hello h = (Hello)c.newInstance();
        h.say();
    }
}

Have you any other idea? What is the best practice to manipulate methods of existing objects?

3条回答
做个烂人
2楼-- · 2019-06-21 20:47

If you can replace all references to the object that you want to make proxied, in particular, if the object already exists but there is only one reference to it, you can do the following:

  1. get the object class and create a proxy class with Javassist;
  2. create an instance of this class (this assumes that there is a no-argument constructor)
  3. copy all fields to that new instance
  4. replace the original reference(s) with the reference(s) to the new object

If there is a copy constructor, you can use it to create a proxy copy of the existing object. (You will have to define it like X$Proxy(X x) {super(x);})

查看更多
贪生不怕死
3楼-- · 2019-06-21 20:49

In default Java virtual machines each object instance is stored on the heap where its field data is stored together with a reference to its Class (and a small area used for garbage collection). You are basically asking if you can redefine this link to point to another Class what is not possible by default.

However, you could hypothetically use sun.misc.Unsafe to override this reference with a subclass as long as this subclass did not introduce new fields. The outcome of this is however not defined and I would not recommend experimenting with it since the users of your framework could experience very subtle bugs. Also, the sun package hierarchy is not intended for public use what could break compatibility.

Another way would be offered by the Attach API. You can redefine classes at runtime using a Java agent. This would however effect all instances of a class but from your purpose this would make sence.

Another possibility would be using something like AspectJ to redefine classes before runtime.

Otherwise, you have to return a new instance that works as a proxy for the cache as you are apparently doing it already. This is absolutely okay and an appproach used by major frameworks such as Hibernate. Note that javassist is significantly slower then for example cglib because it reads in class files directly instead of using reflective access in order to avoid class loading. This might break performance when using your cache.

查看更多
闹够了就滚
4楼-- · 2019-06-21 20:50

https://github.com/verhas/djcproxy claims to do that. Unfortunately, it documents a design flaw that makes it thread-unsafe.

https://javax0.wordpress.com/2016/02/03/creating-proxy-object-using-djcproxy/ says:

The implementation has some flows(*), for example the late method proxy instantiations have no advantage really but the same time may hurt in case of multi-thread execution of the proxies.

(*) flaws

EDIT In addition, djcproxy is slow.

查看更多
登录 后发表回答