为了账号安全,请及时绑定邮箱和手机立即绑定

Java动态代理 深度详解

标签:
Java

代理模式是设计模式中非常重要的一种类型,而设计模式又是编程中非常重要的知识点,特别是在业务系统的重构中,更是有举足轻重的地位。代理模式从类型上来说,可以分为静态代理和动态代理两种类型。

在开始之前,我们先假设这样一个场景:有一个蛋糕店,它们卖的蛋糕都是用蛋糕机做的,而且不同种类的蛋糕由不同的蛋糕机来做,这样就有:水果蛋糕机、巧克力蛋糕机等。它们卖的面包片也是用面包机做的,同样不同种类的面包片也是由不同的面包机来做,这样就有:葡萄干面包机、红豆面包机等。这个场景用 Java 语言描述就是下面这样:

//做蛋糕的机器
public interface CakeMachine{
    void makeCake();
}

//专门做水果蛋糕的机器
class FruitCakeMachine implements CakeMachine{
    public void makeCake() {
        System.out.println("Making a fruit cake...");
    }
}

//专门做巧克力蛋糕的机器
public class ChocolateCakeMachine implements CakeMachine{
    public void makeCake() {
        System.out.printf("making a Chocolate Cake...");
    }
}

//做面包的机器
public interface BreadMachine {
    void makeBread();
}

//专门做红豆面包的机器
public class RedBeanBreadMachine implements BreadMachine {
    public void makeBread() {
        System.out.println("making red bean bread....");
    }
}

//专门做葡萄干面包的机器
public class CurrantBreadMachine implements BreadMachine{
    public void makeBread() {
        System.out.println("making currant bread...");
    }
}

//蛋糕店
public class CakeShop {
    public static void main(String[] args) {
        new FruitCakeMachine().makeCake();
        new ChocolateCakeMachine().makeCake();
        new RedBeanBreadMachine().makeBread();
        new CurrantBreadMachine().makeBread();
    }
}

上面的代码抽象出了一个 CakeMachine 接口和 BreadMachine 接口,有各种蛋糕机(FruitCakeMachine、ChocolateCakeMachine 等)实现了 CakeMachine 接口,有各种面包机(RedBeanBreadMachine、CurrantBreadMachine 等)实现了 BreadMachine 接口,最后蛋糕店(CakeShop)直接利用这些蛋糕机做蛋糕。

这样的一个例子真实地描述了实际生活中的场景。但生活中的场景往往是复杂多变的,假设这个时候来了一个顾客,他想要一个水果蛋糕,但他特别喜欢杏仁,希望在水果蛋糕上加上一层杏仁。这时候我们应该怎么做呢?

因为我们的蛋糕机只能做水果蛋糕(程序设定好了),没办法做杏仁水果蛋糕。最简单的办法是直接修改水果蛋糕机的程序,做一台能做杏仁水果蛋糕的蛋糕机。这种方式对应的代码修改也很简单,直接在原来的代码上进行修改,生成一台专门做杏仁水果蛋糕的机器就好了,修改后的 FruitCakeMachien 类应该是这样子:

//专门做水果蛋糕的机器,并且加上一层杏仁
class FruitCakeMachine implements CakeMachine{
    public void makeCake() {
        System.out.println("making a Fruit Cake...");
        System.out.println("adding apricot...");
    }
}

虽然上面这种方式实现了我们的业务需求。但是仔细想一想,在现实生活中如果我们遇到这样的一个需求,我们不可能因为一个顾客的特殊需求就去修改一台蛋糕机的硬件程序,这样成本太高!而且从代码实现角度上来说,这种方式从代码上不是很优雅,修改了原来的代码。根据代码圈中「对修改封闭、对扩展开放」的思想,我们在尝试满足新的业务需求的时候应该尽量少修改原来的代码,而是在原来的代码上进行拓展。

那我们究竟应该怎么做更加合适一些呢?我们肯定是直接用水果蛋糕机做一个蛋糕,然后再人工撒上一层杏仁啦。我们需要做的,其实就是设计一个杏仁代理类(ApricotCakeProxy),这个代理类就完成撒杏仁这个动作,之后让蛋糕店直接调用即可代理类去实现即可。

