AOP 要点、原理及问题记录

10 minute

AOP 简介

AOP, 即 “Aspect-oriented Programming”,就是面向切面编程,AOP 将日志记录,性能统计,异常处理等代码从业务逻辑代码中划分出来,对这些行为的分离就是创建一个切面,在切面对这些功能编程,将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

Spring 和 AspectJ 是两个广泛使用的 AOP 实现方案。

AOP 术语

AOP 术语主要包括通知(Advice)、连接点(Join point)、切点(Pointcut)、切面(Aspect)、引入(Introduction)和织入(Weaving)。

连接点是在应用执行过程中能够插入切面的一个点。由于一个切面并不需要通知应用的所有连接点,所以有了切点。

切点定义了需要在哪些连接点上执行通知,如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。

通知定义了切面的工作,说明切面是什么以及何时使用。如下是五种通知类型:

  • 前置通知(Before):在目标方法被调用之前调用通知功能;
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么,不管方法是否有异常,都会执行该通知;
  • 返回通知(After-returning):在目标方法成功执行之后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为。

切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和在何处完成其功能。

引入允许我们向现有的类添加新方法或属性。

织入即是引入的一种方式,它是把切面应用到目标对象并创建新的代理对象的过程。

AOP 织入时机

在 Java 平台上,有三个 AOP 织入时机:

  1. 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ 就扩展了 Java 编译器,使用关键字 aspect 来实现织入;
  2. 类加载器:在目标类被装载到 JVM 时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
  3. 运行期:目标对象和切面都是普通 Java 类,通过 JVM 的动态代理功能或者第三方库实现运行期动态织入,Spring AOP 即是这样实现。

Spring 可以集成 AspectJ,可以在 Spring 项目中同时使用 Spring AOP 和 AspectJ AOP。通过配置,可以将 AspectJ 切面应用于 Spring 容器管理的 Bean,实现更丰富的 AOP 功能。

注意 Spring AOP 只作用于 Spring 所管理的 Bean,所以调用自己 new 出来的对象的方法不会被代理,即使 Spring 集成 AspectJ 使用静态代理模式,代理的也是 Spring 所管理的 Bean。而单纯使用 AspectJ 则没有这个问题。

AOP 中的动态代理模式

Spring AOP 是通过动态代理实现的,但是什么是动态代理?所以需要进一步学习。

代理模式是一种设计模式,给某一个对象提供一个代理,由代理对象控制原对象的引用,代理对象在客户端和目标对象之间起到中介作用。代理模式可以分为静态代理和动态代理两种类型,而动态代理中又分为 JDK 动态代理和 CGLIB 动态代理两种。

JDK 动态代理

在 JDK 动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),通过动态代理类需要实现 InvocationHandler 接口,重写 invoke 方法定义切面逻辑。通过 Proxy 静态方法 Proxy.newProxyInstance() 完成织入操作。

JDK 动态代理可分为以下几步:

  1. 拿到被代理对象的引用,并且通过反射获取到它的所有的接口;
  2. 通过 JDK Proxy 类重新生成一个新的类,同时新的类要实现被代理类所实现的所有的接口;
  3. 动态生成 Java 代码,把新加的业务逻辑方法由一定的逻辑代码去调用;
  4. 编译新生成的 Java 代码 .class;
  5. 将新生成的 Class 文件重新加载到 JVM 中运行。

JDK 动态代理的核心是通过重写被代理对象所实现的接口中的方法来重新生成代理类来实现的,那么假如被代理对象没有实现接口呢?那么这时候就需要 CGLIB 动态代理了。

CGLIB 动态代理

CGLIB 通过继承被代理对象来实现动态代理,和 JDK 动态代理需要实现指定接口一样,CGLIB 也要求代理对象必须要实现 MethodInterceptor 接口,并重写其唯一的方法 intercept 定义切面逻辑。通过 Enhancer 的 create 方法来完成织入操作。

CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。具体来说,它利用 ASM 开源包,对代理对象类的 class 文件加载进来,通过修改其字节码生成子类来处理。

因为 CGLIB 是通过继承目标类来重写其方法来实现的,故而如果是 final 和 private 方法则无法被重写,也就是无法被代理。

Spring AOP 个人使用问题记录

拦截同对象嵌套方法

Spring AOP 中,对于某个对象 Obj 中的某个方法 A,如果方法 A 内部嵌套调用了 Obj 的其它方法 B,那么方法 B 的切面逻辑不会被执行。这是因为方法嵌套调用时是调用的当前对象而非代理对象,也就是说实际调用方式为 this.B()

可以通过 AopContext 调用嵌套方法,即将嵌套方法发射到代理对象进行切面增强:

1void A() {
2    // B(); // NOT WORK
3    ((Obj) AopContext.currentProxy()).B();
4}

同时需要在 context 中配置 aop 的 expose-proxy 属性为 true:

1<aop:aspectj-autoproxy expose-proxy="true"/>

避免切点覆盖

一个地方对应一个切点,多个切点将相互覆盖,可通过切点表达式排除其他切点进行控制。比如为了让 execExportTask 方法执行 aroundTask 通知而不是 around 通知:

 1@Around(value = "execution(* xxx.impl..*.*(..)) && !execution(* xxx.impl.ApproveExportServiceImpl.execExportTask(..)) && args(arg)")
 2public Object around(ProceedingJoinPoint pjp, Object arg) throws Throwable {
 3    return innerAround(pjp, arg);
 4}
 5
 6@Around(value = "execution(* xxx.impl.ApproveExportServiceImpl.execExportTask(..)) && args(arg)")
 7public Object aroundTask(ProceedingJoinPoint pjp, Object arg) throws Throwable {
 8    System.out.println(arg);
 9    return pjp.proceed();
10}

另外也可以通过 @Order 注解指定切点类之间的优先级进行控制。

重要文章