【从零构建Spring|第十二节】 基于JDK, Cglib实现AOP切面

【从零构建Spring|第十二节】 基于JDK, Cglib实现AOP切面

前言

本章节正式从 IoC 的实现,转向关于 AOP 内容的开发。

AOP(Aspect Oriented Programming) 内容开发,意为:面向切面编程,通过预处理的方式以及运行期间动态代理实现程序功能的统一维护。AOP 也是 OOP 的延续,在 Spring 框架中是一个非常非常重要的内容,使用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得各模块之间的业务逻辑耦合度降低

剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

举一个不太恰当的例子:可以把切面理解为用刀切韭菜,一根一根切总是有点慢,那么用手(代理)把韭菜捏成一把,用菜刀或者斧头这样不同的拦截操作来处理。而程序中其实也是一样,只不过韭菜变成了方法,菜刀变成了拦截方法。

实现 AOP 的技术,主要分为两大类:

  • 一是采用动态代理技术(典型代表为Spring AOP),利用截取消息的方式(典型代表为AspectJ-AOP),对该消息进行装饰,以取代原有对象行为的执行;
  • 二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

关于代理,可以给一个接口的实现,但是不是通过 new 一个对象实例,而是通过代理的方式去替换掉这个实现类,使用代理类来去处理所需要的逻辑,例如:

@Test
public void test_proxy_class() {
IUserService userService = (IUserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IUserService.class}, (proxy, method, args) -> "你被代理了!");
String result = userService.queryUserInfo();
System.out.println("测试结果:" + result);
}

代理类的实现基本如下,那么有了一个基本的思路后,接下来就需要考虑下怎么给方法做代理,而不是代理类。另外怎么去代理所有符合某些规则的所有类中方法呢。如果可以代理掉所有类的方法,就可以做一个方法拦截器,给所有被代理的方法添加上一些自定义处理,比如打印日志、记录耗时、监控异常等

规则:其实也就是 execution 表达式,他的通常格式如下:"execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))",也就是寻找包下面的所有方法

也可以是注解,通过@AroundBefore、@AroudAfter 等等寻找合适的切点。

设计

想要将 AOP 切面编程思想融合到 Spring,我们需要思考两个问题:

  • 方法代理:如何给符合规则的方法做代理
  • 拦截处理:做好代理方法的案例后,怎么把类的职责抽分出来?

image-20230213195718812

AOP 只需根据匹配规则处理需要被处理的方法,在拦截方法之后,执行方法的扩展操作

手写Spring-基于JDK和Cglib动态代理

  • 整个类关系图就是 AOP 实现核心逻辑的地方
  • AspectJExpressionPointcut 的核心功能主要依赖于 aspectj 组件并处理 Pointcut、ClassFilter,、MethodMatcher 接口实现,专门用于处理类和方法的匹配过滤操作。
  • AopProxy 是代理的抽象对象,它的实现主要是基于 JDK 的代理和 Cglib 代理。在前面章节关于对象的实例化 CglibSubclassingInstantiationStrategy,我们也使用过 Cglib 提供的功能。

动态代理流程

一个动态代理的流程代码如下:

  • 首先整个案例的目标是给一个 UserService 当成目标对象,对类中的所有方法进行拦截添加监控信息打印处理。
  • 从案例中你可以看到有代理的实现 Proxy.newProxyInstance,有方法的匹配 MethodMatcher,有反射的调用 invoke(Object proxy, Method method, Object[] args),也用用户自己拦截方法后的操作。这样一看其实和我们使用的 AOP 就非常类似了,只不过你在使用 AOP 的时候是框架已经提供更好的功能,这里是把所有的核心过程给你展示出来了。
  • 从测试结果可以看到我们已经对 UserService#queryUserInfo 方法进行了拦截监控操作,其实后面我们实现的 AOP 就是现在体现出的结果,只不过我们需要把这部分测试的案例解耦为更具有扩展性的各个模块实现。

按照这样的逻辑,我们需要将核心功能按照模块功能解耦

