Динамический прокси Java от входа в принцип до реального боя

Android

содержание

  • предисловие
  • Что такое динамический прокси и чем отличается статический прокси
  • Простое использование динамического прокси Java
  • Интерпретация принципа динамического прокси Java
  • Использование динамических прокси в Android

## Предисловие Я считаю, что слово динамический агент одновременно знакомо и незнакомо многим партнерам по разработке Android. Знакомство должно быть связано с тем, что вы можете часто слушать некоторые группы, и эксперты B в блоге говорят об этом. Незнакомо, потому что в повседневной разработке Android я похоже, не использовал эту штуку в андроиде, да и сам я этому не научился (особенно маленькие друзья, которые вышли из обучающего класса, насколько я знаю, большинство обучающих классов Android не будут говорить об этой штуке, а некоторые просто просты, я не буду упоминать об этом). И это знакомое и незнакомое чувство заставляет некоторых друзей думать, что динамические агенты очень продвинуты, и это знания, используемые только большими парнями. А я, сегодня, помогу своим друзьям стащить динамического агента с алтаря.В следующий раз, когда ты притворишься, что заставляешь маленького эксперта говорить с тобой о динамическом агенте, ты постучишь его по голове и скажешь Л (стар.) З (ребенок) тоже будет 😁.

Что такое динамический прокси и чем отличается статический прокси

На самом деле, как динамический прокси, так и статический прокси можно рассматривать как использование режима прокси.Что такое режим прокси?

Прокси-объект предоставляется объекту, а прокси-объект обеспечивает и контролирует доступ к исходному объекту. Режим прокси на самом деле является очень простым режимом дизайна. Конкретные детали могут быть собственными Baidu, поэтому я не буду его здесь представлять.

В статическом прокси вам нужно вручную написать прокси-класс для прокси-класса (класс делегирования) и написать новый метод для каждого метода, который необходимо проксировать.Эта часть должна быть завершена перед компиляцией. Динамический прокси — это прокси-класс, который может динамически генерироваться «на лету», и нет необходимости вручную реализовывать каждый прокси-метод. Проще говоря, в классе делегата есть 1000 методов, которые необходимо делегировать (например, цель делегата — вывести дополнительную часть вывода до и после выполнения каждого метода в качестве примера). , вам нужно вручную написать класс делегата и реализовать эту 1000 методов, а со статическими прокси для этого потребуется несколько простых строк кода.

Простое использование динамического прокси Java

Связанные классы для динамических прокси

Существует два основных класса динамических прокси:

  • Прокси (в составе пакета java.lang.reflect) в основном отвечает за управление и создание классов прокси.
  • Интерфейс InvocationHandler имеет только один метод вызова, который в основном отвечает за часть вызова метода, который нам нужно реализовать в динамическом прокси.
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
//三个参数分别是 代理类proxy  委托类被执行的方法method 方法传入的参数args
//返回值则是method方法执行的返回值

Возьмем конкретный пример использования.Предположим есть следующий интерфейс и два класса отвечающих за работу ордера

public interface OrderHandler {
    void handler(String orderId);
}

public class NormalOrderHandler implements OrderHandler {
    @Override
    public void handler(String orderId) {
        System.out.println("NormalOrderHandler.handler():orderId:"+orderId);
    }
}

public class MaxOrderHandler implements OrderHandler {
    @Override
    public void handler(String orderId) {
        System.out.println("MaxOrderHandler.handler():orderId:"+orderId);
    }
}

В настоящее время нам необходимо печатать строку информации до и после каждого вызова метода обработчика и перехватывать первые 10 цифр, когда длина orderId превышает 10 (во всяком случае, не спрашивайте меня, какой RZ упомянул это требование , это же не блогер 😁) На этом этапе мы можем сделать следующий код:

//创建一个处理器类实现InvocationHandler接口并实现invoke方法
public class OrderHandlerProxy implements InvocationHandler {

    //委托类 在这里就相当于实现了OrderHandler的类的对象
    Object target;

