深入理解Java动态代理

代理模式

代理模式UML

使用代理模式创建代理对象,让代理对象来控制对某个对象的访问, 被代理对象可以是远程对象,创建开销大的对象或者需要安全控制的对象等.

Proxy 称为代理对象.
RealSubject 是被代理的对象,也称为委托对象.
Subject 是他们抽象出来的接口.

RealSubjectProxy都继承自Subject, Proxy 内部持有一个 RealSubject的变量,调用代理的方法,代理中将直接调用RealSubject对应的方法.

静态代理

静态代理,在编译期间就需要指定好代理类,即在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了.

    interface Subject {
        void request();
    }

     class Proxy implements Subject {
        private RealSubject realSubject;

        public Proxy(RealSubject realSubject) {
            this.realSubject = realSubject;
        }

        @Override
        public void request() {
            // 添加log
            System.out.println("log...");
            realSubject.request();
        }
    }

     class RealSubject implements Subject {

        @Override
        public void request() {
            // todo ...
            System.out.println("request from http");
        }
    }

    public static void main(String[] args) {
        Subject proxy = new Proxy(new RealSubject());
        proxy.request();
    }

这样做的优点:

1. 可以隐藏委托类的实现,可以进行权限控制和安全控制.

2. 实现客户端和委托类解耦,只要对外接口不变,客户端就不需要修改调用方式.

3. 通过扩展代理类,进行一些功能的附加与增强.

动态代理

动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码.代理类和委托类的关系是在程序运行时确定。

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

Java中的动态代理是使用Proxy.newProxyInstance()方法生成的.

先观察下 Proxy.newProxyInstance()的参数.

    Object newProxyInstance(
            ClassLoader loader,
            Class<?>[] interfaces,
            InvocationHandler h
    )

ClassLoader loader:

表示加载生成代理类的类加载器,通常情况下是直接使用
当前线程的类加载器 Thread.currentThread().getContextClassLoader(),
或者 使用 代理接口的类加载器. 如 Subject.class.getClassLoader().

少部分情况下,需要使用特殊的类加载器,如Android插件化中,使用动态代理,可能需要传入插件对应的类加载器.

Class<?>[] interfaces:

这里,传入你感兴趣的 委托类所实现的接口.
如下面例子中的 Subject.class, 则你需要传入 new Class[]{Subject.class}作为参数.
如果 RealSubject实现了多个接口Subject1,Subject2,Subject3...,而你对其中Subject1,Subject2感兴趣,
你可以传入 new Class[]{Subject1.class,Subject2.class}, JVM运行时生成的字节码类,将会实现传入的这些接口.

InvocationHandler h

方法调度处理器接口. 内部有一个回调方法 Object invoke(Object proxy, Method method, Object[] args).
实现该方法,可以拦截 上一个参数传入的接口的方法, 你可以对这些方法进行增强, 甚至改变方法的行为.

实现 InvocationHandler 通常需要传入一个 委托对象, 然后在invoke()方法中,对委托对象进行修改或者增强操作.

来看一个简单的动态代理的例子.

// proxy.Subject.java
public interface Subject {
    void doSomething();
}

// proxy.RealSubject.java
public class RealSubject implements Subject {
    @Override
    public void doSomething() {
        System.out.println("RealSubject do ...");
    }
}

// ReflectTest.java
public class ReflectTest {
    public static void main(String[] args) {
        // 委托对象
        RealSubject realSubject = new RealSubject();
        // 方法调度处理器
        InvocationHandler handler = new ProxyHandler(realSubject);

        // 生成代理对象
        Subject proxy = (Subject) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{Subject.class},
                handler
        );

        // 这里使用辅助类,保存JVM生成动态代理类到本地,此时先忽略
        ProxyUtils.generateClassFile(realSubject.getClass(), "SubjectProxy");

