spring AOP 增强

AOP 底层实现方式之一是代理,由代理结合通知和目标,提供增强功能

第一种是动态代理,也是主要的代理方法。Spring AOP 中的代理增强主要有 JDK 动态代理和 CGLIB 两种实现方式,具体使用哪种实现方式取决于目标对象是否实现了接口。如果目标对象实现了接口,则使用 JDK 动态代理;如果目标对象没有实现接口,则使用 CGLIB。第二种是织入(weaving)。基本上是使用aspectj,其主要思想就是将增强功能写入目标的字节码中。

除此以外,aspectj 提供了两种另外的 AOP 底层实现:

  • 第一种是通过 ajc 编译器在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中

  • 第二种是通过 agent 在加载目标类时,修改目标类的字节码,织入增强功能

简单比较的话:

  • aspectj 在编译和加载时,修改目标字节码,性能较高

  • aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法、对 final 也能增强

  • 但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语法,因此没有广泛流行

  1. JDK动态代理: JDK动态代理是基于接口的代理,它利用Java的反射机制在运行时动态生成代理对象。JDK动态代理要求目标类实现一个或多个接口,代理对象实现了这些接口,并将方法调用转发给目标对象。JDK动态代理使用**java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler**接口来实现代理。

    使用JDK动态代理的步骤如下:

    • 定义一个实现InvocationHandler接口的代理处理器类,该类负责实现代理逻辑。

    • 通过Proxy类的**newProxyInstance()**方法创建代理对象,该方法需要传入目标类的类加载器、目标类实现的接口以及代理处理器实例。

    • 通过代理对象调用方法,代理处理器会将方法调用转发给目标对象并进行额外的处理。

    JDK动态代理的优点是它是基于接口的代理,不依赖第三方库,易于使用。但它只能代理实现了接口的目标类。

  2. CGLIB动态代理: CGLIB动态代理是基于类的代理,它通过生成目标类的子类来实现代理。CGLIB动态代理不要求目标类实现接口,它可以代理任何类(包括没有实现接口的类)。CGLIB动态代理使用字节码生成库,通过继承目标类并重写其方法来实现代理。

    使用CGLIB动态代理的步骤如下:

    • 定义一个继承自**net.sf.cglib.proxy.MethodInterceptor**的代理处理器类,该类负责实现代理逻辑。

    • 使用Enhancer类创建代理对象,设置目标类的类和代理处理器。

    • 通过代理对象调用方法,代理处理器会将方法调用转发给目标对象并进行额外的处理。

    CGLIB动态代理的优点是它可以代理任意类,不需要实现接口,而且生成的代理类性能比JDK动态代理更高。但它依赖于CGLIB库,并且无法代理final类和final方法。

JDK代理

先从一段示例代码讲起

这段代码整体还是比较清晰的。

我们定义了一个接口(JDK代理要求实现接口),和接口的实现类。而代理的行为则通过接口回调来实现,这也是代理的常见操作了,这里我们仅仅是简单地在调用方法前后打印一些提示。

而似乎黑匣子只有 Proxy::newProxyInstance 。 我们打印出来的增强类名为

Untitled

由于它是由JDK运行期间动态生成的字节码文件经由类加载获得的,所以我们无法直接查看其中的内容,这也就是为什么我要在示例代码中生成加入System.getProperties().put("sum.misc.ProxyGenerator.saveGeneratedFiles","true"); 这样子动态生成的字节码文件就能被我们查看了。

可以看到无论是什么增强的方法,其实现都是在**InvocationHandler** 中去完成的。而也正是因为**InvocationHandler** 的存在,才让JDK能在没确定代理类的行为前就将代理类生成好。

CGLIB代理

这里先给出示例代码:

先从最直观的地方将起,可以看到我们创建增强对象时传入了一个接口,在该接口中我们规定了增强行为,这里只是在调用方法前后打印一些信息。

而我们调用方法本体有三种方法:

  1. 反射调用原方法

  2. 使用invoke(Object obj, Object[] args) 调用原方法

  3. 使用invokeSuper(Object obj, Object[] args) 调用原方法

其中方法2要我们传递的是未增强的对象,而方法3传递的是增强后的对象,即这里的proxy。

我们看看这两个方法的结构:

这两个方法在try{}语句块中的行为似乎极为相似,只有invoke()的调用者不同。我们点开FastClassInfo :

嗯嗯,这么看f1与f2都是FastClass ,我们再看看这个负责调用原方法的FastClass是何方神圣

Untitled

嘶,抽象类,而且你找不到它的子类。这是因为这玩意的实现实际上是由cglib动态生成的字节码,你也就无法直接看到它的实现,也无法直接调试。