image-20230213202919056

  • 代理对象:可以用 JDK 方式实现,也可以用 Cglib 实现
  • 方法匹配器:方法匹配器操作其实已经是一个单独的实现类了,不过我们还需要把传入的目标对象、方法匹配、拦截方法,都进行统一的包装,方便外部调用时进行一个入参透传。
  • 方法拦截器:按照代码给出的逻辑,只要在参数返回后执行相关扩展逻辑操作即可
  • 反射调用:它目前已经是实现 MethodInvocation 接口的一个包装后的类,参数信息包括:调用的对象、调用的方法、调用的入参

工程

切点 Pointcut

定义切点接口 Pointcut,即org.springframework.aop.Pointcut,Spring AOP 体系对切点的顶层抽象,贯穿整个 AOP 框架。

/**
* 切入点接口
* 定义用于获取 ClassFilter、MethodMatcher 的两个类,这两个接口获取都是切点表达式提供的内容。
*/
public interface Pointcut {
/**
* 返回此切入点的类筛选器。
*/
ClassFilter getClassFilter();

/**
* 返回此切入点的方法匹配器
*/
MethodMatcher getMethodMatcher();
}
  • 在该接口中主要负责对系统的相应的 Joinpoint 进行捕捉,对系统中所有的对象进行 Joinpoint 所定义的规则进行匹配。ClassFilter 与 MethodMatcher 分别用于在不同的级别上限定 Joinpoint 的匹配范围,满足不同粒度的匹配
  • ClassFilter 限定在类级别上,MethodMatcher 限定在方法级别上

提示

Spring Aop 主要支持在方法级别上的匹配,所以对类级别的匹配支持相对简单一些

方法匹配器 MethodMatcher

/**
* 方法匹配器
* 找到表达式范围内匹配下的目标类和方法。
*/
public interface MethodMatcher {

// 这个称为静态匹配:在匹配条件不是太严格时使用,可以满足大部分场景的使用
boolean matches(Method method, @Nullable Class<?> targetClass);
// 这个称为动态匹配(运行时匹配): 它是严格的匹配。在运行时动态的对参数的类型进行匹配
boolean matches(Method method, @Nullable Class<?> targetClass, Object... args);

//两个方法的分界线就是boolean isRuntime()方法,步骤如下
// 1、先调用静态匹配,若返回true。此时就会继续去检查isRuntime()的返回值
// 2、若isRuntime()还返回true,那就继续调用动态匹配
// (若静态匹配都匹配上,动态匹配那铁定更匹配不上得~~~~)

// 是否需要执行动态匹配
boolean isRuntime();

}
  • 当需要统计用户登录次数时,登录传入的参数就可以忽略,静态匹配足以
  • 当需要在登录时对用户账号执行特殊操作(如赋权),就需要对参数进行检验,需要动态匹配
  • 当然在实现中没有那么复杂,我们只需要了解这些机制即可,目前只实现了静态匹配。

切入点表达式 AspectJExpressionPointcut

它与 Expression 表达式有关,这个切点表达式的解析,需要依赖于 AspectJ 的 jar 包进行解析,Spring 在 使用 @Aspect 注解时会大量使用到它

在 Spring 框架中,实现切点匹配的还有:基于正则的 JdkRegexpMethodPointcut 。用AspectJExpressionPointcut实现的切点比JdkRegexpMethodPointcut实现切点的好处就是,在设置切点的时候可以用切点语言来更加精确的表示拦截哪个方法。(可以精确到返回参数,参数类型,方法名,当然,也可以模糊匹配)

源码分析:

  • Spring 支持的 AspectJ 的切点语言表达式一共有 10 中(加上后面的自己的 Bean 方式一共11种), 详细介绍文章可看这篇:Spring AOP中@Pointcut切入点表达式最全面使用介绍 (opens new window),我们本章所实现的 execution,一般能在运行指定方法时拦截指定方法,用的最多
  • 在实现中,我们能发现 AspectJExpressionPointCut 既是方法匹配器,又是类筛选器,也是切点,同时这个类主要是的 Aspectj 包提供的表达式检验方法使用
  • 实现匹配 matches 方法:类筛选和方法匹配

组装代理信息 AdvisedSupport

数据结构为 AdvisedSupport { targetSource 代理目标对象,MethodInterceptor 方法拦截器,MethodMatcher 方法匹配器}

TargetSource 代理目标对象

TargetSource 用于获取当前 MethodInvocation (方法调用)所需的 target (目标对象),target 通过反射方式被调用 method.invoke(target, args) ,proxy 代理的不是 target 而是 targetSource。

