写点什么

Java 基础 | Proxy 动态代理机制详解

作者:知了一笑
  • 2021 年 11 月 09 日
  • 本文字数:4905 字

    阅读完需:约 16 分钟

Java基础 | Proxy动态代理机制详解

一、Jvm 加载对象

在说 Java 动态代理之前,还是要说一下 Jvm 加载对象的过程,这个依旧是理解动态代理的基础性原理:



Java 类即源代码程序.java类型文件,经过编译器编译之后就被转换成字节代码.class类型文件,类加载器负责读取字节代码,并转换成 java.lang.Class 对象,描述类在元数据空间的数据结构,类被实例化时,堆中存储实例化的对象信息,并且通过对象类型数据的指针找到类。


过程描述:源码->.java 文件->.class 文件->Class 对象->实例对象


所以通过 New 创建对象,独断其背后很多实现细节,理解上述过程之后,再了解一个常用的设计模式,即代理模式。

二、代理模式

1、基本描述

代理模式给某一个(目标)对象提供一个代理对象,并由代理对象持有目标对象的引用。所谓代理,就是一个对象代表另一个对象执行相应的动作程序。而代理对象可以在客户端和目标对象之间起到中介的作用。



代理模式在实际的生活中场景很多,例如中介、律师、代购等行业,都是简单的代理逻辑,在这个模式下存在两个关键角色:


目标对象角色:即代理对象所代表的对象。


代理对象角色:内部含有目标对象的引用,可以操作目标对象;AOP 编程就是基于这个思想。

2、静动态模式

  • 静态代理:在程序运行之前确定代理角色,并且明确代理类和目标类的关系。

  • 动态代理:基于 Java 反射机制,在 JVM 运行时动态创建和生成代理对象。

三、静态代理

基于上述静态代理的概念,用一段代码进行描述实现,基本逻辑如下:


  • 明确目标对象即被代理的对象;

  • 定义代理对象,通过构造器持有目标对象;

  • 代理对象中定义前后置增强方法;


目标对象与前后置增强代码就组成了代理对象,这样就不用直接访问目标对象,像极了电视剧中那句话:我是律师,我的当事人不方便和你对话。


public class Proxy01 {    public static void main(String[] args) {        TargetObj targetObj = new TargetObj() ;        ProxyObj proxyObj = new ProxyObj(targetObj) ;        proxyObj.invoke();    }}class TargetObj {    public void execute (){        System.out.println("目标类方法执行...");    }}class ProxyObj {    private TargetObj targetObj ;    /**     * 持有目标对象     */    public ProxyObj (TargetObj targetObj){        this.targetObj = targetObj ;    }    /**     * 目标对象方法调用     */    public void invoke (){        before () ;        targetObj.execute();        after () ;    }    /**     * 前后置处理     */    public void before (){        System.out.println("代理对象前置处理...");    }    public void after (){        System.out.println("代理对象后置处理...");    }}
复制代码


静态代理明确定义了代理对象,即有一个代理对象的.java文件加载到 JVM 的过程,很显然的一个问题,在实际的开发过程中,不可能为每个目标对象都定义一个代理类,同样也不能让一个代理对象去代理多个目标对象,这两种方式的维护成本都极高。


代理模式的本质是在目标对象的方法前后置入增强操作,但是又不想修改目标类,通过前面反射机制可以知道,在运行的时候可以获取对象的结构信息,基于 Class 信息去动态创建代理对象,这就是动态代理机制。


顺便说一句:技术的底层实现逻辑不好理解是众所周知,然而基础知识点并不复杂,例如代理模式的基本原理,但是结合到实际的复杂应用中(AOP 模式),很难活灵活现的理解到是基于反射和动态代理的方式实现的。

四、动态代理

1、场景描述

基于一个场景来描述动态代理和静态代理的区别,即最近几年很火的概念,海外代购:



在代购刚兴起的初期,是一些常去海外出差的人,会接代购需求,即代理人固定;后来就兴起海外代购平台,海淘等一系列产品,即用户代购需求(目标对象)由代购平台去实现,但是具体谁来操作这个就看即时分配,这个场景与动态代理的原理类似。

2、基础 API 案例

首先看两个核心类,这里简述下概念,看完基本过程再细聊:


  • Proxy-创建代理对象,核心参数:

  • ClassLoader:(目标类)加载器;

  • Interfaces:(目标类)接口数组;

  • InvocationHandler:代理调用机制;

  • InvocationHandler-代理类调用机制:

  • invoke:这个上篇说的反射原理;

  • method:反射类库中的核心 API;


目标对象和接口


interface IUser {    Integer update (String name) ;}class UserService implements IUser {    @Override    public Integer update(String name) {        Integer userId = 99 ;        System.out.println("UserId="+userId+";updateName="+name);        return userId ;    }}
复制代码


代理对象执行机制


class UserHandler implements InvocationHandler {    private Object target ;    public UserHandler (Object target){        this.target = target ;    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        System.out.println("before()...");        Object result = method.invoke(target, args);        System.out.println("after()...");        return result;    }}
复制代码


具体组合方式


