Spring 知识 -- Spring AOP

本文最后更新于:4 个月前

相关概念

Aop是什么

与OOP对比,面向切面,传统的OOP开发中的代码逻辑是自上而下的,而这些过程会产生一些横切性问题,这些横切性的问题和我们的主业务逻辑关系不大,这些横切性问题不会影响到主逻辑实现的,但是会散落到代码的各个部分,难以维护。AOP是处理一些横切性问题,AOP的编程思想就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。使代码的重用性和开发效率更高。

好处

对比oop,aop 能够将横切性的问题和业务逻辑解偶,减少模板代码,提高可维护性和开发效率

使用场景

  1. 日志记录
  2. 权限验证
  3. 效率检查
  4. 事务管理
  5. exception

AOP 底层技术

Spring 默认使用 JDK 动态代理,也可以使用 CGLib,二者都是动态代理,在运行时初始化时期进行织入。

博客

  • 区别

  • JDK 动态代理是通过接口反射得到 byte 数组,存放代理类的字节码,然后通过 native 方法把字节码转化成 class 对象。

Spring AOP 和 AspectJ

AOP 是一种概念,Spring AOP 和 AspectJ,Javassist 等都是具体实现。

Spring 仅仅是借用 AspectJ 的注解语法而已,Spring 使用 JDK 动态代理或者 CGLib 动态代理,是结合 Spring IOC 容器的动态织入;而 AspectJ 是一个 AOP 框架,是使用静态织入的方式。

博客

术语解释

  1. aspect:切面,一定要给spring去管理

    切面抽象为类

  2. pointcut:切点表示连接点的集合

    PointCut是JoinPoint的谓语,这是一个动作,主要是告诉通知连接点在哪里,切点表达式决定 JoinPoint 的数量

  3. Joinpoint:连接点

    目标对象中的方法
    JoinPoint是要关注和增强的方法,也就是我们要作用的点

  4. Weaving :织入

    把代理逻辑加入到目标对象上的过程叫做织入

  5. target 目标对象,原始对象

  6. aop Proxy 代理对象,包含了原始对象的代码和增加后的代码的那个对象

  7. advice:通知

    增加的逻辑和加入到原方法的位置

    1. advice通知类型:

    2. Before:连接点执行之前,但是无法阻止连接点的正常执行,除非该段执行抛出异常

    3. After:连接点正常执行之后,执行过程中正常执行返回退出,非异常退出

    4. After throwing:执行抛出异常的时候

    5. After (finally):无论连接点是正常退出还是异常退出,都会执行

    6. Around advice:围绕连接点执行,例如方法调用。

      这是最有用的切面方式。around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续加入点还是通过返回自己的返回值或抛出异常来快速建议的方法执行。

    7. ProceedingJoinPoint 和JoinPoint的区别:

      ProceedingJoinPoint 继承了JoinPoint,proceed()这个是aop代理链执行的方法。并扩充实现了proceed()方法,用于继续执行连接点。JoinPoint仅能获取相关参数,无法执行连接点。

    8. JoinPoint的方法

      1. java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;

      2. Signature getSignature() :获取连接点的方法签名对象;

      3. java.lang.Object getTarget() :获取连接点所在的目标对象;

      4. java.lang.Object getThis() :获取代理对象本身;

      5. proceed()有重载,有个带参数的方法,可以在 AOP 的实现逻辑中修改目标方法的的参数,再通过 proceed(Object[] object) 方法传入修改的参数。

Spring AOP 使用步骤

基本步骤

开启 @AspectJ 注解支持

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

声明切面 aspect

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class NotVeryUsefulAspect {

}

声明切点 pointcut

@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature

声明通知 advice

例如:声明一个 before advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

Introductions 引入

Introductions (known as inter-type declarations in AspectJ) enable an aspect to declare that advised objects implement a given interface, and to provide an implementation of that interface on behalf of those objects.

引入(在AspectJ中称为类型间声明)使切面可以声明通知对象(目标对象)实现给定的接口,并且使切面提供该接口的实现给那些对象。

@Aspect
public class UsageTracking {

    // 使用这个注解使得 service 包下面所有的类都加入了 TestDao 接口的方法声明,并且加入了实现类 TestDaoImpl 中的逻辑
    // 相当于加入了父类扩展
    @DeclareParents(value="com.xzy.myapp.service.*+",
    defaultImpl=TestDaoImpl.class)
    public static TestDao dao;
}

Aspect Instantiation Models 切面实例化模型

By default, there is a single instance of each aspect within the application context. AspectJ calls this the singleton instantiation model. It is possible to define aspects with alternate lifecycles. Spring supports AspectJ’s perthis and pertarget instantiation models; percflow, percflowbelow, and pertypewithin are not currently supported.

默认情况下,应用程序上下文中每个切面都有一个实例。AspectJ将此称为单例实例化模型。也可以使用备用生命周期来定义方面。Spring支持AspectJ的perthis和pertarget实例化模型;目前不支持percflow,percflowbelow和pertypewithin。

  • 要求:

    1. AspectJ对象的注入类型为prototype
    2. 目标对象也必须是prototype的

    原因为:只有目标对象是原型模式的,每次getBean得到的对象才是不一样的,由此针对每个对象就会产生新的切面对象,才能产生不同的切面结果。

  • 场景:

    不常用,当有共享变量的时候,并且目标对象是 prototype 每次需要产生不同切面时使用。

     // 使用方式如下:
    @Aspect("perthis(this(com.chenss.dao.IndexDaoImpl))")