//杏仁蛋糕代理
public class ApricotCakeProxy implements CakeMachine{
    private CakeMachine cakeMachine;
    public ApricotCakeProxy(CakeMachine cakeMachine) {
        this.cakeMachine = cakeMachine;
    }
    public void makeCake() {
        cakeMachine.makeCake();
        System.out.println("adding apricot...");
    }
}

//蛋糕店
public class CakeShop {
    public static void main(String[] args) {
          //可以给各种各样的蛋糕加上杏仁
          FruitCakeMachine fruitCakeMachine = new FruitCakeMachine();
        ApricotCakeProxy apricotProxy = new ApricotCakeProxy(fruitCakeMachine);
        apricotProxy.makeCake();
        apricotProxy = new ApricotCakeProxy(new ChocolateCakeMachine());
        apricotProxy.makeCake();
    }
}

这其实就对应了即使模式中的代理模式,虽然调用的是 ApricotCakeProxy 类的方法,但实际上真正做蛋糕的是 FruitCakeMachine 类。ApricotCakeProxy 类只是在 FruitCakeMachine 做出蛋糕后,撒上一层杏仁而已。而且通过代理,我们不仅可以给水果蛋糕撒上一层杏仁,还可以给巧克力蛋糕、五仁蛋糕等撒上一层杏仁。只要它是蛋糕(实现了 CakeMachine 接口),那么我们就可以给这个蛋糕撒上杏仁。

通过代理实现这样的业务场景,这样我们就不需要在原来的类上进行修改,从而使得代码更加优雅,拓展性更强。如果下次客人喜欢葡萄干水果蛋糕了了,那可以再写一个 CurrantCakeProxy 类来撒上一层葡萄干,原来的代码也不会被修改。上面说的这种业务场景就是代理模式的实际应用,准确地说这种是静态代理。

业务场景的复杂度往往千变万化,如果这个特别喜欢杏仁的客人,他也想在面包上撒一层杏仁,那我们怎么办?我们能够使用之前写的 ApricotCakeProxy 代理类么?不行,因为 ApricotCakeProxy 里规定了只能为蛋糕(实现了 CakeMachine 接口)的实体做代理。这种情况下,我们只能再写一个可以为所有面包加杏仁的代理类:ApricotBreadProxy。

//杏仁面包代理
public class ApricotBreadProxy implements BreadMachine{

    private BreadMachine breadMachine;

    public ApricotBreadProxy(BreadMachine breadMachine) {
        this.breadMachine = breadMachine;
    }

    public void makeBread() {
        breadMachine.makeBread();
        System.out.println("adding apricot...");
    }
}

//蛋糕店
public class CakeShop {
    public static void main(String[] args) {
          //可以给各种各样的面包加上杏仁
        RedBeanBreadMachine redBeanBreadMachine = new RedBeanBreadMachine();
        ApricotBreadProxy apricotBreadProxy = new ApricotBreadProxy(redBeanBreadMachine);
        apricotBreadProxy.makeBread();
        CurrantBreadMachine currantBreadMachine = new CurrantBreadMachine();
        apricotBreadProxy = new ApricotBreadProxy(currantBreadMachine);
        apricotBreadProxy.makeBread();
    }
}

最终输出结果:

making red bean bread....
adding apricot...
making currant bread...
adding apricot...

我们可以看到我们也成功地做出了客人想要的杏仁红豆面包、杏仁葡萄干面包。

对于客人来说,他肯定希望我们所有的产品都有一层杏仁,这样客人最喜欢了。为了满足客人的需求,那如果我们的产品有 100 种(饼干、酸奶等),我们是不是得写 100 个代理类呢?有没有一种方式可以让我们只写一次实现(撒杏仁的实现),但是任何类型的产品(蛋糕、面包、饼干、酸奶等)都可以使用呢?其实在 Java 中早已经有了针对这种情况而设计的一个接口,专门用来解决类似的问题,它就是动态代理 —— InvocationHandler。

动态代理与静态代理的区别是静态代理只能针对特定一种产品(蛋糕、面包、饼干、酸奶)做某种代理动作(撒杏仁),而动态代理则可以对所有类型产品(蛋糕、面包、饼干、酸奶等)做某种代理动作(撒杏仁)。