所以我们在示例代码中加入了System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\Users\\mskk\\Desktop\\STUDY\\spring高级\\代码\\show"); 让cglib生成的字节码文件保存到我们的磁盘中,这样我们就能看到它的实现了

Untitled

我们看到生成了三个类,而其中第一个是cglib为我们生成的增强类——没错,增强类也是cglib动态生成的。而后两个类就都是FastClass的实现类了——和刚刚的FastClassInfo 中有两个FastClass似乎对应上了?

我们简单看一下这两个FastClass的结构:

Untitled
Untitled

至少方法种类都是一样的,我们再来看看它两的involve()方法

被第一个吓晕 😇。 但是冷静看看就知道它们都是依据var1变量来执行不同的方法,并且调用方法的都是var10000 。 而var10000 则是由var2转换来的。

我们先看看比较短的第二个吧,由类型强转很容易看出这里的var2 应该就是我们的未增强对象了,和我们刚刚在讲三种调用原始方法的方法那里对应上了,传进来的为增强对象就是在这里调用它的方法了。而我们也确实能看到我们要增强的方法foo() 。 而它这里还为我们增加了equals() , toString()hashCode() 方法。

我们再来看看第一个比较长的invoke() 吧。

emmmmmm,眼睛细一点就能发现第一个强转的类型怎么有些眼熟?这不是刚刚cglib动态生成的增强类吗?

Untitled
Untitled

这么说调用原方法的实际上是我们的增强类。这似乎也和我们刚刚讲的三种方法对应上了。

我们再来看看cglib为我们生成的增强类:

首先可以看到该增强类继承了我们的目标类,这也对应了cglib的增强类是目标类的子类,即cglib无法增强final修饰的类。这里看到它有很多的Method 的成员变量,基本都对应到了它父类的方法。还有许多MethodProxy 的成员变量,实际上也是和父类方法一一对应,实际上就是我们在示例中的methodProxy 了,而在它们的创建方法中,var1 对应的是要增强的类(即父类),而var0就是增强类本身;而诸如(Ljava/lang/Object;)Z 则实际上是java字节码中描述方法的参数和返回值所用的格式;第三个参数不难猜出是原始方法名,而第四个参数就是增强后的方法名。 所以不难知道它是依据这些参数找到形成方法签名来找到对应的方法的,并且应该也是类似于两个FastClass一样对应着原始类和增强类。

而它的sig1、sig2、createInfo这些成员变量实际用到也是在init() 内,去加载FastClass :

我们先来看看FastClassgetIndex() ,因为结合init()invoke(Object obj, Object[] args) invokeSuper(Object obj, Object[] args) 方法我们能看出我们对FastClass的方法调用还与这个方法有关。

可以看到该方法有三个重载,而我们的init() 使用的是第三种重载,即参数为方法签名:

Untitled

直接上结论吧,相当清晰,getIndex() 方法查找到对应的索引,然后MethodProxy 又基于这个索引调用FastClassinvoke(int var1, Object var2, Object[] var3)FastClass 再基于索引调用对应的方法

这么说来FastClass 可以视为一个工具类,让我们基于方法签名查找方法索引,再让FastClass 来基于索引调用原方法。

? 可是这么一看它不应该是同一个增强类的MethodProxy 都应该共用相同的FastClass吗 🤔?毕竟大家增强的都是同一个类,也都属于同一个增强类,方法都一样,甚至似乎一个MethodProxy 只对应一个方法,需要每个对象都专门持有FastClass 这个记录了所有方法的工具类的对象吗?

这只能从我们是怎么获得FastClass 来去找到答案了。我们在MethodProxy 中不难发现FastClassInfo也是在init() 方法中加载的。这就是刚刚被我跳过的 fci.f1 = helper(ci, ci.c1); fci.f2 = helper(ci, ci.c2);

不多bb,直接给结论:FastClass是基于类加载创建的,由private static FastClass helper(CreateInfo ci, Class type) 方法中的type参数决定创建对应类型的FastClass 。而类加载构建的过程中,相关的信息都是被缓存起来的,也就是说,我们拿着相同的参数创建出来的FastClass 对象都是同一个对象。而我们在init() 中调用helper(CreateInfo ci, Class type) 时的参数都是在创建MethodProxy 时提供的(对应到Enhancer 的静态代码块中),而fci.f1 = helper(ci, ci.c1); fci.f2 = helper(ci, ci.c2); 中的ci.c1 ci.c2 对应的就是原始类和增强类,所以创建出来的两个FastClass 对象也就实际对应着原始类和增强类。现在看来前面的伏笔都一一对应上了。

切点

todo

环绕通知

todo

Last updated