为什么 Spring Aop 代理不直接代理 target,而是通过代理 TargetSource 间接代理 target?

在通常情况下,一个 proxy 只能代理一个 target,每次方法调用的目标也是唯一固定的 target。

但如果让 proxy 代理 TargetSource,可以使得每次方法调用的 target 实例不同,这种机制使得方法的调用变得灵活,可以扩展很多高级功能,例如目标对象池(target pool),运行时目标对象热替换(hot swap)(当前并未实现这些扩展,非核心机制)

TargetSource 组件本身与 Spring IoC 无关,target 的生命周期不一定受 Spring 容器管理,以往的 XML 中 AOP 的配置,只是对受容器管理的 Bean 而言的,当然也可以手动创建一个 Target,同时使用 Spring AOP 而不使用 Spring IoC 容器

public class TargetSource {

private final Object target;

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

/**
* 返回此 {@link TargetSource} 返回的目标类型。
* 可以返回 null,尽管 TargetSource 的某些用法可能只适用于预定的目标类。
*/
public Class<?>[] getTargetClass() {
return this.target.getClass().getInterfaces();
}

/**
* 返回目标实例。在 AOP 框架调用 AOP 方法调用的“目标”之前立即调用。
*/
public Object getTarget() {
return this.target;
}
}

MethodInterceptor 方法拦截器

前言

Interceptor 拦截器中定义了通知的增强方式,也就是对 JoinPoint (连接点) 的拦截。一个通用的拦截器可以拦截所有的运行时事件,运行时连接点可以是一次方法调用、字段访问、异常产生。Interceptor 接口强调概念而非功能。

由 Interceptor 扩展出的ConstructorInterceptorMethodInterceptor 两个子接口,才具体定义了拦截方式。他们一个用于拦截构造方法,一个拦截普通方法。但是 Spring AOP 对构造方法的拦截,原因是 Spring 框架本身通过 BeanPostProcessor 的定义已经将 Bean 生命周期的扩展实现的很充分了,完全可以在初始化前后拦截实现方法增强

MethodInterceptor 只定义了增强方式,实现交付给用户自定义具体的增强内容。当然 Spring 也提供了三种预定义的增强内容:前置通知 BeforeAdvice,后置通知 AfterAdvice 以及动态引介通知 DynamicIntroductionAdvice。前两者定义了增强内容的执行时机(方法调用前后执行),后者则是可以编辑目标类要实现的接口列表

最后,Spring 预定义的通知还是要通过对应的适配器,适配成 MethodInterceptor 接口类型的对象(如:MethodBeforeAdviceInterceptor 负责适配 MethodBeforeAdvice)。

public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation var1) throws Throwable;
}
...
/**
* 自定义拦截方法, 该拦截器最终会被 JDK、Cglib 方式实现的 AopProxy 调用
*/
public class UserServiceInterceptor implements MethodInterceptor {

/**
* 户自定义的拦截方法需要实现 MethodInterceptor 接口的 invoke 方法,
* 使用方式与 Spring AOP 非常相似,也是包装 invocation.proceed() 放行,并在 finally 中添加监控信息。
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
System.out.println("监控 - Begin By AOP");
System.out.println("方法名称:" + invocation.getMethod());
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("监控 - End\r");
}
}

}

约定动态代理接口 AopProxy

public interface AopProxy {

/**
* 定义一个标准接口,用于获取代理类。
* 因为具体实现代理的方式可以有 JDK 方式,也可以是 Cglib 方式,所以定义接口会更加方便管理实现类。
*/
Object getProxy();

}
  • 定义一个标准接口,用于获取代理类。当具体实现由多种方式时,使用接口统一规范,能更方便管理实现类

JDK 实现动态代理

JDK动态代理是面向接口的代理模式,如果被代理目标没有接口那么Spring也无能为力

Spring通过java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。

  • 需要实现接口 AopProxy、InvocationHandler,这样就可以把代理对象 getProxy 和反射调用方法 invoke 分开处理了
  • getProxy 方法是代理一个对象的操作,需要提供入参 ClassLoader、AdvisedSupport、和当前类 this,因为当前类提供 invoke

Cglib 实现动态代理

CGLib是一个强大、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过 CGlib