    public Object bind(Object target){
        this.target=target;

        //重点之一,通过Proxy的静态方法创建代理类 第一个参数为委托类的类加载器,
        //第二个参数为委托类实现的接口集,第三个参数则是处理器类本身
        return Proxy.newProxyInstance(this.target.getClass().getClassLoader(),
                this.target.getClass().getInterfaces(),this);

    }
    
    //重点之二
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        
        //判断执行的方法是否是我们需要代理的handler方法
        if (method.getName().equalsIgnoreCase("handler")){
            System.out.println("OrderHandlerProxy.invoke.before");
            String orderId= (String) args[0];

            //对orderId的长度做限制
            if (orderId.length()>=10){
                orderId=orderId.substring(0,10);
            }
            
            //重点之三,这个地方通过反射调用委托类的方法
            Object invoke = method.invoke(target, orderId);
            
            System.out.println("OrderHandlerProxy.invoke.after");
            return invoke;
        }else {
            //当前执行的方法不是我们需要代理的方法时不做操作直接执行委托的相应方法
            System.out.println("Method.name:"+method.getName());
            return method.invoke(target,args);
        }
    }
}

Далее идет конкретный код, использующий динамический прокси.

public class NormalApplicationClass {
    public void handlerOrder(OrderHandler orderHandler,String orderId){
        //创建处理器对象
        OrderHandlerProxy proxy=new OrderHandlerProxy();
        //为传入的实现了OrderHandler接口的对象创建代理类并实例化对象
        OrderHandler handler = (OrderHandler) proxy.bind(orderHandler);
        
        handler.handler(orderId);
        System.out.println(handler.toString());
    }
    public static void main(String[] args) {
        NormalApplicationClass app=new NormalApplicationClass();
        app.handlerOrder(new MaxOrderHandler(),"012345678999");

    }
}

Конкретное возвращаемое значение выглядит следующим образом

OrderHandlerProxy.invoke.before
MaxOrderHandler.handler():orderId:0123456789
OrderHandlerProxy.invoke.after
Method.name:toString
com.against.javase.rtti.use.MaxOrderHandler@d716361

Как видно из вышеизложенного, мы успешно напечатали строку вещей до и после выполнения метода-обработчика, а длина orderId была ограничена, и это не повлияло на вызовы других методов, таких как toString самого объекта.

Интерпретация принципа динамического прокси

Мы знаем, что большая разница между динамическим прокси и статическим прокси заключается в том, что процесс создания прокси-класса отличается, и операция создания прокси-класса в динамическом прокси в основном выполняется в методе Proxy.newProxyInstance(), тогда мы в основном смотрим на этот метод исходный код

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
            throws IllegalArgumentException
    {
        //省略掉部分代码,主要是一些Jvm安全性的验证和权限的验证

        /*
         * 获取或者生成一个代理类的Class对象。为什么是获取或生成呢?
         * 这是应为有缓存机制的存在,
         * 当第二次调用newProxyInstance方法并且上次生成的代理类的缓存还未过期时会直接获取
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * 通过反射获取代理类的构造器,并通过构造器生成代理类对象
         */
        try {
            //...

            //通过反射获取代理类的构造器,
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }

            //通过构造器生成代理对象,这里返回的就是给我们使用的代理类的对象了
            return cons.newInstance(new Object[]{h});
        } //....省略掉一些无关代码
    }

Из приведенного выше кода видно, что Proxy сначала получает объект Class прокси-класса, затем получает конструктор посредством отражения, внедряет экземпляр InvocationHandler (то есть класс процессора, который мы реализуем сами) из конструктора и создает экземпляр класса прокси. Операция получения объекта Class прокси-класса находится в Class> cl = getProxyClass0(loader, intfs); Здесь мы продолжаем отслеживать и обнаруживаем, что это очень просто, достаточно сделать несколько регулярных проверок и вызвать proxyClassCache.get (загрузчик, интерфейсы); мы проверяем и находим

    /**
     * a cache of proxy classes
     */
    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new     ProxyClassFactory());
