Как создать динамический прокси в java, который сохраняет аннотации параметров для методов?

В настоящее время я пытаюсь проксировать некоторые существующие ресурсы JAX/RS, чтобы позволить мне использовать поддержку валидации метода Hibernate Validator. Однако, когда я прокси-сервер моего класса (в настоящее время используется cglib 2.2), аннотация FormParam отсутствует в параметрах в прокси-классе, поэтому JAX/RS runtime (apache wink) не заполняет параметры. Вот несколько тестовых кодов, которые показывают это:

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

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 net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import javassist.util.proxy.ProxyFactory;

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) {
        }
    }

    static void dumpAnnotations(String type, Object proxy, Object forObject,
            String forMethod) {
        String className = forObject.getClass().getName();

        System.err.println(type + " proxy for Class: " + className);

        for (Method method : proxy.getClass().getMethods()) {
            if (method.getName().equals(forMethod)) {
                final int paramCount = method.getParameterTypes().length;
                System.err.println(" Method: " + method.getName() + " has "
                        + paramCount + " parameters");
                int i = 0;
                for (Annotation[] paramAnnotations : method
                        .getParameterAnnotations()) {
                    System.err.println("  Param " + (i++) + " has "
                            + paramAnnotations.length + " annotations");
                    for (Annotation annotation : paramAnnotations) {
                        System.err.println("   Annotation "
                                + annotation.toString());
                    }
                }
            }
        }
    }

    static Object javassistProxy(IProxyMe in) throws Exception {
        ProxyFactory pf = new ProxyFactory();
        pf.setSuperclass(in.getClass());
        Class c = pf.createClass();
        return c.newInstance();
    }

    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 {
                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 {
                        return method.invoke(in, args);
                    }
                });
    }

    public static void main(String[] args) throws Exception {
        IProxyMe proxyMe = new ProxyMe();
        dumpAnnotations("no", proxyMe, proxyMe, "aMethod");
        dumpAnnotations("javassist", javassistProxy(proxyMe), proxyMe,
            "aMethod");
        dumpAnnotations("cglib", cglibProxy(proxyMe), proxyMe, "aMethod");

        dumpAnnotations("jdk", jdkProxy(proxyMe), proxyMe, "aMethod");
    }
}

Это дает мне следующий результат:

no proxy for Class: ProxyTester$ProxyMe
 Method: aMethod has 1 parameters
  Param 0 has 1 annotations
   Annotation @ProxyTester.TestAnnotation()
javassist proxy for Class: ProxyTester$ProxyMe
 Method: aMethod has 1 parameters
  Param 0 has 0 annotations
cglib proxy for Class: ProxyTester$ProxyMe
 Method: aMethod has 1 parameters
  Param 0 has 0 annotations
jdk proxy for Class: ProxyTester$ProxyMe
 Method: aMethod has 1 parameters
  Param 0 has 0 annotations

Есть ли другие альтернативы?

Ответ 1

Я подозреваю, что аннотации не добавляются динамически к экземплярам прокси. (вероятно, потому, что это не просто). Однако при вызове (или фильтрации) можно получить аннотации из экземпляра фактического метода. Например,

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

Ответ 2

CGLib Enhancer технически просто распространяется от вашего класса. Я не знаю, возможно ли это для вас (количество объектов), но как выставлять интерфейс вместо класса?

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

Удалите аннотации из расширенного класса и поместите их в интерфейс. Затем проверьте на интерфейс. Для многих таких ресурсов может быть много интерфейсов для котельных пластин, но все равно швы намного лучше, чем отображение всего, чтобы сформировать вспомогательные объекты или DTO.

Ответ 3

Я никогда не работал с cglib, но я знаю, что с Javassist вы можете использовать обработчики invocation, чтобы получить экземпляр класса, а затем есть всевозможные способы нацелиться на что-то, не изменяя его аннотации. Один из моих любимых способов - подключить метод, который не находится внутри этого класса, но вызывает его, а затем, когда он вызывает метод внутри этого класса, он может изменять уровень байт-кода или использовать немного более медленный, но хорошо читаемый level api, чтобы внести коррективы.

Это фрагмент из моего кода, который работает более 2 лет без каких-либо проблем. Это использует 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);");
                            }
                        }
                    });

Он получает пул классов по умолчанию. Затем добавляет финиш для статического класса, из которого мы хотим отловить метод. Однако этот метод не находится в конечном классе, именно в классе mod мы вводим в исходную банку во время выполнения. Затем он получает метод внутри этого класса. Затем он получает весь класс ClassPool каждого класса, который связан с экземпляром одного класса, который мы получили из myPool. Почему это, ограничивает то, во что мы возимся или может испортиться, занимает немного времени во время генерации кода.

Затем он добавляет новый метод в ctCreature, который является классом, который мы инициировали в переменной, ранее сожалеющей об этом. Метод создается в этом классе, но затем копируется в другой класс, из которого мы хотим его использовать. Затем он перехватывает объявленный метод modifyFightSkill, который в этом случае происходит правильно, когда мы хотим, чтобы наш код вызывался. Поэтому, когда он срабатывает, мы запускаем новый редактор выражений.

В этом редакторе Expression убедитесь, что фактический класс, который мы хотим быть внутри, но не испортить ни одну из его исходных конструкций, и получает метод внутри этого класса, мы хотим что-то сделать без изменения его основного класса.

Как только это произойдет, и все это проверено с помощью ifs и goodies, метод затем заменяет исходный метод новым методом с помощью replace. Забирает старую, ставит новую. Все ваши аннотации из содержащего класса нетронуты, потому что потому, что мы пришли скрытно из стороны.

Теперь мы могли бы просто вскочить прямо в класс, который нам нужен, ударить по этому методу, сделать то, что хотим. Однако мы бы столкнулись с проблемами или перепутали конструкторы или другие вещи. То, как вы приближаетесь к введению кода, так же важно, как и при работе с большим проектом.

Этот ответ, возможно, был немного длинным, но InvokationHandler может быть немного трудно понять.