How can I create a dynamic proxy in java that retains parameter annotations on methods?

前端 未结 3 1112
一整个雨季
一整个雨季 2021-02-04 09:03

I currently am trying to proxy some existing JAX/RS resources, in order to allow me to use the Hibernate Validator\'s method validation support. However, when I proxy my class (

相关标签:
3条回答
  • 2021-02-04 09:41

    I suspect, annotations are not added dynamically to the proxy instances. (probably because it is not straightforward). However it is possible to get the annotations from actual method instance during invocation (or filtering). For example,

    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Annotation;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    import javassist.util.proxy.MethodHandler;
    import javassist.util.proxy.ProxyFactory;
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    public class ProxyTester
    {
        @Target({ PARAMETER })
        @Retention(RUNTIME)
        public static @interface TestAnnotation {}
    
        public static interface IProxyMe {
            void aMethod(@TestAnnotation int param);
        }
    
        public static class ProxyMe implements IProxyMe {
            public void aMethod(@TestAnnotation int param) {
                System.out.println("Invoked " + param);
                System.out.println("-----------------");
            }
        }
    
        static void dumpAnnotations(String type, Object proxy, Object forObject, String forMethod)
        {
            String className = forObject.getClass().getName();
            System.out.println(type + " proxy for Class: " + className);
    
            for(Method method : proxy.getClass().getMethods()) {
                if(method.getName().equals(forMethod)) {
                    printAnnotations(method);
                }
            }
        }
    
        static void printAnnotations(Method method)
        {
            int paramCount = method.getParameterTypes().length;
            System.out.println("Method: " + method.getName() + " has "  + paramCount + " parameters");
    
            for(Annotation[] paramAnnotations : method.getParameterAnnotations())
            {
                System.out.println("Annotations: " + paramAnnotations.length);
    
                for(Annotation annotation : paramAnnotations)
                {
                    System.out.println("   Annotation " + annotation.toString());
                }
            }
        }
    
        static Object javassistProxy(IProxyMe in) throws Exception
        {
            ProxyFactory pf = new ProxyFactory();
            pf.setSuperclass(in.getClass());
    
            MethodHandler handler = new MethodHandler()
                {
                    public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable
                    {
                        if(thisMethod.getName().endsWith("aMethod"))
                            printAnnotations(thisMethod);
    
                        return proceed.invoke(self, args);
                    }
                };
            return pf.create(new Class<?>[0], new Object[0], handler);
        }
    
        static Object cglibProxy(IProxyMe in) throws Exception
        {
            Object p2 = Enhancer.create(in.getClass(), in.getClass().getInterfaces(),
                new MethodInterceptor()
                {
                    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
                    {
                        printAnnotations(arg1);
                        return arg3.invokeSuper(arg0, arg2);
                    }
                });
    
            return p2;
        }
    
        static Object jdkProxy(final IProxyMe in) throws Exception
        {
            return java.lang.reflect.Proxy.newProxyInstance(in.getClass().getClassLoader(), in.getClass().getInterfaces(),
                new InvocationHandler()
                {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
                    {
                        printAnnotations(method);
                        return method.invoke(in, args);
                    }
                });
        }
    
        public static void main(String[] args) throws Exception
        {
            IProxyMe proxyMe = new ProxyMe();
            IProxyMe x = (IProxyMe) javassistProxy(proxyMe);
            IProxyMe y = (IProxyMe) cglibProxy(proxyMe);
            IProxyMe z = (IProxyMe) jdkProxy(proxyMe);
    
            dumpAnnotations("no", proxyMe, IProxyMe.class, "aMethod");
            dumpAnnotations("javassist", x, IProxyMe.class, "aMethod");
            dumpAnnotations("cglib", y, IProxyMe.class, "aMethod");
            dumpAnnotations("jdk", z, IProxyMe.class, "aMethod");
    
            System.out.println("<<<<< ---- Invoking methods ----- >>>>>");
            x.aMethod(1);
            y.aMethod(2);
            z.aMethod(3);
        }
    }
    
    0 讨论(0)
  • 2021-02-04 09:46

    CGLib Enhancer technically is just extending from your Class. I do not know if this is feasible for you (number of objects) but how about exposing an interface instead of the class?

    Object p2 = Enhancer.create(resource.getClass(),
        new Class[] { IResource.class },
        new MethodInterceptor() {
          public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3)
              throws Throwable {
                return arg3.invokeSuper(arg0, arg2);
               }
        });
    

    Remove the annotations from the enhanced class and put them into the interface. Validate then against the interface. This might be a lot of boiler-plate interfaces for many such resources, but still seams a lot better than mapping everything to form backing objects or DTOs.

    0 讨论(0)
  • 2021-02-04 10:05

    I have never worked with cglib however I know that with Javassist you can use the invocationhandlers to get an instance of a class, and then there are all kinds of ways to target something without changing its annotations. One of my favorite ways is to hook into a method that is not inside of that class, but calls to it, then when it calls to the method inside that class it can make bytecode level changes, or use the slightly slower but human readable high level api to make your adjustments.

    This is a snippit out of some of my life running code that has worked for over 2 years with no issues. This is using javassist.

    ClassPool myPool = new ClassPool(ClassPool.getDefault());
                        myPool.appendClassPath("./mods/bountymod/bountymod.jar");
                        CtClass ctTHIS = myPool.get(this.getClass().getName());
                        ctCreature.addMethod(CtNewMethod.copy(ctTHIS.getDeclaredMethod("checkCoinBounty"), ctCreature, null));
                        ctCreature.getDeclaredMethod("modifyFightSkill").instrument(new ExprEditor() {
                            public void edit(MethodCall m)
                                    throws CannotCompileException {
                                if (m.getClassName().equals("com.wurmonline.server.players.Player")
                                        && m.getMethodName().equals("checkCoinAward")) {
                                    String debugString = "";
                                    if (bDebug)
                                        debugString = "java.util.logging.Logger.getLogger(\"org.gotti.wurmunlimited.mods.bountymod.BountyMod"
                                                + "\").log(java.util.logging.Level.INFO, \"Overriding checkCoinAward to checkCoinBounty\");\n";
                                    m.replace(debugString + "$_ = checkCoinBounty(player);");
                                }
                            }
                        });
    

    It gets the default class pool. Then adds the finish for a static class we want to snag a method from. However that method is not in the final class, it is in the mod class that we are injecting into the original jar at runtime. Then it gets a method inside that class. Then it gets all the entire classPool of every class that is associated with single class instance we got from myPool. Why do this, limits how much we are messing with or can mess up, takes a bit out of memory during the code generation.

    Then it adds a new method to ctCreature which is a class we initiated in a variable earlier sorry about that. The method is created in this class, but then copied into another class where we want to use it from. Then it hooks into the declared method of modifyFightSkill, which in this case happens right when we wanna have our code called. So when it fires, we start a new Expression Editor.

    That Expression Editor then makes sure that the actual class we want to be inside of but not mess up any of its original construction, and gets the method inside that class we want to do something with without changing its core class.

    Once that happens and it is all validated with ifs and goodies, the method then replaces the original method, with our new method via replace. Takes the old one out, puts the new one in. All of your annotations from the containing class are untouched because because we came in sneakily from the side.

    Now we could have just jumped right into the class we needed, hit that method, did what we want. However we would have ended up with problems, or messed up constructors or other things. The way you approach getting to your code injection is as important as anything when working with a large project.

    This answer may have been a little long, but the InvokationHandler can be a bit hard to grasp at first.

    0 讨论(0)
提交回复
热议问题