Java 动态代理原理

动态代理

代理模式

代理模式强调在对被代理对象的控制。代理模式知识点不做赘述。

静态代理,代理类的代码是在编译期间就已经确定好的。

动态代理,代理类的代码编译期间是没有的,只有在运行期间才能确定。

简单回顾下静态代理相关代码,以统计方法耗时为例。

//声明接口类
public interface IDoSth {
    void doSth(String s);
}
//声明实际类
public class DoSth implements IDoSth{
    @Override
    public void doSth(String s) {
        System.out.println("doSth:" + s);
    }
}
//声明静态代理类,可对被代理的对象进行定制化处理,此处将 xx 掉包
public class ProxyDoSth implements IDoSth{
    private IDoSth instance;
    public ProxyDoSth(IDoSth ins) {
        this.instance = ins;
    }
    @Override
    public void doSth(String s) {
        long start = System.currentTimeMillis();
        instance.doSth(s);
        System.out.println(System.currentTimeMillis()-start);
    }
}

动态代理相关语法

// 动态代理相关代码
        IDoSth ins = new DoSth();
      IDoSth proxy = (IDoSth) Proxy.newProxyInstance(Test.class.getClassLoader(), ins.getClass().getInterfaces(), new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long start = System.currentTimeMillis();
            Object res = method.invoke(ins, args);
            System.out.println("method: " + method.getName() + " costTime : "
                + (System.currentTimeMillis() - start));
            return res;
        }
      });
        proxy.doSth("做饭");
        // 打印 proxy 对象相关类和接口信息
        System.out.println(proxy.getClass()); 
      System.out.println(proxy.getClass().getSuperclass());
      for (Class cls : proxy.getClass().getInterfaces()) {
            System.out.println(cls);
      }
}
//输出结果
doSth:做饭
method: doSth costTime : 1 // 方法耗时
class com.sun.proxy.$Proxy0 //proxy 类的类名
class java.lang.reflect.Proxy //proxy 类父类
interface jjava.dynamicProxy.IDoSth //proxy 类实现的接口

由 proxy 对象可知,其类名com.sun.proxy.$Proxy0 ,该类父类为 java.lang.reflect.Proxy且实现了 IDoSth 接口。

这里面有两个关键类,一个是 Proxy,一个是InvocationHandler。Proxy 主要负责代理类的生成,InvocationHandler 主要负责用户自定义实现代理类的功能。

动态代理的本质

动态代理没那么玄乎,仅仅是在程序运行期间生成了代理类的代码而已。那这份代码长什么样子呢?

我们将动态生成的 Proxy 类的代码保存在class 文件中,这一步代码如下

//$Proxy0 为生成的代理类的名字,path 为保存的文件名,实际上就是将byte 数组保存到本地,newProxyInstance() 方法内部就是通过这种方式生成的 代理类的 byte[]
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", ins.getClass().getInterfaces());
      String path = "/${projectPath}/jjava/dynamicProxy/$Proxy0.class";
      try(FileOutputStream fos = new FileOutputStream(path)) {
            fos.write(classFile);
            fos.flush();
            System.out.println("cls 文件写入成功");
      } catch (Exception e) {
            System.out.println("cls 文件写入失败");
            e.printStackTrace();
      }

生成的 $Proxy0 类的代码如下

public final class $Proxy0 extends Proxy implements IDoSth {
    private static Method m1;   //java.lang.Object#equals()
    private static Method m2;   //java.lang.Object#toString()
    private static Method m3;   //jjava.dynamicProxy.IDoSth#doSth()
    private static Method m0;   //java.lang.Object#hashcode()
    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);
        }
    }
        // 通过 proxy.doSth(xx) 时,本质上调的是这个方法,内部是通过调用 InvocationHandler 中的 invoke 方法实现的。
    public final void doSth(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 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("jjava.dynamicProxy.IDoSth").getMethod("doSth", Class.forName("java.lang.String"));
            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());
        }
    }   
}

通过类信息,可以和签名的log信息互相验证。同时 由于所有动态代理产生的类都是Proxy 的子类,所以我们在使用时,只能用 interfaces 中的任一接口承接,而不能用自定义的其他类型,否则会报 ClassCastException。

可以看到 $Proxy0类的构造函数,包括一个 InvocationHandler 类型的参数,并将该参数传给了父类的函数,该阐述就是我们调用Proxy.newProxyInstance() 方法传入的 h。

我们在调用代理类的方法时,如 proxy.doSth(),本质上是通过super.h.invoke(this, m3, new Object[]{var1}); 实现的。h为我们传入的 InvocationHandler 的实例,m3 为一个Method 对象,最终是通过调用 m3.invoke(target,args ) 实现被代理对象的方法调用。m3 是一个 Method 类的实例。与此同时,代理类 $Proxy0 除了实现所有接口类的方法外,还自动帮我们添加了Object 类 的3个方法,分别是 toString()、hashcode()、equals()。并且在代理类的类初始化阶段对这几个Method 实例进行赋值。

动态代理类的名字规则

类全路径名名 = pkgName + className
包名主要主要取决于接口是否是public 的。如果要代理的接口全都是public 的,则类的全路径名是 com.sun.proxy,否则就是非public 接口的包名。
类名为 Proxy{n},n为当前已经创建的动态代理类的个数,由于有缓存的逻辑,只有不同数组接口的代理,才会创建新的类。n从0开始,在同进程内依次递增。源码如下:

    Proxy$ProxyClassFactory#apply()                 
                        /*
             * Record the package of a non-public proxy interface so that the
             * proxy class will be defined in the same package.  Verify that
             * all non-public proxy interfaces are in the same package.
             */
            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 + ".";// 默认值 com.sun.proxy.
            }

            /*
             * Choose a name for the proxy class to generate.
             */
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num; //proxyClassNamePrefix 为“$Proxy”

个人理解

动态代理是对接口的代理,而不是对类的代理。没有接口则无从代理。

jvm由于不支持运行时修改一个类或修改方法,如修改方法内容,添加方法等。动态代理由于是在运行期间生产了新的代理类去扩展功能,也算是对此种不足提供了一种支持。

动态代理是 AOP 思想的一种实现,完美的实现了一个切面。方便我们在每个方法执行前后定制功能。最常见的就是统计每个方法的执行耗时,在方法执行前记录时间戳,方法执行完后再记录一次。

静态代理也可以做切面编程,只不过当使用静态代理时,手动编写代理接口类的成本太大。相对于静态代理,动态代理是在此层面上做了一层抽象和封装。定制化需求只需要在 InvocationHandler 的 invoke() 方法中添加就可以了。而且 invoke() 方法传入的参数是 一个 object,意味着任何类的实例都可以被代理(只要实现相应接口),大大增强了InvocationHandler 子类复用性。

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

很多 android 和 java 框架都应用了动态代理技术。如 Spring,retrofit等。

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

推荐阅读更多精彩内容