从源码角度讲述动态代理的实现

一、引言

在 Spring 中,最重要的应该当属 IOC 和 AOP 了,IOC 的源码流程还比较简单,但 AOP 的流程就较为抽象了。

其中,AOP 中代理模式的重要性不言而喻,但对于没了解过代理模式的人来说,痛苦至极

于是,我就去看了动态代理的实现,发现网上大多数文章讲的都是不清不楚,甚至讲了和没讲似的,让我极其难受

本着咱们方向主打的就是源码,直接从从源码角度讲述一下代理模式

兄弟们系好安全带,准备发车!

注意:本文篇幅较长,请留出较长时间来阅读

二、定义

代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

举个生活中常见的例子:客户想买房,房东有很多房,提供卖房服务,但房东不会带客户看房,于是客户通过中介买房。

这时候对于房东来说,不直接和客户沟通,而是交于中介进行代理

对于中介来说,她也会在原有的基础上收取一定的中介费

三、静态代理

我们创建 Landlord 接口如下:

public interface Landlord {
    // 出租房子
    void apartmentToRent();
}
复制代码

创建其实现类 HangZhouLandlord 代表杭州房东出租房子

public class HangZhouLandlord implements Landlord {
    @Override
    public void apartmentToRent() {
        System.out.println("杭州房东出租房子");
    }
}
复制代码

创建代理类 LandlordProxy,代表中介服务

public class LandlordProxy {

    public Landlord landlord;

    public LandlordProxy(Landlord landlord) {
        this.landlord = landlord;
    }

    public void apartmentToRent() {
        apartmentToRentBefore();
        landlord.apartmentToRent();
        apartmentToRentAfter();
    }

    public void apartmentToRentBefore() {
        System.out.println("出租房前,收取中介费");
    }

    public void apartmentToRentAfter() {
        System.out.println("出租房后,签订合同");
    }
}
复制代码

创建最终测试:

public class JavaMain {
    public static void main(String[] args) {
        Landlord landlord = new HangZhouLandlord();

        LandlordProxy proxy = new LandlordProxy(landlord);
		  // 从中介进行租房
        proxy.apartmentToRent();
    }
}
复制代码

得出最终结果:

出租房前,收取中介费
杭州房东出租房子
出租房后,签订合同
复制代码

通过上述 demo 我们大概了解代理模式是怎么一回事

四、动态代理

动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能,动态代理又被称为JDK代理或接口代理。

静态代理与动态代理的区别:

1、JDK代理

代码如下:

