剖析代理模式及Java两种动态代理(JDK动态代理和CGLIB动态代理)

简介: 本文详述了代理模式以及我们经常接触到的两种具体实现(JDK动态代理和CGLIB动态代理),为读者理解代理模式、JDK动态代理和CGLIB动态代理提供帮助

代理模式

什么是代理模式

代理模式是设计模式的一种,他是指一个对象A通过持有另一个对象B,可以具有B同样的行为的模式。他在对象B的基础上提供了一层访问控制,当你需要访问对象B时,你需要经过对象B的代理对象A来实现访问。因此代理模式也被称为委托模式,他能够提供非常好的访问控制。
对象A并不提供真正的执行逻辑,而是通过组合B去调用B的目标方法来实现目标逻辑。而A的作用则是在调用B方法的前后提供一些准备和善后的工作。即A虽然是“伪军”,但它可以增强B,在调用B的方法前后都做些其他的事情。Spring AOP就是使用了动态代理完成了代码的动态“织入”。

举个例子,今天我有一部好的电影叫做奥特曼大战菊花怪,我觉得需要一个实力演员来镇场,于是我准备去找彭yu晏来演菊花怪,于是我找到了他的电话打给他。但是他说“菊花怪吗?听起来好有意思啊,但是我不知道有没有时间,你找我经纪人聊吧“。于是他给了我经纪人的电话,经纪人来负责他的时间安排和酬金。
其中经纪人的角色就是代理,而彭yu晏就是被代理类,我要通过代理来找彭yu晏演戏,而代理只负责收钱和安排,安排好了谈妥了他才会让彭yu晏来演菊花怪。并且真正演戏的是yu晏,而不是代理。

为什么要使用代理模式

通过前面的例子我们能够理解什么是代理以及代理负责做什么,并且一定程度上理解了代理的重要性。那么在映射到程序中我们为什么需要代理模式。
以Spring AOP为例,Spring AOP就是代理模式的一个典型的使用,通过代理模式我们能够将我们的主干逻辑从复杂的逻辑线中横向抽取出来作为一个单独的逻辑。然后再通过代理将一些边缘的、细节的逻辑插入到刚才抽取出的主干逻辑的前后。以下图为例(结合前面演戏的例子)
在这里插入图片描述
这样的方式可以简化代码的复杂度,分离主要逻辑和次要逻辑,使得代码职责更加清淅。同时,由于一个代理类可以代理不同的对象(只要实现了相同接口),因此代理模式也具有非常良好的扩展性。

静态代理

静态代理是代理模式的一种基本使用方法,一般不太常用,因为扩展性太差。简单来说就是代理类与被代理类完全绑定,即我们常说的写死。但是通过静态代理我们能够毕竟直观简单的理解代理模式。

同样以前面的演戏为例

/**
 * 演员(被代理类)
 */
class Actor{
    private String name;

    public Actor(String name){
        this.name = name;
    }

    public void action(){
        System.out.println(this.name + "开始拍戏");
    }
}
/**
 * 经纪人(代理类)
 */
class Agent{
    private Actor actor;

    public Agent(Actor actor){
        this.actor = actor;
    }

    public void action(){
        //增强
        this.before1();
        this.before2();

        //主干逻辑
        actor.action();

        //增强
        this.after();
    }

    private void before1(){
        System.out.println("开始聊剧本");
    }

    private void before2(){
        System.out.println("开始谈酬金");
    }

    private void after(){
        System.out.println("开始收钱");
    }
}
/**
 *场景
 */
public static void main(String[] args) {
        Agent agent =  new Agent(new Actor("彭yu晏"));
        agent.action();

    }

执行结果
在这里插入图片描述

上述代码中经纪人通过持有演员来实现对演员的代理和增强。演员的演戏是主要逻辑,其他是次要逻辑。
同时,经纪人与演员高度绑定,经纪人的方法与演员的方法之间的关系是直接写死了。演员有多少方法就需要经纪人显式地写出多少对应的方法,即两者关系在编写代码的时候就已经确定了。这就是静态代理。
当然,静态代理的实现不只有组合这一种方式,还可以通过实现同一个接口或者继承目标类重写被代理对象的方法来实现静态代理。

静态代理总结:

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。

那么静态代理和动态代理的区别是什么?

