• 作者简介:咱们好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 系列专栏:Java规划形式、Kafka从成神到升仙、Spring从成神到升仙系列
  • 假如感觉博主的文章还不错的话,请三连支撑一下博主哦
  • 博主正在努力完结2023计划中:以梦为马,扬帆起航,2023追梦人
  • 联系办法:hls1793929520,和咱们一同学习,一同前进

署理形式

一、导言

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

其中,AOP 中署理形式的重要性显而易见,但关于没了解过署理形式的人来说,苦楚至极

所以,我就去看了动态署理的完结,发现网上大多数文章讲的都是不清不楚,乃至讲了和没讲似的,让我极其难受

本着咱们方向主打的便是源码,直接从从源码角度叙述一下 署理形式

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

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

二、定义

署理形式的定义:因为某些原因需求给某方针供给一个署理以操控对该方针的拜访。这时,拜访方针不适合或者不能直接引证方针方针,署理方针作为拜访方针和方针方针之间的中介。

举个日子中常见的例子:客户想买房,房东有许多房,供给卖房服务,但房东不会带客户看房,所以客户经过中介买房。

2023年再不会动态代理,就要被淘汰了
这时分关于房东来说,不直接和客户交流,而是交于中介进行署理

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

三、静态署理

咱们创立 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署理或接口署理。

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

  • 静态署理在编译时就现已完结了,编译完结后署理类是一个实践的 class 文件
  • 动态署理是在运行时动态生成的,即编译完结后没有实践的 class 文件,而是在运行时动态生成类字节码,并加载到 JVM

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字节码)就有许多途径:

2023年再不会动态代理,就要被淘汰了

  • 从本地获取

  • 从网络中获取

  • 运行时核算生成,这种场景运用最多的是动态署理技能,在 java.lang.reflect.Proxy 类中,便是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的署理类的二进制字节省

    2023年再不会动态代理,就要被淘汰了
    所以,动态署理便是想办法,依据接口或方针方针,核算出署理类的字节码,然后再加载到 JVM 中运用

1.2 JDK动态署理流程

所以,咱们能够得出一个定论:咱们上面的 $Proxy0 实践上是 JVM 在编译时期加载出来的类,因为这个类是编译时期加载的,所以咱们没办法在 IDEA 里边看到。

或许一般的文章,到这儿基本就完毕了,让咱们知道 $Proxy0是由 JVM 编译时期加载出来的类

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

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

Arthas 是一款线上监控确诊产品,经过大局视角实时查看运用 load、内存、gc、线程的状况信息,并能在不修正运用代码的情况下,对事务问题进行确诊,包含查看办法调用的出入参、异常,监测办法履行耗时,类加载信息等,大大提升线上问题排查功率

当咱们一切预备完结后,发动咱们上面动态署理的测验 JavaMain

发动完结后,进入咱们的 arthas 页面,履行命令:java -jar arthas-boot.jar

2023年再不会动态代理,就要被淘汰了

咱们能够看到,咱们的方针类 com.company.proxy.JavaMain 就出现了,随后咱们按下 4,进入到咱们的监控页面。

2023年再不会动态代理,就要被淘汰了

随后运用 jad com.sun.proxy.$Proxy0 之后,能够看到咱们现已解析出来 $Proxy0 的源码了

2023年再不会动态代理,就要被淘汰了

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

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;
                    }
                }
        );

假如说咱们这个猜想是正确的话,那么会得出这样的几个定论:

  • 咱们的署理类实践上是完结了 Landlord 的接口,然后重写了 Landlord 接口中的 apartmentToRent 办法
  • 当外界调用署理类的 apartmentToRent() 办法时,实践上是调用的咱们自定义的 new InvocationHandler() 类里边的 invoke 办法

2023年再不会动态代理,就要被淘汰了

还有咱们的最终一步,也便是证明 $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});
}

咱们经过源码能够看到,总共分为三步(下面为反射的内容,如不了解可提前学习下反射):

  • 拿到 $Proxy0Class
  • 依据 Class 拿到其结构办法
  • 依据结构办法传入参数进行实例化

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

2、Cglib署理

cglib (Code Generation Library ) 是一个第三方代码生成类库,运行时在内存中动态生成一个子类方针然后完结对方针方针功用的扩展。cglib 为没有完结接口的类供给署理,为 JDK 的动态署理供给了很好的弥补。

2023年再不会动态代理,就要被淘汰了

  • 最底层是字节码
  • ASM 是操作字节码的工具
  • cglib 基于 ASM 字节码工具操作字节码(即动态生成署理,对办法进行增强)
  • SpringAOP 基于 cglib 进行封装,完结 cglib 办法的动态署理

