Java动态代理 深度详解(二)

如何使用动态代理?

参照上面的例子,我们可以知道要实现动态代理需要做两方面的工作。

  • 首先需要新建一个类,并且这个类必须实现 InvocationHandler 接口。
//杏仁动态代理
public class ApricotHandler implements InvocationHandler{

    private Object object;

    public ApricotHandler(Object object) {
        this.object = object;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(object, args);    //调用真正的蛋糕机做蛋糕
        System.out.println("adding apricot...");
        return result;
    }
}
  • 在调用的时候使用 Proxy.newProxyInstance() 方法生成代理类。
public class CakeShop {
    public static void main(String[] args) {
        //水果蛋糕撒一层杏仁
        CakeMachine fruitCakeMachine = new FruitCakeMachine();
        ApricotHandler fruitCakeApricotHandler = new ApricotHandler(fruitCakeMachine);
        CakeMachine fruitCakeProxy = (CakeMachine) Proxy.newProxyInstance(fruitCakeMachine.getClass().getClassLoader(),
                fruitCakeMachine.getClass().getInterfaces(), fruitCakeApricotHandler);
        fruitCakeProxy.makeCake(); 
}
  • 最后直接使用生成的代理类调用相关的方法即可。

动态代理的几种实现方式

动态代理其实指的是一种设计模式概念,指的是通过代理来做一些通用的事情,常见的应用有权限系统、日志系统等,都用到了动态代理。

Java 动态代理只是动态代理的一种实现方式而已,动态代理还有另外一种实现方式,即

CGLib(Code Generation Library)。

Java 动态代理只能针对实现了接口的类进行拓展,所以细心的朋友会发现我们的代码里有一个叫 MachineCake 的接口。而 CGLib 则没有这个限制,因为 CGLib 是使用继承原有类的方式来实现代理的。

我们还是举个例子来说明

CGLib 是如何实现动态代理的吧。还是前面的例子:我们要做杏仁水果蛋糕、巧克力水果蛋糕、五仁巧克力蛋糕,这时候用代码描述是这样的。

首先我们需要写一个杏仁拦截器类,这个拦截器可以给做好的蛋糕加上杏仁。

public class ApricotInterceptor implements MethodInterceptor {
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        methodProxy.invokeSuper(o, objects);
        System.out.println("adding apricot...");
        return o;
    }
}

接着直接让蛋糕店使用 CGLib 提供的工具类做杏仁水果蛋糕:

public class CakeShop {
    public static void main(String[] args) { 
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(FruitCakeMachine.class);
        enhancer.setCallback(new ApricotInterceptor());
        FruitCakeMachine fruitCakeMachine = (FruitCakeMachine) enhancer.create();
        fruitCakeMachine.makeCake();
    }
}

上面的 enhancer.setSuperClass() 设置需要增强的类,而 enhancer.setCallback() 则设置需要回调的拦截器,即实现了 MethodInterceptor 接口的类。最后最后使用 enhancer.create() 生成了对应的增强类,最后输出结果为:

making a Fruit Cake...
adding apricot...

和我们预期的一样。如果要做一个杏仁巧克力蛋糕,那么直接让蛋糕店利用ApricotHandler 再做一个就可以了,它们的区别只是传入的增强类不同。

public class CakeShop {
    public static void main(String[] args) { 
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ChocolateCakeMachine.class);
        enhancer.setCallback(new ApricotInterceptor());
        ChocolateCakeMachine chocolateCakeMachine = (ChocolateCakeMachine) enhancer.create();
        chocolateCakeMachine.makeCake();
    }
}

可以看到,这里传入的增强类是 ChocolateCakeMachine,而不是之前的 FruitCakeMachine。

对比 Java 动态代理和 CGLib 动态代理两种实现方式,你会发现

Java 动态代理适合于那些有接口抽象的类代理,而 CGLib 则适合那些没有接口抽象的类代理。

Java动态代理的原理

从上面的例子我们可以知道,Java 动态代理的入口是从 Proxy.newInstance() 方法中开始的,那么我们就从这个方法开始边剖析源码边理解其原理。

image

其实通过这个方法,Java 替我们生成了一个继承了指定接口(CakeMachine)的代理类(ApricotHandler)实例。从

Proxy.newInstance()

的源码我们可以看到首先调用了 getProxyClass0 方法,该方法返回了一个 Class 实例对象,该实例对象其实就是 ApricotHandler 的 Class 对象。接着获取其构造方法对象,最后生成该 Class 对象的实例。其实这里最主要的是 getProxyClass0() 方法,这里面动态生成了 ApricotHandler 的 Class 对象。下面我们就深入到 getProxyClass0() 方法中去了解这里面做了什么操作。