前者需要在一开始就要确定代理类需要代理的对象,然后根据代理对象去编写代理类,可以这么认为,==静态代理就是要自己编写代理类==。而动态代理是在是现阶段不用关系代理谁,而在运行阶段才确定代理哪个对象,换句话说,就是==动态代理不需要我们去写代理类,而是确定好增强逻辑后由程序根据增强逻辑为我们实现代理类==。而Java中常用的动态代理是JDK代理和CGLIB代理。



JDK动态代理

jdk动态代理是jre提供给我们的类库,可以直接使用,不依赖第三方。其原理是通过实现与被代理类相同的接口,再将被代理类组合,来实现对被代理类的增强。

这与我们前面写的例子所实现代理类的方式不一样。因此这里先提供一张简化后的类图来描述代理类与被代理类之间的关系来方便后续的理解。其本质上与前面的例子是一致的,区别就是是否实现同一个接口。
在这里插入图片描述
理解了类图中代理类与被代理类之间的关系后我们就可以深入去理解JDK动态代理的内容了

下面给出与类图关系一致的JDK动态代理实现代码
先提供一个演员的接口

/**
 * 接口
 */
interface Actor{
    public void action();
}

再提供演员的实现类即被代理类

/**
 * 演员(被代理类)
 */
class ActorPYY implements Actor{
    private String name = "彭yu晏";

    @Override
    public void action(){
        System.out.println(this.name + "开始拍戏");
    }
}

明星演出前需要有人收钱,由于要准备演出,自己不做这个工作,一般交给一个经纪人。但由于==动态代理不直接提供代理类(经纪人),而是给出增强逻辑,由程序在运行时给出代理类并实现==。因此这里只提供增强逻辑

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

class ActorAgent implements InvocationHandler{
    /**
     *被代理对象
     */
    Object target;

    public ActorAgent(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //增强
        before1();
        before2();

        //调用主干逻辑
        Object result = method.invoke(this.target,args);

        //增强
        after();
        return null;
    }

    /**
     * 生成代理类
     * @return
     */
    public Object createProxy(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }

    private void before1(){
        System.out.println("开始聊剧本");
    }

    private void before2(){
        System.out.println("开始谈酬金");
    }

    private void after(){
        System.out.println("开始收钱");
    }
}

上述例子中,==方法creatProxy返回的对象才是我们的代理类==,它需要三个参数,前两个参数的意思是在==同一个类加载器下==通过实现与被代理类相同的接口创建出一个对象,该对象需要一个属性,也就是第三个参数,也就是我们在动态代理类中实现的==InvocationHandler==接口,这个通过重写这个接口的invoke方法来实现代理增强。需要注意的是这个CreatProxy方法不一定非得在我们的ActorAgent 类中,往往放在一个工厂类中,这里只是为了方便所以放在这里。

场景类

public static void main(String[] args) {
        Actor proxy = (Actor) new ActorAgent(new ActorPYY()).createProxy();
        proxy.action();
        
        //打印代理类的类名
        System.out.println(proxy.getClass().getName());;
    }

执行结果
在这里插入图片描述

==Proxy==(jdk类库提供) 根据B的接口生成一个实现类,我们称为C,它就是动态代理类(该类型的类名为 ==$Proxy+数字== 的新类(参考场景类最后一行代码的执行结果))。

==JDK动态代理生成新代理类的过程==:
由于拿到了被代理类所实现的所有接口,也就能声明一个新的类型去实现该接口的所有方法,但这些方法显然都是“虚”的,它需要调用其他对象的方法。当然这个被调用的对象不能是对象B,如果直接调用对象B,那就没法增强了,等于饶了一圈又回来了。
所以它调用的是B的包装类,即给出了增强逻辑的InvocationHandler实现类,这个类不光包含被代理对象,还包含增加逻辑。上述例子中就是ActorAgent类, 这个接口里面有个方法,它是被代理对象的所有方法的调用入口(invoke),调用之前我们可以添加加自己的代码增强。
看下我们的实现,我们在InvocationHandler里调用了对象B(target)的方法,调用之前增强了B的方法。

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //增强
        before1();
        before2();

        //调用主干逻辑
        Object result = method.invoke(this.target,args);

        //增强
        after();
        return null;
    }

所以可以这么认为C代理了InvocationHandler,InvocationHandler代理了我们的类B,两级代理。

整个JDK动态代理的秘密也就这些,简单一句话,动态代理就是要在程序运行中动态生成被代理对象的代理类,由于代理类是动态生成的,所以叫动态代理。而代理类的生成逻辑就是实现与被代理类相同的接口,并通过InvocationHandler来请求对应的方法,该InvocationHandler包含被代理对象,并负责分发请求给被代理对象,分发前后均可以做增强。