运用 cglib 需求引入 cglib 的jar包,假如你现已有 spring-core 的jar包,则无需引入,因为 spring 中包含了cglib

  • cglib 的Maven坐标

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.2.5</version>
    </dependency>
    

2.1 cglib动态署理完结

仍是同样的配方,咱们要创立一个需求署理的类(UserServiceImpl),但不需求完结任何的接口,因为咱们的 cglib 是依据类来进行创立的。

UserServiceImpl

public class UserServiceImpl {
    // 查询功用
    List<String> 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<String> 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();
    }
}

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

2023年再不会动态代理,就要被淘汰了

  • JVM 编译期间,咱们的 Enhancer 会依据方针类的信息去动态的生成 动态署理类并设置 回调
  • 当用户在经过上述的动态署理类履行 findUserList() 办法时,有两个履行选项
    • 若设置了回调接口,则直接调用UserLogProxy 中的 intercept ,然后经过 FastClass 类调用动态署理类,履行CGLIB$findUserList$0 办法,调用父类的 findUserList() 办法
    • 若没有设置回调接口,则直接调用父类的 findUserList() 办法

五、署理形式总结

1、三种署理形式完结办法的对比

  • jdk 署理和 CGLIB 署理

    • 运用 CGLib 完结动态署理,CGLib 底层采用 ASM 字节码生成结构,运用字节码技能生成署理类,在JDK1.6 之前比运用 Java 反射功率要高。唯一需求留意的是,CGLib 不能对声明为 final 的类或者办法进行署理,因为 CGLib 原理是动态生成被署理类的子类。

    • JDK1.6JDK1.7JDK1.8 逐渐对 JDK 动态署理优化之后,在调用次数较少的情况下,JDK 署理功率高于 CGLib 署理功率,只有当进行很多调用的时分,JDK1.6JDK1.7CGLib 署理功率低一点,但是到 JDK1.8 的时分,JDK 署理功率高于 CGLib 署理。所以假如有接口运用 JDK 动态署理,假如没有接口运用 CGLIB 署理。

  • 动态署理和静态署理

    • 动态署理与静态署理相比较,最大的好处是接口中声明的一切办法都被转移到调用处理器一个会集的办法中处理(InvocationHandler.invoke)。这样,在接口办法数量比较多的时分,咱们能够进行灵敏处理,而不需求像静态署理那样每一个办法进行中转。
    • 假如接口添加一个办法,静态署理形式除了一切完结类需求完结这个办法外,一切署理类也需求完结此办法。添加了代码保护的复杂度。而动态署理不会出现该问题

2、署理形式优缺陷

长处:

  • 署理形式在客户端与方针方针之间起到一个中介效果和保护方针方针的效果;
  • 署理方针能够扩展方针方针的功用;
  • 署理形式能将客户端与方针方针分离,在必定程度上降低了体系的耦合度;

缺陷:

  • 添加了体系的复杂度;

3、署理形式运用场景

  • 功用增强

    • 当需求对一个方针的拜访供给一些额定操作时,能够运用署理形式
  • 长途(Remote)署理

    • 实践上,RPC 结构也能够看作一种署理形式,GoF 的《规划形式》一书中把它称作长途署理。经过长途署理,将网络通信、数据编解码等细节隐藏起来。客户端在运用 RPC 服务的时分,就像运用本地函数相同,无需了解跟服务器交互的细节。除此之外,RPC 服务的开发者也只需求开发事务逻辑,就像开发本地运用的函数相同,不需求重视跟客户端的交互细节。
  • 防火墙(Firewall)署理

    • 当你将浏览器配置成运用署理功用时,防火墙就将你的浏览器的请求转给互联网;当互联网回来响应时,署理服务器再把它转给你的浏览器。
  • 保护(Protect or Access)署理

    • 操控对一个方针的拜访,假如需求,能够给不同的用户供给不同等级的运用权限。

六、结尾

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

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

但经过这篇文章,我相信,99% 的人应该都能够理解了动态署理形式的来龙去脉

当然,好刀要用在刀刃上,在面试中,若面试官提及 规划形式动态署理SpringDubbo 都能够引出动态署理,基本这篇文章无差别秒杀

假如你能看到这,那博主必须要给你一个大大的鼓舞,谢谢你的支撑!

喜爱的能够点个重视,后续会更新 Spring 源码系列文章

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,Java范畴新星创作者,喜爱后端架构和中间件源码。

咱们下期再会。