继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程

  • 基于 Cglib 使用 Enhancer 代理的类可以在运行期间为接口使用底层 ASM 字节码增强技术处理对象的代理对象生成,因此被代理类不需要实现任何接口。

两者对比:

JDK 动态代理是面向接口,在创建代理实现类时比 CGLib 要快,创建代理速度快。

CGLib 动态代理是通过字节码底层继承要代理类来实现(如果被代理类被 final 关键字所修饰,那么抱歉会失败),在创建代理这一块没有 JDK 动态代理快,但是运行速度比 JDK 动态代理要快

使用注意:

如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制)

如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。

使用:

通过配置Spring的中aop:config标签来显示的指定使用动态代理机制 proxy-target-class=true表示使用CGLib代理,如果为false就是默认使用JDK动态代理

<aop:config proxy-target-class="true">
<!-- 切面详细配置.. -->
</aop:config>

测试

AOP 是如何完成动态代理的?

@Test
public void test_dynamic() {
// 目标对象
IUserService userService = new UserService();

/*
组装代理信息
*/
AdvisedSupport advisedSupport = new AdvisedSupport();
// 填充目标对象。
advisedSupport.setTargetSource(new TargetSource(userService));
// 填充用户自定义拦截器
advisedSupport.setMethodInterceptor(new UserServiceInterceptor());
advisedSupport.setMethodMatcher(new AspectJExpressionPointCut("execution(* com.bantanger.springframework.test.bean.IUserService.*(..))"));

// 代理对象(JdkDynamicAopProxy)
IUserService proxy_jdk = (IUserService) new JdkDynamicAopProxy(advisedSupport).getProxy();
// 测试调用
System.out.println("测试结果:" + proxy_jdk.queryUserInfo());

// 代理对象(Cglib2AopProxy)
IUserService proxy_cglib = (IUserService) new Cglib2AopProxy(advisedSupport).getProxy();
// 测试调用
System.out.println("测试结果:" + proxy_cglib.register("半糖"));
}
  • 先创建出目标对象,将其填充到 advisedSupport 包装类中的 TargetSource。通过这种方式,反馈出 Spring AOP 中的核心:不通过 new 的方式创建一个对象实例,其实本质而言,是 Spring 底层 new 出所需要的 bean
  • 往 advisedSupport 填充用户自定义拦截器以及方法匹配器所需匹配规则
  • 之后调用动态代理的具体实例 JDK、Cglib,通过 getProxy 获取代理对象,本质是基于 Proxy.newProxyInstance。

image-20230302193744276

全流程分析

创建需要代理的对象(目前是使用 new 的方式,之后会加载在 BeanFactory 里)

组装代理信息

  • 目标代理对象
  • 用户自定义拦截器
  • 方法匹配器(传入 AspectJExpressionPointCut 和表达式)

通过 Jdk、Cglib 两种实例化方式创建代理对象。

JDK 方式:

  1. 通过传入的组装代理信息 AdvisedSupport 创建 JDK 代理创建工厂
  2. 调用 getProxy ,内部使用的是 Proxy.newProxyInstance 方式指定 ClassLoader 对象和一组 interface 来创建动态代理对象,返回代理对象
  3. 通过代理对象调用方法,在调用前被 invoke 拦截(其实是通过反射机制获取动态代理类的构造参数)
  4. 方法匹配器判断当前方法是否需要代理,如果需要进入下一步
  5. invoke 方法内部获取组装代理信息 AdvisedSupport 中的用户拦截器
  6. 传入数据结构 ReflectiveMethodInvocation ,提供入参对象 { 目标对象、方法、入参 },调用用户拦截器增强方法

Cglib 方式:

  1. 通过传入的组装代理信息 AdvisedSupport 创建 Cglib 代理创建工厂
  2. 调用 getProxy ,内部使用的是 enhancer.create 方式创建代理对象,返回代理对象
  3. 通过代理对象调用方法,在调用前被 DynamicAdvisedInterceptor#intercept 拦截
  4. 方法匹配器判断当前方法是否需要代理,如果需要进入下一步
  5. invoke 方法内部获取组装代理信息 AdvisedSupport 中的用户拦截器
  6. 传入数据结构 ReflectiveMethodInvocation ,提供入参对象 { 目标对象、方法、入参 },调用用户拦截器增强方法