//看到WeakCache我们就能大概猜到这东西是做什么的了吧?而看ProxyClassFactory的类名我们不难看出创建代理类的Class对象的操作都在这里面

И этот очень простой класс является одним из основных методов применения метода.

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
//...前面主要是一些生成proxyName,代理类类名的操作,省略掉
//具体的生成操作就是在这个方法里面
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
try {
    //这个方法是一个native方法,通过类加载器和类名以及类的文件的字节流来加载出Class对象。
    return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        throw new IllegalArgumentException(e.toString());
     }
}

Затем вы можете найти сгенерированную операцию класса в proxygenerator.generateProxyclass (ProxyName, Interfaces, AccessFlags); Эта часть кода сложная, мы просто смотрим на место.

        this.addProxyMethod(hashCodeMethod, Object.class);
        this.addProxyMethod(equalsMethod, Object.class);
        this.addProxyMethod(toStringMethod, Object.class);
//通过这个地方我们知道了,为什么我们对代理类的toString等方法的调用也会走代理类的invoke方法,这是应为在生成委托类的时候帮我们重写了这几个方法。

//生成一个带有InvocationHandler参数的构造器
private ProxyGenerator.MethodInfo generateConstructor() throws IOException {
        ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
        DataOutputStream var2 = new DataOutputStream(var1.code);
        this.code_aload(0, var2);
        this.code_aload(1, var2);
        var2.writeByte(183);
        var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));
        var2.writeByte(177);
        var1.maxStack = 10;
        var1.maxLocals = 2;
        var1.declaredExceptions = new short[0];
        return var1;
    }

Следующая операция — не что иное, как генерация соответствующего прокси-метода в соответствии с интерфейсом, реализованным классом делегата, и генерация конструктора, которому нужно передать объект InvocationHandler, но здесь генерируется файл .class, а не .java, который мы написали вручную .файл, а файл .class является скомпилированным файлом, и jvm может быть непосредственно загружен в него для выполнения, что избавляет от необходимости компилировать этот шаг. Давайте посмотрим, как выглядит прокси-класс, который генерирует для нас Proxy:

public final class $Proxy0 extends Proxy implements OrderHandler {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void handler(String var1) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.against.javase.rtti.use.OrderHandler").getMethod("handler", Class.forName("java.lang.String"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

Мы видим, что этот прокси-класс, по сути, реализует интерфейс класса делегата, и передает каждый вызов метода интерфейса, реализованного классом делегата, в метод вызова написанного нами класса-обработчика, все ясно, динамический прокси Не загадочно, ха-ха 😁.

Использование динамических прокси в Android

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

Простая имитация функции масляного ножа (buttknife) в реальном бою.

(Советы переднего ряда, чтобы прочитать эту часть, требуют некоторых знаний об отражении) При выборе конкретного примера я думал о некоторых вещах, таких как хук SystemManager, но эта часть больше относится к системному уровню Android, чем динамический прокси, который более громоздкий в написании и сложный в использовании. код. Наконец, я решил сымитировать простую функцию ножа для масла. Все должны быть знакомы с ножом для масла. Должно быть, использовались следующие две аннотации:

@BindView()
@OnClick()

Сегодня нам предстоит реализовать функцию этих двух нот, следуя непосредственно коду Определить две заметки

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
    int value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
}

Затем используйте эти две аннотации в Activity

public class MainActivity extends AppCompatActivity {

    @InjectView(R.id.tv)
    private TextView tv;

    @InjectView(R.id.iv)
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ViewInject.inject(this);
        tv.setText("inject success!");

        iv.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher));
    }

    @OnClick({R.id.tv,R.id.iv})
    public void onClick(View view){
        switch (view.getId()){
            case R.id.tv:
                Toast.makeText(this,"onClick,TV",Toast.LENGTH_SHORT).show();
                break;
            case R.id.iv:
                Toast.makeText(this,"onClick,IV",Toast.LENGTH_SHORT).show();
                break;
        }
    }

}

