当前位置:首页 > 技术 > 后端 > 正文内容

用大白话讲Java动态代理的原理

anan3个月前 (07-29)后端86

动态代理是什么

首先说下代理模式,代理模式是常见的一种java设计模式,特征是代理类委托类实现了同样的接口,代理类主要负责为委托类预处理、过滤、转发,以及事后处理等。代理类与委托类之间通常会存在关联关系,一个代理类的实例与它的委托类的实例是关联的。代理类的实例本身是并不真正关心被调用方法的内部逻辑,而是会通过内部访问调用 委托类的实例真正实现了的方法,来为调用者提供服务。

有代理的话,在访问实际对象时,是通过代理实例来访问、调用委托类方法的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。

动态代理对比静态代理,最大的特点是代理类是在程序运行时生成的,并非在编译期生成,能做的事情也多了,自然风险也高了。

动态代理最简单的用法

用一个比较接近生活的例子:中午,饿了的室友 委托 持家有道的你 去点外卖

Hungry.java :接口

public interface Hungry {
     void callLunch();
}

Roommate.java :Hungry接口的实现类,也就是委托类

public class Roommate implements Hungry{
    private String name;
    public Roommate(String name) {
        this.name = name;
    }

    @Override
    public void callLunch() {
        System.out.println("好饿,今天午饭点外卖吧");
    }
}

public class RoommateInvocationHandler<T> implements InvocationHandler {

    private T rommate;

    public RoommateInvocationHandler(T roommate){
        this.rommate = roommate;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("下单前,我先帮你看下有没有平台优惠券吧");
        Object result = method.invoke(rommate , args);
        return result;
    }
}

InvocationHandler是一个接口,由代理实例内部的invocation handler实现的接口。每个代理实例都有一个关联的invocation handler。当代理实例上调用方法时,method.invoke(baseImpl, args),此方法将被编码并织入到代理实例内部的 invocation handler实现的invoke方法中。

利用 Proxy 的方式实现动态代理,调用 委托类接口 的方法,完成午餐点外卖这个操作

public static void main(String[] args) {
	    Roommate roommate = new Roommate("zhangsan");
        Hungry proxyInstance = (Hungry) Proxy.newProxyInstance(
                roommate.getClass().getClassLoader(),
                roommate.getClass().getInterfaces(),
                new RoommateInvocationHandler<Roommate>(roommate)
        );
        proxyInstance.callLunch();
}
//输出结果
下单前,我先帮你看下有没有平台优惠券吧
好饿,今天午饭点外卖吧

代理实例proxyInstance的类型是Hungry,所以只能调用Hungry里规定的方法。Roommate作为接口实现类,不是来自接口的其他的方法,是无法通过动态代理调用的。

可以看到代理实例在调用委托类实现的方法时,可以很方便地在调用方法的前后执行一些操作,在示例代码中则是在调用方法前简单输出了一行: System.out.println("下单前,我先帮你看下有没有平台优惠券吧"),还可以有其他用途,例如记录这个方法的耗时时间,对方法的参数或者返回结果进行修改等等。这也是Spring,Dagger进行AOP编程的原理。

那为什么继承InvocationHandler接口和持有委托类引用的RoommateInvocationHandler调用来自Hungry接口的callLunch()方法时可以调用到委托类对callLunch()的逻辑实现呢,看看它的背后原理:

动态代理的实现原理

Proxy.newProxyInstance() 入手,逐步分析 InvocationHandler 如何建立代理实例和委托实例的关联:

public static Object newProxyInstance(ClassLoader loader ,  Class<?>[] interfaces,
                               InvocationHandler h) throws IllegalArgumentException {
    	//InvocationHandler必须非空,说明是个重要角色
        Objects.requireNonNull(h);
		//获取委托类的接口
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         * 核心:通过类加载器和委托类接口,在内存中查找出或者生成指定的代理类
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         * 利用指定的invocation handler调用它的构造器方法,构建代理类的实例返回
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

来到这一步好像就停下了,那么接下来探究 cl 这个实例创建过程发生了什么:

在上面示例代码main函数的后面接着补充。利用ProxyGenerator.generateProxyClass生成这个动态生成的类文件,写入了指定路径的class文件内

$Proxy0 是 代理类在系统内部的编号,在示例代码只生成了一个代理类所以编号是 $Proxy0

        byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",Roommate.class.getInterfaces());
        String filePath = "C:\\Users\\ODM\\Desktop\\RoommateProxy.class";
        try(FileOutputStream fos = new FileOutputStream(filePath)) {
            fos.write(classFile);
            fos.flush();
        }catch (IOException e){
            e.printStackTrace();
            System.out.println("error:写入文件");
        }

使用反编译工具,我这里用的是jd-gui反编译,这个$Proxy0类,实现了Proxy类,继承了和委托类相同的接口

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy_test.Hungry;

public final class $Proxy0 extends Proxy implements Hungry{
  private static Method m1;
  private static Method m3; //由下方静态代码块得知,m3代表callLunch()这一个方法
  private static Method m2;
  private static Method m0;
  
  /*
  * 父类Proxy的构造器,其中 h 属性为 InvocationHandler引用
  * protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
  */
  public $Proxy0(InvocationHandler paramInvocationHandler) throws {
    super(paramInvocationHandler);
  }
 //关键!可供外界调用,方法名与委托类实现接口的方法相同,利用 InvocationHandler调用invoke
  public final void callLunch() throws {
    try{
      this.h.invoke(this, m3, null);
      return;
    }
    catch (Error|RuntimeException localError){
      throw localError;
    }
    catch (Throwable localThrowable){
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
    
  public final boolean equals(Object paramObject) throws {}
  public final String toString() throws {...}
  public final int hashCode() throws {...}
  
  static{
    try{
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("proxy_test.Hungry").getMethod("callLunch", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException){
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException){
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
}

事情逐渐明朗起来,从这个动态类的源码,可以分析出: $Proxy0 ,在构建这个类时,会调用了父类Proxy的构造方法,将InvocationHandler引用传递给了父类Proxy的 h 属性,于是当我们在外界使用 代理实例 调用了 callLunch() 这个方法时,就会来到这一句 this.h.invoke(this, m3, null); 由于h属性其实是InvocationHandler引用,调用了它的invoke,也就导致了上面示例代码中的RoommateInvocationHandler类的重写过的invoke方法也就被调用了,RoommateInvocationHandler也持有委托类的引用,所以委托类的方法也被调用起来了。

Java的继承机制是单继承,多接口。代理类因为必须要继承Proxy类,所以java的动态代理只能对接口进行代理,无法对一个class类进行动态代理。

动态代理原理总结

用大白话的方式讲:

有一个类InvocationHandler,它的性质类似一个中介,中介类构建时持有了委托对象,所以可以在它的invoke方法中调用了委托对象实现接口的具体方法。当外部调用这个InvocationHandler的invoke方法时,对 invoke 的调用最终都转为对委托对象的方法调用。

创建明面上负责代理的代理实例时,在内存中动态生成的类不但继承了Proxy,也实现了与委托对象相同的接口,因此代理实例可以调用此接口的方法,然后通过持有的中介类对象来调用中介类对象的invoke方法,最终达到代理实例执行了委托者的方法。

打赏
版权声明:所有来源为第三方内容,若本站收录的文章无意侵犯了贵司版权,请给下面邮箱地址来信,我们会及时处理和回复,谢谢。

管理员邮箱:42004990@qq.com

微信公众号

分享给朋友:

相关文章

31道Java核心面试题,一次性打包送给你

31道Java核心面试题,一次性打包送给你

先看再点赞,给自己一点思考的时间,微信搜索【沉默王二】关注这个靠才华苟且的程序员。本文 GitHub github.com/itwanger 已收录,里面还有一线大厂整理的面试题,以及我的系列文章。二...

Java 分布式任务调度平台:PowerJob 快速开始+配置详解

Java 分布式任务调度平台:PowerJob 快速开始+配置详解

本文适合有 Java 基础知识的人群作者:HelloGitHub-Salieri引言HelloGitHub 推出的《讲解开源项目》系列。项目地址:https://github.com/KFCFans/...

OAuth2.0分布式系统环境搭建

OAuth2.0分布式系统环境搭建

好好学习,天天向上本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航介绍OAuth(开放授权)是一个开放...

深入理解Java虚拟机--个人总结(持续更新)

深入理解Java虚拟机--个人总结(持续更新)

深入理解Java虚拟机--个人总结(持续更新)每天按照书本学一点,会把自己的总结思考写下来,形成输出,持续更新,立帖为证-- 2020年7月7日 开始第一次学习 -- ...

OAuth 2.0详解

OAuth 2.0详解

OAuth 2.0详解概念:OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如基本消息,照片,联系人列表),而无需将 用户名 和 密码 提供给第三方应...

Java面试必问:ThreadLocal终极篇 淦!

Java面试必问:ThreadLocal终极篇 淦!

点赞再看,养成习惯,微信搜一搜【敖丙】关注这个互联网苟且偷生的程序员。本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文...