        // 代理运行方法
        proxy.doSomething();
    }

    // 代理方法调度器
    static class ProxyHandler implements InvocationHandler {
        // 委托对象
        RealSubject realSubject;

        ProxyHandler(RealSubject realSubject) {
            this.realSubject = realSubject;
        }

        /**
         * 在此方法中,进行委托对象方法的增强或者修改
         * 该示例中只是简单的对其添加log.
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("do before ...");

            // 处理委托对象的方法
            Object result = method.invoke(realSubject, args);
            System.out.println("do after ...");
            return result;
        }
    }
}

//do before ...
//RealSubject do ...
//do after ...

从这里,我们可以窥探出 动态代理与静态代理的一些差别,我们来总结一下.

动态代理使用 InvocationHandlerinvoke()方法来统一处理委托类的方法, 也就是说, 即使委托对象实现的接口中有几百个方法,我们也只要在这一个方法中处理即可.换句话说,如果委托类增加了一些方法,而我们不需要对方法进行修改,那动态代理这部分的代码,可以不需要改动,静态代理则达不到这种效果.

动态代理在运行时,动态生成字节码数据,我们在编写代码的时候是看不到真正的代理类代码.

动态代理一个重要的应用就是 AOP(面向切面编程), 主要处理 日志记录,性能统计,安全控制,事务处理,异常处理等等

如果我们需要对一些 不对外开发的或者不容易直接操作的类或者api进行操作,我们也可以使用 动态代理来处理. 如Android插件化中,对系统资源的HOOK, 如对ActivityManager的Hook就用到动态代理技术.

动态代理原理

Java的动态代理是通过 Proxy .newProxyInstance()方法来生成的. 我们来追踪下源码, 源码中 我把无关和不打紧的代码去掉,以便分析.

public class Proxy {
    // 生成的动态代理的构造参数类
    private static final Class<?>[] constructorParams = {InvocationHandler.class};

    // 动态代理类的缓存池
    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
            proxyClassCache = new WeakCache<>(new KeyFactory(), new Proxy.ProxyClassFactory());

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
            throws IllegalArgumentException {
        final Class<?>[] intfs = interfaces.clone();
        // 1. 生成动态代理类
        Class<?> cl = getProxyClass0(loader, intfs);
        try {
            // 2. 反射创建动态代理类实例
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            return cons.newInstance(new Object[]{h});
        } catch (Exception e) {
            throw new InternalError(e.toString(), e);
        }
    }

    private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
        // 如果通过传入的 类加载器和接口类已经缓存过, 则直接从缓存中获取 之前已经生成的代理类
        // 否则, 将会通过ProxyClassFactory来创建一份动态代理类
        return proxyClassCache.get(loader, interfaces);
    }

    /**
     * 生成代理类的工厂
     */
    private static final class ProxyClassFactory
            implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
        // 生成的动态代理类的类名前缀
        private static final String proxyClassNamePrefix = "$Proxy";

        // 为生成的动态代理类加上数字标签
        private static final AtomicLong nextUniqueNumber = new AtomicLong();

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {

                // 1. 验证传入的接口类,是否是用传入的 类加载器 加载的,不是则报错
                Class<?> interfaceClass = null;
                try {
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(intf + " is not visible from class loader");
                }

                // 2. 验证传入的类是接口类型, 不是则报错
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(interfaceClass.getName() + " is not an interface");
                }

                // 3. 验证是否传入重复的接口类 , 是则报错
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                            "repeated interface: " + interfaceClass.getName());
                }
            }

            String proxyPkg    = null;     // package to define proxy class in
            int    accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            // 4. 对所有非公开的接口进行判断,
            // 4.1 判断所有的非公开接口,是不是在同一个包下, 不在 则报错, 在则生成的代理类与其 同包名
            // 4.2 如果没有非公开的接口,直接使用默认的包名 : com.sun.proxy
            for (Class<?> intf : interfaces) {
                int flags = intf.getModifiers();
                if (!Modifier.isPublic(flags)) {
                    accessFlags = Modifier.FINAL;
                    String name = intf.getName();
                    int    n    = name.lastIndexOf('.');
                    String pkg  = ((n == -1) ? "" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if (!pkg.equals(proxyPkg)) {
                        throw new IllegalArgumentException(
                                "non-public interfaces from different packages");
                    }
                }
            }
            if (proxyPkg == null) {
                // if no non-public proxy interfaces, use com.sun.proxy package
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }

            // 5. 生成动态代理类的名称
            // 形如 : com.sun.proxy.$Proxy0 com.sun.proxy.$Proxy1
            long   num       = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            // 6. 通过代理生成器 直接生成 代理类的 字节码数据 即 .class类型的数据
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                    proxyName, interfaces, accessFlags);
            try {
                // 7. 将字节码数据转为 Class 类
                return defineClass0(loader, proxyName,
                        proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }
        }
    }
}

从源码中我们可以看出, 生成代理对象大步骤分为两个.

  1. 生成动态代理类对象
  2. 反射生成动态代理实例对象

我们主要看第一个步骤. 它又分为好几个小的流程, 在上面源码分析中已经很清楚的写出来,这里就不在赘述.

它最终会调用 ProxyGenerator.generateProxyClass() 方法来生成字节码文件.

然而这个过程 我们并不能拿到 这个生成的对象,也就是说 我们从代码上是看不到这份文件的, 这对我们分析有很大的麻烦.

而既然 代码是通过 ProxyGenerator.generateProxyClass()这个方法来生成, 我们可以尝试通过这个方法来将生成的代码保存到本地, 以便于分析.

public class ProxyUtils {
    /**
     * 根据类信息和提供的代理类名称,生成字节码并且保存到本地
     *
     * @param clazz     委托类
     * @param proxyName 生成代理类的名称
     */
    public static void generateClassFile(Class<?> clazz, String proxyName) {
        // 根据类信息和提供的代理类名称,生成字节码
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
        String path      = clazz.getResource(".").getPath();
        System.out.println(path);
        FileOutputStream out = null;

        try {
            // 生成.class文件保存到本地
            out = new FileOutputStream(path + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这个类可以帮助我们保存生成的代理类对象, 在上个例子中 有调用到过 ProxyUtils.generateClassFile(realSubject.getClass(), "SubjectProxy");,
我们直接打开生成的类来看下.

public final class SubjectProxy extends Proxy implements Subject {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public SubjectProxy(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 void doSomething() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    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 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"));
            m3 = Class.forName("proxy.Subject").getMethod("doSomething");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以看出, 生成的代理类, 是继承 Proxy类,并且实现了Subject接口, 构造函数传入 InvocationHandler对象.
然后,它将实现来的所有方法, 都通过反射的方式,调用 InvocationHandler.invoke()方法来实现.
InvocationHandler中将传入委托类 来完成反射调用.

也就是说, 代理类, 最终调用的是 通过InvocationHandler.invoke()方法进行增强或者修改的, 委托类(RealSubject)所对应的方法.

至此,我们分析完了, 动态代理实现的原理.

动态代理的不足

动态代理是一定要基于接口的, 如果委托对象没有实现相应的接口, 是无法对其创建动态代理的.
JDK为我们提供的代理实现方案确实没法解决这个问题, 那怎么办呢? 可以使用 CGLib动态代理, 这里就不对其进行展开, 感兴趣的可以自行搜索了解.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容