image

getProxyClass0() 方法首先是做了一些参数校验,之后从 proxyClassCache 参数中取出 Class 对象。其实 proxyClassCache 是一个 Map 对象,缓存了所有动态创建的 Class 对象。从源码中的注释可以知道,如果从 Map 中取出的对象为空,那么其会调用

ProxyClassFactory

生成对应的 Class 对象。

image

在 ProxyClassFactory 类的源码中,最终是调用了

ProxyGenerator.genrateProxyClass()

方法生成了对应的 class 字节码文件。

到这里,我们已经把动态代理的 Java 源代码都解析完了,现在思路就很清晰了。Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

方法简单来说执行了以下操作:

  • 1、生成一个实现了参数 interfaces 里所有接口且继承了 Proxy 的代理类的字节码,然后用参数里的 classLoader 加载这个代理类。
  • 2、使用代理类父类的构造函数 Proxy(InvocationHandler h) 来创造一个代理类的实例,将我们自定义的 InvocationHandler 的子类传入。
  • 3、返回这个代理类实例,因为我们构造的代理类实现了 interfaces(也就是我们程序中传入的 fruitCakeMachine.getClass().getInterfaces() 里的所有接口,因此返回的代理类可以强转成 MachineCake 类型来调用接口中定义的方法。

CGLib动态代理的原理

因为 JVM 并不允许在运行时修改原有类,所以所有的动态性都是通过新建类来实现的,上面说到的 Java 动态代理也不例外。所以对于 CGLib 动态代理的原理,其实也是通过动态生成代理类,最后由代理类来完成操作实现的。

对于 CGLib 动态代理的实现,我并没有深入到源码中,而是通过查阅资料了解了其大概的实现原理。

  • 首先,我们在使用的时候通过 enhancer.setSuperclass(FruitCakeMachine.class) 传入了需要增加的类,CGLib 便会生成一个继承了改类的代理类。
  • 接着,我们通过 enhancer.setCallback(new ApricotInterceptor()) 传入了代理类对象,CGLib 通过组装两个类的结构实现一个静态代理,从而达到具体的目的。

而在 CGLib 生成新类的过程中,其使用的是一个名为 ASM 的东西,它对 Java 的 class 文件进行操作、生成新的 class 文件。如果你对 CGLib 的原理感兴趣,不妨看看这篇文章:从兄弟到父子:动态代理在民间是怎么玩的?

动态代理的应用

动态代理在代码界可是有非常重要的意义,我们开发用到的许多框架都使用到了这个概念。我所知道的就有:Spring AOP、Hibernate、Struts 使用到了动态代理。

  • Spring AOP。Spring 最重要的一个特性是 AOP(Aspect Oriented Programming 面向切面编程),利用 Spring AOP 可以快速地实现权限校验、安全校验等公用操作。而 Spring AOP 的原理则是通过动态代理实现的,默认情况下 Spring AOP 会采用 Java 动态代理实现,而当该类没有对应接口时才会使用 CGLib 动态代理实现。
  • Hibernate。Hibernate 是一个常用的 ORM 层框架,在获取数据时常用的操作有:get() 和 load() 方法,它们的区别是:get() 方法会直接获取数据,而 load() 方法则会延迟加载,等到用户真的去取数据的时候才利用代理类去读数据库。
  • Struts。Struts 现在虽然因为其太多 bug 已经被抛弃,但是曾经用过 Struts 的人都知道 Struts 中的拦截器。拦截器有非常强的 AOP 特性,仔细了解之后你会发现 Struts 拦截器其实也是用动态代理实现的。

总结

我们通过蛋糕店的不同业务场景介绍了静态代理和动态代理的应用,接着重点介绍了动态代理两种实现方式(Java 动态代理、CGLib 动态代理)的使用方法及其实现原理,其中还针对 Java 动态代理的源码进行了简单的分析。最后,我们介绍了动态代理在实际上编程中的应用(Spring AOP、Hibernate、Struts)。

欢迎加入学习交流群569772982,大家一起学习交流

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,295评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,015评论 11 349
  • 今天是AlphaGo对战李世石的的最后一场比赛,随着5场比赛的即将落幕和“阿尔法狗”的完美胜利,让国人开始更加期待...
    柠檬乌冬面阅读 1,237评论 0 2
  • 零用钱 儿子之前收到几百元的红包,放到他自己的钱包里。上完财商课后,我决定对儿子的零用钱进行管理。于是和儿子约定:...
    ivisliu阅读 472评论 0 0
  • 有时候,不写点东西还真意识不到“脑子里以为正常的句子怎么会这么残疾?!”而且,即使自己明明以前遇到过这种情况,在看...
    去往风口的猪阅读 145评论 0 0