支持的 Pointcut 类型

  1. execution: For matching method execution join points. This is the primary pointcut designator to use when working with Spring AOP.

    用于匹配方法执行 join points连接点,最小粒度方法,在aop中主要使用。

    格式: execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

    这里问号表示当前项可以有也可以没有,其中各项的语义如下

    modifiers-pattern:方法的可见性,如public,protected;
    ret-type-pattern:方法的返回值类型,如int,void等;
    declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
    name-pattern:方法名类型,如buisinessService();
    param-pattern:方法的参数类型,如java.lang.String;
    throws-pattern:方法抛出的异常类型,如java.lang.Exception;

    • example:

      @Pointcut("execution(* com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和类的任意方法
      
      @Pointcut("execution(public * com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和类的public方法
      
      @Pointcut("execution(public * com.chenss.dao.*.*())")//匹配com.chenss.dao包下的任意接口和类的public 无方法参数的方法
      
      @Pointcut("execution(* com.chenss.dao.*.*(java.lang.String, ..))")//匹配com.chenss.dao包下的任意接口和类的第一个参数为String类型的方法
      
      @Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
      
      @Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
      
      @Pointcut("execution(public * *(..))")//匹配任意的public方法
      
      @Pointcut("execution(* te*(..))")//匹配任意的以te开头的方法
      
      @Pointcut("execution(* com.chenss.dao.IndexDao.*(..))")//匹配com.chenss.dao.IndexDao接口中任意的方法
      
      @Pointcut("execution(* com.chenss.dao..*.*(..))")//匹配com.chenss.dao包及其子包中任意的方法

      https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-pointcuts-examples

  2. within: Limits matching to join points within certain types (the execution of a method declared within a matching type when using Spring AOP).

    表达式的最小粒度为类

    // within与execution相比,粒度更大,仅能实现到包和接口、类级别。而execution可以精确到方法的返回值,参数个数、修饰符、参数类型等
    @Pointcut("within(com.chenss.dao.*)")//匹配com.chenss.dao包中的任意方法
    @Pointcut("within(com.chenss.dao..*)")//匹配com.chenss.dao包及其子包中的任意方法
  3. this: Limits matching to join points (the execution of methods when using Spring AOP) where the bean reference (Spring AOP proxy) is an instance of the given type.

    表示代理对象

    使用 JDK 动态代理时,指向接口和继承的父类proxy
    使用 CGLib 动态代理时,指向接口和子类(没有继承proxy)

  4. target: Limits matching to join points (the execution of methods when using Spring AOP) where the target object (application object being proxied) is an instance of the given type.

    指向接口和子类
    表示目标对象

    
    /* 此处需要注意的是,如果配置设置proxyTargetClass=false,或默认为false,则是用JDK代理,否则使用的是CGLIB代理
    * JDK代理的实现方式是基于接口实现,代理类继承Proxy,实现接口。
    * 而CGLIB继承被代理的类来实现。
    * 所以使用target会保证目标不变,关联对象不会受到这个设置的影响。
    * 但是使用this对象时,会根据该选项的设置,判断是否能找到对象。
    */
    @Pointcut("target(com.chenss.dao.IndexDaoImpl)")//目标对象,也就是被代理的对象。限制目标对象为com.chenss.dao.IndexDaoImpl类
    @Pointcut("this(com.chenss.dao.IndexDaoImpl)")//当前对象,也就是代理对象,代理对象时通过代理目标对象的方式获取新的对象,与原值并非一个
    @Pointcut("@target(com.chenss.anno.Chenss)")//具有@Chenss的目标对象中的任意方法
    @Pointcut("@within(com.chenss.anno.Chenss)")//等同于@target
  5. args: Limits matching to join points (the execution of methods when using Spring AOP) where the arguments are instances of the given types.

    args表达式的作用是匹配指定参数类型和指定参数数量的方法,与包名和类名无关

    /**
    * args同execution不同的地方在于:
    * args匹配的是运行时传递给方法的参数类型
    * execution(* *(java.io.Serializable))匹配的是方法在声明时指定的方法参数类型。
    */
    @Pointcut("args(java.io.Serializable)")//匹配运行时传递的参数类型为指定类型的、且参数个数和顺序匹配
    @Pointcut("@args(com.chenss.anno.Chenss)")//接受一个参数,并且传递的参数的运行时类型具有@Classified
  6. @target: Limits matching to join points (the execution of methods when using Spring AOP) where the class of the executing object has an annotation of the given type.

    @target(里面是一个注解类xx,表示所有加了xx注解的类,和包名无关)

  7. @args: Limits matching to join points (the execution of methods when using Spring AOP) where the runtime type of the actual arguments passed have annotations of the given types.

  8. @within: Limits matching to join points within types that have the given annotation (the execution of methods declared in types with the given annotation when using Spring AOP).

  9. @annotation: Limits matching to join points where the subject of the join point (the method being run in Spring AOP) has the given annotation.

其他注意要点

通知注解执行顺序

  • 正常情况执行执行顺序:

  • 异常情况:

声明式事务 @Transactional 注解

声明式事务和普通 AOP 有些区别,后者对于代理逻辑的增强可以是在目标方法调用之前(@Before),或者目标方法调用之后(@After),或者目标方法的前后(@Around),而对于声明式事务,则是在目标方法的里面某一段或者某些部分加入代理逻辑。