接下来我们针对这个业务场景做一个代码的抽象实现。首先我们分析一下可以知道这种场景的共同点是希望在所有产品上都做「撒一层杏仁」的动作,所以我们就做一个杏仁动态代理(ApricotHandler)。

//杏仁动态代理
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;
    }
}

撒杏仁的代理写完之后,我们直接让蛋糕店开工:

public class CakeShop {
    public static void main(String[] args) {
        //动态代理(可以同时给蛋糕、面包等加杏仁)
        //给蛋糕加上杏仁
        FruitCakeMachine fruitCakeMachine = new FruitCakeMachine();
        ApricotHandler apricotHandler = new ApricotHandler(fruitCakeMachine);
        CakeMachine cakeMachine = (CakeMachine) Proxy.newProxyInstance(fruitCakeMachine.getClass().getClassLoader(),
                fruitCakeMachine.getClass().getInterfaces(),
                apricotHandler);
        cakeMachine.makeCake();
        //给面包加上杏仁
        RedBeanBreadMachine redBeanBreadMachine = new RedBeanBreadMachine();
        apricotHandler = new ApricotHandler(redBeanBreadMachine);
        BreadMachine breadMachine = (BreadMachine) Proxy.newProxyInstance(redBeanBreadMachine.getClass().getClassLoader(),
                redBeanBreadMachine.getClass().getInterfaces(),
                apricotHandler);
        breadMachine.makeBread();
    }
}

输出结果为:

making a fruit cake...
adding apricot...
making red bean bread....
adding apricot...

从输出结果可以知道,这与我们想要的结果是一致的。与静态代理相比,动态代理具有更加的普适性,能减少更多重复的代码。试想这个场景如果使用静态代理的话,我们需要对每一种类型的蛋糕机都写一个代理类(ApricotCakeProxy、ApricotBreadProxy、ApricotCookieProxy等)。但是如果使用动态代理的话,我们只需要写一个通用的撒杏仁代理类(ApricotHandler)就可以直接完成所有操作了。直接省去了写 ApricotCakeProxy、ApricotBreadProxy、ApricotCookieProxy 的功夫,极大地提高了效率。

看到这里,大家应该清楚为什么有了静态代理之后,还需要有动态代理了吧。静态代理只能针对某一接口(面包 或 蛋糕)进行操作,如果要对所有接口都(所有产品)都能做一样操作,那就必须要动态代理出马了。

如何使用动态代理?

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

  • 必须新建一个类,并且这个类必须实现 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) {
        //给蛋糕加上杏仁
        FruitCakeMachine fruitCakeMachine = new FruitCakeMachine();
        ApricotHandler apricotHandler = new ApricotHandler(fruitCakeMachine);
        CakeMachine cakeMachine = (CakeMachine) Proxy.newProxyInstance(fruitCakeMachine.getClass().getClassLoader(),
                fruitCakeMachine.getClass().getInterfaces(),
                apricotHandler);
        cakeMachine.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) { 
        //CGLib动态代理(可以同时给蛋糕、面包等加杏仁)
        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(RedBeanBreadMachine.class);
        enhancer.setCallback(new ApricotInterceptor());
        RedBeanBreadMachine chocolateCakeMachine = (RedBeanBreadMachine) enhancer.create();
        chocolateCakeMachine.makeBread();
    }
}

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

对比 Java 动态代理和 CGLib 动态代理两种实现方式,你会发现 Java 动态代理适合于那些有接口抽象的类代理,而 CGLib 则适合那些没有接口抽象的类代理。

Java动态代理的原理

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

Java动态代理

其实通过这个方法,Java 替我们生成了一个继承了指定接口(CakeMachine)的代理类(ApricotHandler)实例。从 Proxy.newInstance() 的源码我们可以看到首先调用了 getProxyClass0 方法,该方法返回了一个 Class 实例对象,该实例对象其实就是 ApricotHandler 的 Class 对象。接着获取其构造方法对象,最后生成该 Class 对象的实例。其实这里最主要的是 getProxyClass0() 方法,这里面动态生成了 ApricotHandler 的 Class 对象。下面我们就深入到 getProxyClass0() 方法中去了解这里面做了什么操作。

Java动态代理

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

Java动态代理

在 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)。

希望这篇文章帮助大家更好地理解动态代理。

以上。

点击查看更多内容
25人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消