JDK 动态代理从入门到掌握

快速入门

本文介绍 JDK 实现的动态代理及其原理,通过 ProxyGenerator 生成动态代理类的字节码文件。

环境要求

要求原因
JDK 8 及以下在 JDK 9 之后无法使用直接调用 ProxyGenerator 中的方法,不便于将动态代理类对应的字节码文件输出
lombok为了使用 @SneakyThrows,避免异常处理代码对主体逻辑的干扰

基本概念

术语描述
目标类可以是任意一个现有的类
代理类对目标类进行功能的扩展,最简单的方式就是继承目标类,然后重写目标类的方法
动态代理类动态代理是实现代理的一种方式,不需要手动去继承目标类,不会写死,通用灵活
原始方法目标类中的方法
增强方法代理类中的方法,增强方法的逻辑处理中包含原始方法的处理逻辑,并且含有额外的扩展逻辑

案例准备

目标类(被代理类)和接口

JDKProxy 这种代理方式必须提供接口,从 Proxy.newProxyInstance() 方法要求的参数也可以看出来,实际上需要的是接口。这是 JDKProxy 的要求,不是动态代理的要求。

public interface IService {
    void show(String msg);
}

目标类是需要被增强的类,使用代理的目的是为了在不修改代码的情况下对目标类原有的功能进行增强扩展,使用动态代理的目的是为了减少手动创建的代理类

public class BaseServiceImpl implements IService {

    @Override
    public void show(String msg) {
        System.out.println(msg);
    }
}

InvocationHandler(增强方法、核心)

通用的增强方法需要提供目标类对象,在 InvocationHandler 对象的 invoke 方法中调用目标类对象 target 的原始方法,是一种委托模式。

public class MyInvocationHandler implements InvocationHandler {
    private final Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("===========method before==========");
		
        // 固定模板,调用原始方法,前后输出代表自定义的增强逻辑
        Object result = method.invoke(target, args);
        
        System.out.println("===========method after===========");
        return result;
    }
}

动态代理案例演示

理解 JDK 动态代理需要回答下面三个问题:

  • 为什么 MyInvocationHandler 类的设计中添加一个成员变量 target,并且在构造方法中强制要求使用者传入这个对象?
  • 通过 Proxy.newProxyInstance() 这个方法便得到了代理类对象,那一个 object 就有一个 Object 类,那么这个对象对应的类是怎么样的?
  • 代理类对象调用增强方法的执行逻辑是怎样的,它是如何和原始方法产生关联的?
public class Demo{
    public static void main(String[] args) {
        // 1. 创建InvocationHandler对象
        IService baseService = new BaseServiceImpl();
        InvocationHandler invocationHandler = new MyInvocationHandler(baseService);

        // 2. 通过JDKProxy生成代理类对象
        IService serviceProxy = (IService) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), 
                                                                  new Class[]{IService.class}, 
                                                                  invocationHandler);
        
        // 3. 调用代理类对象中的增强方法
        serviceProxy.show("Hello World");
        serviceProxy.toString(); //这是默认被增强的三个方法之一
    }
}
  • 首先,我们要理解 invoke() 的设计思路是什么,它对应的是一个通用的、增强后的方法

    既然是增强方法,那么就需要调用原始方法,因此需要一个目标类对象,所以在 MyInvocationHandler 这个类中有一个 target 用来接收该对象,并且是通过构造方法强制要求使用者来提供。这就是为什么要求在 InvocationHandler 中提供一个目标类对象。(回答第一个问题)

  • 其次,代理类对象对应的这个动态代理类,在 Proxy.newProxyInstance() 的底层逻辑中是通过 ProxyGenerator 来生成的。后面将使用 ProxyGenerator模拟该过程,并额外将动态代理类对应的字节码输出到文件中进行查看,在 JDK 9 之后我们不能够直接调用这个类,因此推荐使用 JDK 8。(未完全回答)

  • 最后,这个问题在回答上面两个问题之后通过流程图解释。(未回答)

JDKProxy 原理

ProxyGenerator 使用

在回答第二个问题之前,先了解 ProxyGenerator 如何使用。输出的字节码文件是 classpath 下的 ServiceJDKProxy.class。

public class ProxyGeneratorDemo{
    @SneakyThrows
    public static void main(){
        // 参数配置:生成的代理类的名称
        String classpath = ClassLoader.getSystemResource("").getFile().substring(1);
        String proxyClassName = "ServiceJDKProxy";
        File classFile = Paths.get(classpath, proxyClassName + ".class").toFile();
        
        // 1. 主体逻辑就一行代码,对目标类的所有接口进行代理,这里决定了JDKProxy是对接口的代理
        byte[] bytes = ProxyGenerator.generateProxyClass(proxyClassName, BaseServiceImpl.class.getInterfaces());

        // 2. 输出到指定文件中
        FileOutputStream fos = new FileOutputStream(classFile);
        fos.write(bytes);

        // 关闭流
        fos.flush();
        fos.close();
    }
}

