动态代理

作者: 健身营养爱好者 | 来源:发表于2018-12-12 16:25 被阅读10次

    前言

    HI,欢迎来到裴智飞的《每周一博》。今天是十二月第三周,我给大家介绍一下动态代理的知识,为什么要介绍这些呢,因为插件化技术同样需要它。

    一. 代理模式和静态代理

    代理模式是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用,通俗的来讲代理模式就是我们生活中常见的中介。下面介绍下代理模式的主要作用;

    A. 中介隔离作用
    在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。

    B. 符合开闭原则,增加类的功能
    代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。比如想在业务功能执行的前后加入一些公共的服务,加入缓存和日志这些功能,就可以使用代理类来完成,而没必要修改已经封装好的委托类。

    代理模式主要有静态代理和动态代理两种,静态代理是先生成源代码,再对其编译,在程序运行之前,代理类class文件就已经被创建了,动态代理是在程序运行时通过反射机制动态创建的。下面我们来看下静态代理的实现;

    1. 定义抽象主题Subject
    public interface Subject {
        public void request();
    }
    
    1. 定义具体主题,也就是被代理的对象
    public class RealSubject implements Subject {
        public void request() {
        }
    }
    
    1. 定义代理对象,也就是中介
    public class Proxy implements Subject {
        // 要代理哪个实现类
        private Subject subject = null;
        // 通过构造函数传递代理者
        public Proxy(Subject subject){
            this.subject = subject;
        }
     
        public void request() {
            this.before();
            this.subject.request();
            this.after();
        }
    
        // 预处理
        private void before(){
            //do something
        }
    
        // 善后处理
        private void after(){
            //do something
        }
    }
    

    这里的before和after不是必须的,只是演示装饰代理方法的一个例子。

    1. 使用代理
            Subject subject = new RealSubject();
            Subject proxy = new Proxy(subject);
            proxy. request();
    

    这里我们可以把Subject理解为房东,Proxy理解为中介,调用者是买房的人,中介代理了许多个房东,每一个房东都是一个RealSubject,都需要实现Subject。

    静态代理优缺点总结:
    优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展;
    缺点:我们得为每一个服务都得创建代理类,另外接口一旦发生改变,代理类也得相应修改;

    二. 动态代理

    动态代理是指利用Java的反射技术,在运行时创建一个实现给定接口的新类及其对象。在动态代理中不需要手动的创建代理类,只需要编写一个动态处理器就可以了,真正的代理对象由JDK运行时动态的创建。

    我们来看下动态代理的具体实现

    1. 编写动态处理器:
      实现InvocationHandler,接口里有一个invoke方法,在这里去实现需求,比如拦截方法,装饰方法等。invoke方法有三个参数;

    A. 动态代理类的引用,可以通过getClass方法得到代理类的一些信息;
    B. 方法对象的引用,代表被动态代理类调用的方法,从中可得到方法名,参数类型,返回类型等;
    C. args对象数组,代表被调用方法的参数,基本类型int会被装箱成对象类型Interger;

    public class DynamicProxyHandler implements InvocationHandler {
    
        private Object object;
    
        public DynamicProxyHandler(final Object object) {
            this.object = object;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("预处理");
            Object result = method.invoke(object, args);
            System.out.println("善后处理");
            return result;
        }
    }
    
    1. 使用动态代理:
      这里主要使用了Proxy.newProxyInstance这个方法,它接受三个参数;

    A. ClassLoader loader:目标对象使用的类加载器;
    B. Class<?>[] interfaces:指定目标对象实现的接口的类型;
    C. InvocationHandler:动态处理器,执行目标对象的方法时会触发事件处理器的invoke方法;

    Subject subject = new RealSubject();
    Subject proxy = (Subject) Proxy.newProxyInstance(
            Subject.class.getClassLoader(),  
            new Class[]{Subject.class}, 
            new DynamicProxyHandler(subject));
    proxy. request();
    

    一般情况下这三个参数都是同一个接口类的ClassLoader,Interfaces,具体代理对象,动态代理代理的是接口,不是类,也不是抽象类。

    前面的静态代理中我们只代理了request这一个方法,假如需要代理10个方法,每个方法里我们都要加入统计功能,那么相当于before和after要写10遍,动态代理就可以解决这个问题,我们只需要在invoke前加入befor,后加入after就可以了,后面我会写一个具体的例子来描述这一点。

    其实动态代理与静态代理的本质一样,都需要生成一个与被代理类实现了同样接口的实现类对象,只不过动态代理支持运行时指定这种实现方式而已。

    三. 动态代理原理

    动态代理的基础是反射,其实这些方法的增加是交给JVM去做了,当我们调用Proxy.newProxyInstance时,JVM会为每一个代理对象生成"$Proxy0"的类,后面的数字是递增的,这个类经过反编译后可以看到具体内容;

    public class Proxy0 extends Proxy implements Subject {
    
        // 第一步, 生成构造器
        protected Proxy0(InvocationHandler h) {
            super(h);
        }
    
        // 第二步, 生成静态域
        private static Method m1;   // hashCode方法
        private static Method m2;   // equals方法
        private static Method m3;   // toString方法
        private static Method m4;   // ...
        
        // 第三步, 生成代理方法
        @Override
        public int hashCode() {
            try {
                return (int) h.invoke(this, m1, null);
            } catch (Throwable e) {
                throw new UndeclaredThrowableException(e);
            }
        }
        
        @Override
        public boolean equals(Object obj) {
            try {
                Object[] args = new Object[] {obj};
                return (boolean) h.invoke(this, m2, args);
            } catch (Throwable e) {
                throw new UndeclaredThrowableException(e);
            }
        }
        
        @Override
        public String toString() {
            try {
                return (String) h.invoke(this, m3, null);
            } catch (Throwable e) {
                throw new UndeclaredThrowableException(e);
            }
        }
        
        @Override
        public void request() {
            try {
                // 构造参数数组, 如果有多个参数往后面添加就行了
                Object[] args = new Object[] {};
                h.invoke(this, m4, args);
            } catch (Throwable e) {
                throw new UndeclaredThrowableException(e);
            }
        }
        
        // 第四步, 生成静态初始化方法
        static {
            try {
                Class c1 = Class.forName(Object.class.getName());
                Class c2 = Class.forName(Subject.class.getName());    
                m1 = c1.getMethod("hashCode", null);
                m2 = c1.getMethod("equals", new Class[]{Object.class});
                m3 = c1.getMethod("toString", null);
                m4 = c2.getMethod("request", new Class[]{Subject.class});
                //...
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
    }
    

    这里我们可以看到在代理类的构造函数里传入了InvocationHandler,然后生成Object的几个方法和Subject接口里的方法,当调用指定的方法时会调用InvocationHandler的对应方法,从这个角度来看,它可以拦截任何方法。

    另外还可以看出:
    A. 代理类默认继承Porxy类,因为Java中只支持单继承,所以JDK动态代理只能去实现接口;
    B. 代理方法都会去调用InvocationHandler的invoke方法,因此我们需要重写InvocationHandler的invoke方法;
    C. 调用invoke方法时会传入代理实例本身,目标方法和目标方法参数,这解释了invoke方法的参数是怎样来的;

    动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

    四. 拦截方法

    前一篇我也说过反射是不能拦截方法的,动态代理可以做到,而且插件化的hook技术就会用到,我们来看一下如何拦截IActivityManagerHandler的方法。

    安卓源码的东西就不去读了,我们想拦截IActivityManager对象,在安卓O上,ActivityManager里的IActivityManager实例叫IActivityManagerSingleton,它是一个单例对象,需要从Singleton类里取出mInstance,这里我们先用反射获得该对象,然后动态代理它。

    Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManager");
    Object iActivityManagerSingleton = ReflectUtils.getValue(activityManagerNativeClass, "iActivityManagerSingleton", null);
    Class<?> singleton = Class.forName("android.util.Singleton");
    // 这个iActivityManagerSingleton是一个Singleton类型的,我们需要从Singleton中再取出这个单例的AMS代理
    Field mInstanceField = singleton.getDeclaredField("mInstance");
    mInstanceField.setAccessible(true);
    // ams的代理对象
    Object rawIActivityManager = mInstanceField.get(iActivityManagerSingleton);
    
    // 创建一个这个对象的代理对象, 然后替换这个字段
    Class<?> iActivityManagerInterface = Class.forName("android.app.IActivityManager");
    Object proxy = Proxy.newProxyInstance(
            Thread.currentThread().getContextClassLoader(),
            new Class<?>[]{iActivityManagerInterface}, 
            new IActivityManagerHandler(rawIActivityManager));
    mInstanceField.set(iActivityManagerSingleton, proxy);
    

    编写具体的IActivityManagerHandler对象;

    public class IActivityManagerHandler implements InvocationHandler {
    
        Object mBase;
    
        public IActivityManagerHandler(Object base) {
            mBase = base;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Log.e("gzq", "invoke拦截了" + method.getName());
            if ("startActivity".equals(method.getName())) {
                Log.e("gzq", "startActivity方法拦截了");
                return method.invoke(mBase, args);
            }
            return method.invoke(mBase, args);
        }
    }
    
    

    我们把这段代码放在Application的attachBaseContext里面,然后点击跳转Activity就会发现打印出 "invoke拦截了XXX",很多方法都会被拦截到。

    五. 动态代理的用途

    1. 实现AOP:面向切面编程
      先来简单看下面向切面编程的思想,传统程序的流程比如银行系统会有一个取款流程,还有一个查询余额流程,我们把这两个流程放到一起,它们都有一个相同的验证流程。

    有没有想过可以把这个验证用户的代码是提取出来,不放到主流程里去呢,有了AOP写代码时就不需要把这个验证用户步骤写进去,框架会自动把代码加过去。这样写代码的时候,只需考虑主流程,而不用考虑那些不重要的流程,比如把效验参数,加log,统计方法耗时这些全部都做成一个个面,所以AOP的本质是在一系列纵向的控制流程中,把那些相同的子流程提取成一个横向的面。

    这里我们以统计方法耗时和增加log为例,写一个通过动态代理实现AOP的代码,传统方法就是方法前后分别计时,然后做差,有多少个方法写多少遍,通过动态代理可以一次做到。

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("method:"+ method.getName());
            long start = System.currentTimeMillis();
            Object res = method.invoke(obj, args);
            System.out.println("耗时:" + (System.currentTimeMillis() - start));
            return res;
        }
    

    我们在InvocationHandler里面代理对象,在invoke里面对方法进行润饰,增加log和统计,这样代理对象的所有方法都会有该功能。

    1. 实现设计模式,比如装饰者模式
      装饰者模式就是在不改变原有类的基础上实现对方法的修改,从上面的例子很明显可以看出来动态代理拦截了方法,自然可以在方法前后进行装饰,比起静态的装饰模式要省不少事;

    2. 通过配置实现框架
      比如我们可以自定义注解,然后在invoke里面去取注解,如果method.getAnnotation是我们定义的注解,就可以做一些处理。

    六. 总结

    本文介绍了动态代理的一些基本知识,这也是通过Hook方式实现插件化的基础,后面我会介绍插件化和热更新的一些知识,感谢大家的阅读,我们下周再见。

    相关文章

      网友评论

        本文标题:动态代理

        本文链接:https://www.haomeiwen.com/subject/yxzrhqtx.html