下面看下动态代理类到底如何调用的InvocationHandler的,为什么InvocationHandler的一个invoke方法能将请求分发到target的所有方法。C中的部分代码示例如下,通过反编译生成后的代码查看。Proxy创造的C是自己(Proxy)的子类,且实现了B的接口,一般都是这么修饰的:

public final class XXX extends Proxy implements XXX

其中一个方法代码如下:

  public final void action() {
    try {
      this.h.invoke(this, m3, null);
      return;
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }

可以看到,C中的方法全部通过调用h实现,其中h就是InvocationHandler,是我们在生成C时传递的第三个参数。这里还有个关键就是action方法(业务方法)跟调用invoke方法时传递的参数m3一定要是一一对应的,但是这些对我们来说都是透明的,由Proxy在newProxyInstance时保证的。留心看到C在invoke时把自己this传递了过去,InvocationHandler的invoke的第一个方法参数也就是我们的动态代理实例类,业务上有需要就可以使用它。(所以千万不要在invoke方法里把请求分发给第一个参数,否则很明显就死循环了)

C类(代理类)中有B(被代理类)中所有方法的成员变量

  private static Method m1;
  private static Method m3;
  private static Method m2;
  private static Method m0;

这些变量在static静态代码块初始化,这些变量是在调用invocationhander时必要的入参,也让我们依稀看到Proxy在生成C时留下的痕迹。

static {
    try {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      m3 = Class.forName("proxyModle.dynamicProxy.Actor").getMethod("action", new Class[0]);
      return;
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new NoSuchMethodError(noSuchMethodException.getMessage());
    } catch (ClassNotFoundException classNotFoundException) {
      throw new NoClassDefFoundError(classNotFoundException.getMessage());
    } 
  }

从以上分析来看,要想彻底理解一个东西,再多的理论不如看源码,底层的原理非常重要。

jdk动态代理类图如下
在这里插入图片描述

JDK动态代理总结:

虽然相对于静态代理,JDK动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫Proxy。Java的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在Java中本质上就行不通。有很多条理由,人们可以否定对 class代理的必要性,但是同样有一些理由,相信支持class动态代理会更美好。接口和类的划分,本就不是很明显,只是到了Java中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。但是,不完美并不等于不伟大,伟大是一种本质,Java动态代理就是佐例。



CGLIB动态代理

JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。

先看CGLIB实现动态代理的代码

class ActorAgent2 implements MethodInterceptor {
    Object target;

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //增强
        before1();
        before2();

        //调用主干逻辑
        Object result = methodProxy.invokeSuper(o,objects);

        //增强
        after();
        return result;
    }

    /**
     * 生成代理类
     * @return
     */
    public Object createProxy(final Object object){
        this.target = object;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    private void before1(){
        System.out.println("开始聊剧本");
    }

    private void before2(){
        System.out.println("开始谈酬金");
    }

    private void after(){
        System.out.println("开始收钱");
    }
}

从代码可以看出,它和jdk动态代理有所不同,它只需要一个类型clazz就可以产生一个代理对象, 所以说是“类的代理”,且创造的对象通过打印类型发现也是一个新的类型。不同于jdk动态代理,jdk动态代理要求对象必须实现接口,而cglib对此没有要求。
==cglib动态代理生成新代理类的过程==:
它生成一个继承被代理类的类型C(代理类),这个代理类持有一个MethodInterceptor,我们setCallback时传入的。 C重写所有被代理类中的方法(方法名一致),然后在C中,构建名叫“CGLIB”+“$父类方法名$”的方法(下面叫cglib方法,所有非private的方法都会被构建),方法体里只有一句话super.方法名(),可以简单的认为保持了对父类方法的一个引用,方便调用。

这样的话,C中就有了重写方法、cglib方法、父类方法(不可见),还有一个统一的拦截方法(增强方法intercept)。其中重写方法和cglib方法肯定是有映射关系的。

C的重写方法是外界调用的入口(LSP原则),它调用MethodInterceptor的intercept方法,调用时会传递四个参数,第一个参数传递的是this,代表代理类本身,第二个参数标示拦截的方法,第三个参数是入参,第四个参数是cglib方法,intercept方法完成增强后,我们调用cglib方法间接调用父类方法完成整个方法链的调用。

==这里有个疑问就是intercept的四个参数,为什么我们使用的是methodProxy而不是method?==

    @Override
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
    {
        System.out.println("收钱");
        
        return arg3.invokeSuper(arg0, arg2);
    }

因为如果我们通过反射 arg1.invoke(arg0, ...)这种方式是无法调用到父类的方法的,子类有方法重写,隐藏了父类的方法,父类的方法已经不可见,如果硬调arg1.invoke(arg0, ...)很明显会死循环。

所以调用的是cglib开头的方法,但是,我们使用arg3也不是简单的invoke,而是用的invokeSuper方法,这是因为cglib采用了fastclass机制,不仅巧妙的避开了调不到父类方法的问题,还加速了方法的调用。

fastclass基本原理是,给每个方法编号,通过编号找到方法执行避免了通过反射调用。

对比JDK动态代理,cglib依然需要一个第三者分发请求,只不过jdk动态代理分发给了目标对象,cglib最终分发给了自己,通过给method编号完成调用。cglib是继承的极致发挥,本身还是很简单的,只是fastclass需要另行理解。

CGLIB代理总结:

测试代码

public static void main(String[] args) {
        int times = 10000;

        ActorPYY pyy = new ActorPYY();
        ActorAgent proxyJDK = new ActorAgent(pyy);
        ActorAgent2 proxyCGLIB = new ActorAgent2();

        long time1 = System.currentTimeMillis();
        Actor actor1 = (Actor) proxyJDK.createProxy();
        long time2 = System.currentTimeMillis();
        System.out.println("jdk创建时间:" + (time2 - time1));

        long time5 = System.currentTimeMillis();
        Actor actor2 = (Actor) proxyCGLIB.createProxy(pyy);
        long time6 = System.currentTimeMillis();
        System.out.println("cglib创建时间:" + (time6 - time5));

        long time3 = System.currentTimeMillis();
        for (int i = 1; i <= times; i++)
        {
            actor1.action();
        }
        long time4 = System.currentTimeMillis();
        System.out.println("jdk执行时间" + (time4 - time3));

        long time7 = System.currentTimeMillis();
        for (int i = 1; i <= times; i++)
        {
            actor2.action();
        }

        long time8 = System.currentTimeMillis();

        System.out.println("cglib执行时间" + (time8 - time7));
    }

经测试,jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。cglib执行速度略大于jdk,所以比较适合单例模式。另外由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果CGLIB动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。

相关文章
|
6天前
|
Java API 开发者
Jdk动态代理为啥不能代理Class?
该文章主要介绍了JDK动态代理的原理以及为何JDK动态代理不能代理Class。
Jdk动态代理为啥不能代理Class?
|
3天前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
4天前
|
Java API Apache
JDK8到JDK24版本升级的新特性问题之在Java中,HttpURLConnection有什么局限性,如何解决
JDK8到JDK24版本升级的新特性问题之在Java中,HttpURLConnection有什么局限性,如何解决
|
4天前
|
Oracle 安全 Java
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
|
4天前
|
Java API 开发者
JDK8到JDK17版本升级的新特性问题之SpringBoot选择JDK17作为最小支持的Java lts版本意味着什么
JDK8到JDK17版本升级的新特性问题之SpringBoot选择JDK17作为最小支持的Java lts版本意味着什么
JDK8到JDK17版本升级的新特性问题之SpringBoot选择JDK17作为最小支持的Java lts版本意味着什么
WXM
|
24天前
|
Oracle Java 关系型数据库
Java JDK下载安装及环境配置超详细图文教程
Java JDK下载安装及环境配置超详细图文教程
WXM
125 3
|
4天前
|
Java 编译器 开发者
JDK8到JDK23版本升级的新特性问题之编写一个简单的module-info.java文件,如何实现
JDK8到JDK23版本升级的新特性问题之编写一个简单的module-info.java文件,如何实现
|
4天前
|
Oracle Java 关系型数据库
简单记录在Linux上安装JDK环境的步骤,以及解决运行Java程序时出现Error Could not find or load main class XXX问题
本文记录了在Linux系统上安装JDK环境的步骤,并提供了解决运行Java程序时出现的"Error Could not find or load main class XXX"问题的方案,主要是通过重新配置和刷新JDK环境变量来解决。
13 0
|
1月前
|
存储 Ubuntu Java
【Linux】已解决:Ubuntu虚拟机安装Java/JDK
【Linux】已解决:Ubuntu虚拟机安装Java/JDK
45 1
|
25天前
|
Java 数据库 Spring
Java编程问题之在测试中使用CGLIB创建代理类如何解决
Java编程问题之在测试中使用CGLIB创建代理类如何解决