跟踪 generateProxyClass 方法可以进入到 generateClassFile 方法中,看到下面的这一段逻辑,所以 JDK 动态代理会通过反射的方式,来获取传递的接口数组中的所有方法,并对这些方法进行增强。这能够回答两个问题:(a)JDK 动态代理模式中对哪些方法进行增强(接口的所有方法),(b)代理类对象是如何获取到目标类的方法对象的(反射遍历)

public class ProxyGenerator {
	private byte[] generateClassFile() {
        // 省略...
        
		for (Class<?> intf : interfaces) {
            for (Method m : intf.getMethods()) {
                // 从设计角度来看,如果需要过滤一些方法对象,按照责任链模式设计,需要额外保存一个列表,里面是一条一条的过滤规则
                addProxyMethod(m, intf);
            }
        }
        
        // 省略...
    }
}

动态代理类字节码文件

字节码反编译之后对应的 Java 源代码如下(经过适当调整),这回答了第二个问题。至于这个字节码是如何生成的,具体可以看源码的操作流程,本质上是按照JVM 字节码规范在对应的位置上填充数据,由于方法对象通过遍历已经获取到了,因此可以填充。

public final class ServiceJDKProxy 
    // 父类是Proxy
    extends Proxy
    // 实现要求代理的所有接口
    implements IService {
    
    private static Method m0;
    private static Method m1;
    private static Method m2;
    private static Method m3;
    static {
        // 除了接口中的方法,默认会获取Object类中的三个方法:hashCode、equals、toString,因此调用代理对象的toString方法也会被增强
        m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
        m2 = Class.forName("java.lang.Object").getMethod("toString");
        
        // 调用Proxy.newProxyInstance()时传入一个接口数组
        // 这里会获取接口数组中所有接口的所有方法,目前只传递一个接口,并且该接口中只有一个方法,因此只显示一个方法
        m3 = Class.forName("org.example.IService").getMethod("show", Class.forName("java.lang.String"));
    }

    public ServiceJDKProxy(InvocationHandler invocationHandler) {
        // 关键点1:调用父类构造器,即Proxy类的构造器
        super(invocationHandler);
    }

    public final int hashCode() {
        return (Integer)super.h.invoke(this, m0, (Object[])null);
    }
    
    public final boolean equals(Object args) {
        return (Boolean)super.h.invoke(this, m1, new Object[]{args});
    }

    public final String toString() {
        return (String)super.h.invoke(this, m2, (Object[])null);
    }

    // 关键点2:所有的方法参数构造成一个字符串,然后再将该特殊格式的字符串反解析成一个Object[],实现参数的传递
    //(类比JSON序列化和反序列化)
    public final void show(String args) {
        // 关键点3:super.h
        super.h.invoke(this, m3, new Object[]{args});
    }
}

动态代理原理图

从上面的字节码文件中,可以梳理出 JDK 动态代理的原理图如下:

  1. 动态代理类和目标类之间没有任何关系,只是共同实现了指定接口

  2. 动态代理类的父类是 Proxy,在父类构造方法中注入了 InvocationHandler 对象,所以后面通过 super.h.invoke() 实质上就是注入的 MyInvocationHandler 对象中的 invoke 方法,也就是自定义 MyInvocationHandler 的 invoke 方法

  3. 在动态代理类中所有的增强方法本质上都是去调用了 InvocationHandler 对象的 invoke 方法,只是传递的参数不同而已

    public class Proxy implements java.io.Serializable {
    	protected InvocationHandler h;
        
        protected Proxy(InvocationHandler h) {
            Objects.requireNonNull(h);
            this.h = h;
        }
    }
    

请添加图片描述

图:JDK 动态代理原理图

下面内存结构需要额外注意的是 Method 对象是指目标类中的方法对象,在动态代理类中所有的增强方法本质上都是去调用了 InvocationHandler 对象的 invoke 方法,只是传递的参数不同而已。

InvocationHandler 对象是连接代理类对象和目标类对象的核心,Proxy 是作为父类。

在这里插入图片描述

图:动态代理类内存结构

增强方法的调用过程

现在我们可以来回答最后一个问题,增强方法是如何被调用的:

  1. 动态代理类中的所有增强方法本质上都是调用 InvocationHandler 对象的 invoke 方法(动态代理类的生成规则)
  2. 动态代理类将目标类中的方法对象 Method(当前调用方法的同名对象)传递给 InvocationHandler 对象
  3. InvocationHandler 对象中此时具有 Method 对象(原始方法)和 Target 对象(目标类),此时便可以调用到原始方法

在这里插入图片描述

图:增强方法的调用过程

总结

JDK 动态代理的核心是对代理类的增强方法和目标类的原始方法对象的进行动态绑定(这部分是 JDK 源码做的事情);

而作为 JDK Proxy 的使用者,我们使用动态代理的核心就是正确地设计自定义的 InvocationHandler 类,也就是传入目标类对象

从调用过程中来看,JDK 完成前半部分的绑定工作,使用者完成后半部分 Target 对象的注入和方法调用工作。

Proxy.newProxyInstance() 主要有两个作用:

  1. 拦截接口数组中的所有方法,创建代理类
  2. 为 Proxy 注入 InvocationHandler 对象,而 InvocationHandler 对象则是连接代理类对象和目标类对象的关键。
  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值