public class Proxy02 {    public static void main(String[] args) {        /*         * 生成$Proxy0的class文件         */        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");        /*         * 目标对象信息         */        IUser userService = new UserService();        ClassLoader classLoader = userService.getClass().getClassLoader();        Class<?>[] interfaces = UserService.class.getInterfaces() ;        /*         * 创建代理对象         */        InvocationHandler userHandler = new UserHandler(userService);        /*         * 代理类对象名         * proxyClassName=com.java.proxy.$Proxy0         */        String proxyClassName = Proxy.newProxyInstance(classLoader,interfaces,userHandler).getClass().getName();        System.out.println("proxyClassName="+proxyClassName);        /*         * 具体业务实现模拟         */        IUser proxyUser1 = (IUser) Proxy.newProxyInstance(classLoader,interfaces,userHandler);        IUser proxyUser2 = (IUser) Proxy.newProxyInstance(classLoader,interfaces,userHandler);        proxyUser1.update("cicada") ;        proxyUser2.update("smile") ;    }}
复制代码


这里之所以要生成代理类的结构信息,因为从 JVM 加载的过程看不到相关内容,关键信息再次被独断:


javap -v Proxy02.class
复制代码



查看代理类名称


/* * proxyClassName=com.java.proxy.$Proxy0 */String proxyClassName = Proxy.newProxyInstance(classLoader,interfaces,userHandler).getClass().getName();System.out.println("proxyClassName="+proxyClassName);
复制代码


下意识输出代理对象名称,这里即对应 JVM 机制,找到 Class 对象名,然后分析结构,这样就明白动态代理具体的执行原理了。


生成代理类.class 文件


System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
复制代码


通过上面 JVM 加载对象的机制可知,描述代理类的 Class 对象一定存在,只是在运行时并没有生成显式的.class文件,通过上面生成代理类.class的语法,会在项目目录的/com/java/proxy路径下创建文件。


顺便说一句:作为一只程序员,复杂总是和我们环环相绕,说好的简单点呢?

3、代理类结构

继承与实现


class $Proxy0 extends Proxy implements IUser {}
复制代码


从代理类的功能来思考,可以想到需要继承 Proxy 与实现 IUser 接口,还有就是持有调用机制的具体实现类,用来做业务增强。


构造方法


public $Proxy0(InvocationHandler var1) throws  {    super(var1);}
复制代码


通过构造方法,持有 UserHandler 具体的执行机制对象。


接口实现


final class $Proxy0 extends Proxy implements IUser {    private static Method m3;    public final Integer update(String var1) throws  {        try {            return (Integer)super.h.invoke(this, m3, new Object[]{var1});        } catch (RuntimeException | Error var3) {            throw var3;        } catch (Throwable var4) {            throw new UndeclaredThrowableException(var4);        }    }}
复制代码


目标类的基本需求update()方法,通过代理类进行承接,并基于 UserHandler 实现具体的增强业务处理。


基础方法


final class $Proxy0 extends Proxy implements IUser {    private static Method m0;    private static Method m1;    private static Method m2;    public $Proxy0(InvocationHandler var1) throws  {        super(var1);    }    static {        try {            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));            m2 = Class.forName("java.lang.Object").getMethod("toString");            m3 = Class.forName("com.java.proxy.IUser").getMethod("update", Class.forName("java.lang.String"));            m0 = Class.forName("java.lang.Object").getMethod("hashCode");        } catch (NoSuchMethodException var2) {            throw new NoSuchMethodError(var2.getMessage());        } catch (ClassNotFoundException var3) {            throw new NoClassDefFoundError(var3.getMessage());        }    }    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);        }    }    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);        }    }}
复制代码


基于 Object 类,定义 Java 中几个常用方法 equals()判断,toString()方法,hashCode()值,这个在分析 Map 源码的时候有说过为什么这几个方法通常都是一起出现。

4、JDK 源码

上面是案例执行的过程和原理,还有一个关键点要明白,即 JDK 源码的逻辑:


IUser proxyUser = (IUser) Proxy.newProxyInstance(classLoader,interfaces,userHandler);
复制代码


Proxy 提供的静态方法newProxyInstance(),通过各个参数的传入,构建一个新的代理 Class 对象,即 $Proxy0 类的结构信息,这里再回首看下三个核心参数:


  • ClassLoader:基于 JVM 运行过程,所以需要获取目标类 UserService 的类加载器;

  • Interfaces:目标类 UserService 实现的接口,从面向对象来考虑,接口与实现分离,代理类通过实现 IUser 接口,模拟目标类的需求;

  • InvocationHandler:代理类提供的功能封装即 UserHandler,可以在目标方法调用前后做增强处理;


最后总结一下动态代理的实现的核心技术点:Jvm 加载原理、反射机制、面向对象思想;每次阅读 JDK 的源码都会惊叹设计者的鬼斧神工,滴水穿石坚持才会有收获。


JVM类加载机制 | 代理模式 | AOP切面编程 | 自定义日志记录 | Map源码分析

五、源代码地址

GitHub·地址https://github.com/cicadasmile/java-base-parentGitEE·地址https://gitee.com/cicadasmile/java-base-parent
复制代码


发布于: 2021 年 11 月 09 日阅读数: 6
用户头像

知了一笑

关注

公众号:知了一笑 2020.04.08 加入

源码仓库:https://gitee.com/cicadasmile

评论

发布
暂无评论
Java基础 | Proxy动态代理机制详解