public class ProxyFactory {
    // 目标方法
    public Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
                // 目标对象的类加载器
                target.getClass().getClassLoader(),
                // 目标对象的接口类型
                target.getClass().getInterfaces(),
                // 事件处理器
                new InvocationHandler() {
                    /**
                     *
                     * @param proxy  代理对象
                     * @param method 代理对象调用的方法
                     * @param args   代理对象调用方法时实际的参数
                     * @return
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("我是前置增强");
                        method.invoke(target, args);
                        System.out.println("我是后置增强");
                        return null;
                    }
                }
        );
    }
}
复制代码

我们测试一下:

public class JavaMain {
    public static void main(String[] args) {
        Landlord landlord = new HangZhouLandlord();

        System.out.println(landlord.getClass());

        Landlord proxy = (Landlord) new ProxyFactory(landlord).getProxyInstance();

        proxy.apartmentToRent();

        System.out.println(proxy.getClass());
        
        while (true){}
    }
}
复制代码

得出结果:

class com.company.proxy.HangZhouLandlord
我是前置增强
杭州房东出租房子
我是后置增强
class com.sun.proxy.$Proxy0
复制代码

这里可能有小伙伴已经懵了,接着往后看

1.1 JDK类的动态生成

Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:

1.2 JDK动态代理流程

所以,我们可以得出一个结论:我们上面的$Proxy0 实际上是 JVM 在编译时期加载出来的类,由于这个类是编译时期加载的,所以我们没办法在 IDEA 里面看到。

可能一般的文章,到这里基本就结束了,让大家知道$Proxy0是由 JVM 编译时期加载出来的类

但大家都知道,小黄的文章主打的就是一个硬核、源码级。所以,我们直接去看$Proxy0 的源代码

首先,我们需要下载一个 arthas 的产品,网址:arthas.aliyun.com/doc/,跟随流程解压…

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

当我们一切准备完成后,启动我们上面动态代理的测试 JavaMain 类

启动完成后,进入我们的 arthas 页面,执行命令:java -jar arthas-boot.jar

我们可以看到,我们的目标类 com.company.proxy.JavaMain 就出现了,随后我们按下4,进入到我们的监控页面。

随后使用 jad com.sun.proxy.$Proxy0 之后,可以看到我们已经解析出来$Proxy0 的源码了

我们将其复制到下面,并删减一些不必要的信息。

public final class $Proxy0 extends Proxy implements Landlord {
    private static Method m3;
    
    // $Proxy0 类的构造方法
    // 参数为 invocationHandler
    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        m3 = Class.forName("com.company.proxy.Landlord").getMethod("apartmentToRent", new Class[0]);
    }

    public final void apartmentToRent() {
        this.h.invoke(this, m3, null);
        return;
    }
}
复制代码

我们先看其有参构造方法,可以看到$Proxy0 的构造方法入参为 InvocationHandler,有没有感觉似曾相识。

如果你这里忘掉了,不妨去看一下动态代理的 ProxyFactory 的代码,可以发现,我们 Proxy.newProxyInstance()的第三个自定义的参数,也正是我们的 InvocationHandler。

我们猜测一下,如果这里的传的 InvocationHandler 是我们之前自定义的 InvocationHandler

那么,如果我调用$Proxy0.apartmentToRent()是不是就是执行下面的代码:

public final void apartmentToRent() {
    this.h.invoke(this, m3, null);
    return;
}

// 这里的h.invoke执行的是我们这里自定义的方法,然后进行的前后增强
public Object getProxyInstance() {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("我是前置增强");
                        method.invoke(target, args);
                        System.out.println("我是后置增强");
                        return null;
                    }
                }
        );
复制代码

如果说我们这个猜测是正确的话,那么会得出这样的几个结论:

还有我们的最后一步,也就是证明$Proxy0 的构造入参 InvocationHandler 就是我们自定义的 InvocationHandler,废话不多说,直接来看代理的源码。

return Proxy.newProxyInstance(ClassLoader,Interfaces,new InvocationHandler() {});
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h){
    // cl = class com.sun.proxy.$Proxy0
    Class<?> cl = getProxyClass0(loader, intfs);
    // cons = public com.sun.proxy.$Proxy0(java.lang.reflect.InvocationHandler)
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    // 根据构造参数实例化对象
    return cons.newInstance(new Object[]{h});
}
复制代码

我们通过源码可以看到,一共分为三步(下面为反射的内容,如不熟悉可提前学习下反射):

这就确定了我们上述的猜想是正确的。

2、Cglib代理

cglib (Code Generation Library ) 是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。cglib 为没有实现接口的类提供代理,为 JDK 的动态代理提供了很好的补充。

使用 cglib 需要引入 cglib 的jar包,如果你已经有 spring-core 的jar包,则无需引入,因为 spring 中包含了cglib 。

2.1 cglib动态代理实现

还是同样的配方,我们要创建一个需要代理的类(UserServiceImpl),但不需要实现任何的接口,因为我们的 cglib 是根据类来进行创建的。

UserServiceImpl

public class UserServiceImpl {
    // 查询功能
    List findUserList() {
        return Collections.singletonList("小A");
    }
}
复制代码

实现 cglib 的工厂类:UserLogProxy

public class UserLogProxy implements MethodInterceptor {
    /**
     * 生成 CGLIB 动态代理类方法
     *
     * @param target
     * @return
     */
    public Object getLogProxy(Object target) {
        // 增强器类,用来创建动态代理类
        Enhancer enhancer = new Enhancer();

        // 设置代理类的父类字节码对象
        enhancer.setSuperclass(target.getClass());

        // 设置回调
        enhancer.setCallback(this);

        // 创建动态代理对象并返回
        return enhancer.create();

    }

    /**
     * @param o         代理对象
     * @param method      目标对象中的方法的Method实例
     * @param objects     实际参数
     * @param methodProxy   代理类对象中的方法的Method实例
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("前置输出");
        Object result = methodProxy.invokeSuper(o, objects);
        return result;
    }
}
复制代码

测试程序:JavaMainTest

public class JavaMainTest {
    public static void main(String[] args) {

        // 目标对象
        UserServiceImpl userService = new UserServiceImpl();
        System.out.println(userService.getClass());

        // 代理对象
        UserServiceImpl proxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService);
        System.out.println(proxy.getClass());

        List list = proxy.findUserList();
        System.out.println("用户信息:" + list);

        while (true) {

        }
    }
}
复制代码

结果:

class com.study.spring.proxy.UserServiceImpl
class com.study.spring.proxy.UserServiceImpl$EnhancerByCGLIB$cd9788d
前置输出
用户信息:[小A]
复制代码

2.2 cglib代理流程

按照上述我们分析$Proxy0 的方法,将 com.study.spring.proxy.UserServiceImpl$EnhancerByCGLIB$cd9788d 取出,得到如下:

public class UserServiceImpl$EnhancerByCGLIB$cd9788d extends UserServiceImpl implements Factory {
    final List findUserList() {
        // 是否设置了回调
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl$EnhancerByCGLIB$cd9788d.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        // 设置回调,需要调用 intercept 方法
        if (methodInterceptor != null) {
            return (List) methodInterceptor.intercept(this, CGLIB$findUserList$0$Method, CGLIB$emptyArgs, CGLIB$findUserList$0$Proxy);
        }
        // 无回调,调用父类的 findUserList 即可
        return super.findUserList();
    }
    final List CGLIB$findUserList$0() {
        return super.findUserList();
    }
}
复制代码

博主先把整个流程图放到下面,然后结合流程图来进行讲解:

五、代理模式总结

1、三种代理模式实现方式的对比

2、代理模式优缺点

优点:

缺点:

3、代理模式使用场景

六、结尾

终于写完了这篇文章,动态代理在我看 AOP 源码时,就感觉挺抽象的

我感觉最大的原因应该在于:代理类动态生成,无法查看,导致对其模糊,从而陷入不理解

但通过这篇文章,我相信,99%的人应该都可以理解了动态代理模式的来龙去脉

当然,好刀要用在刀刃上,在面试中,若面试官提及设计模式、动态代理、Spring、Dubbo 都可以引出动态代理,基本这篇文章无差别秒杀

如果你能看到这,那博主必须要给你一个大大的鼓励,谢谢你的支持!

喜欢的可以点个关注,后续会更新 Spring 源码系列文章

我们下期再见。

展开阅读全文

页面更新:2024-06-02

标签:源码   动态   字节   加载   接口   角度   对象   目标   模式   代码   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top