Эта часть кода очень простая, тут и говорить нечего, фокус на ViewInject.inject(this); здесь

public class ViewInject {

    public static void inject(Activity activity) {

        Class<? extends Activity> activityKlazz = activity.getClass();

        try {

            injectView(activity,activityKlazz);
            proxyClick(activity,activityKlazz);

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private static void proxyClick(Activity activity,Class<? extends Activity> activityKlazz) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        Method[] declaredMethods = activityKlazz.getDeclaredMethods();

        for (Method declaredMethod : declaredMethods) {

            //获取标记了OnClick注解的方法
            if (declaredMethod.isAnnotationPresent(OnClick.class)){
                OnClick annotation = declaredMethod.getAnnotation(OnClick.class);

                int[] value = annotation.value();

                //创建处理器类并且生成代理类,同时将activity中我们标记了OnClick的方法和处理器类绑定起来
                OnClickListenerProxy proxy=new OnClickListenerProxy();
                Object listener=proxy.bind(activity);
                proxy.bindEvent(declaredMethod);


                for (int viewId : value) {
                    Method findViewByIdMethod =
                            activityKlazz.getMethod("findViewById", int.class);

                    findViewByIdMethod.setAccessible(true);
                    View view = (View) findViewByIdMethod.invoke(activity, viewId);

                    //通过反射把我们的代理类注入到相应view的onClickListener中
                    Method setOnClickListener = view.getClass().getMethod("setOnClickListener", View.OnClickListener.class);

                    setOnClickListener.setAccessible(true);
                    setOnClickListener.invoke(view,listener);
                }
            }
        }

    }


    private static void injectView(Activity activity,Class<? extends Activity> activityKlazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {

        /**
         * 注入view其实很简单,通过反射拿到activity中标记了InjectView注解的field。
         * 然后通过反射获取到findViewById方法,并且执行这个方法拿到view的实例
         * 接着将实例赋值给activity里的field上
         */
        for (Field field : activityKlazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(InjectView.class)) {
                InjectView annotation = field.getAnnotation(InjectView.class);

                int viewId = annotation.value();

                    Method findViewByIdMethod =
                            activityKlazz.getMethod("findViewById", int.class);

                    findViewByIdMethod.setAccessible(true);
                    View view = (View) findViewByIdMethod.invoke(activity, viewId);
                    field.setAccessible(true);
                    field.set(activity, view);

            }
        }
    }

}

Код выше закомментирован, давайте взглянем на класс процессора OnClickListenerProxy:

public class OnClickListenerProxy implements InvocationHandler {

    static final String TAG="OnClickListenerProxy";

    //这个其实就是我们相关的activity
    Object delegate;

    //这个是activity中我们标记了OnClick的方法,最终的操作就是把对OnClickListener中OnClick方法的调用替换成对这个event方法的调用
    Method event;


    public Object bind(Object delegate){
        this.delegate=delegate;
        //生成代理类,这个没什么好说的了
        return Proxy.newProxyInstance(this.delegate.getClass().getClassLoader(),
                new Class[]{View.OnClickListener.class},this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Log.e(TAG,"invoke");
        Log.e(TAG,"method.name:"+method.getName()+"  args:"+args);

        //判断调用的是onClick方法的话,替换成对我们event方法的调用。
        if ("onClick".equals(method.getName())){
            for (Object arg : args) {
                Log.e(TAG,"arg:"+arg);
            }
            View view= (View) args[0];
            return event.invoke(delegate,view);
        }
        return method.invoke(delegate,args);
    }
    public void bindEvent(Method declaredMethod) {
        event=declaredMethod;
    }
}

Благодаря приведенной выше реализации кода мы смогли запустить проект и успешно реализовать необходимые нам функции. Конечно, есть много деталей, с которыми нужно разобраться, но этого достаточно в качестве демонстрации и обучения, и мы также реализовали некоторые функции известной библиотеки с открытым исходным кодом, такой как Butter Knife, с помощью нашего собственного метода. очень круто?

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