Spring Boot「45」扩展:面向切片编程

201 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 30 天,点击查看活动详情

Spring 框架提供的功能,除了 IoC 容器外,另外一个重要的特性就是 AOP 编程。今天我将带着大家一块总结下 AOP 中的相关概念,以及 Spring AOP 中的基本机制。

01-概念/术语

AOP(面向切面编程)是OOP的补充,为开发者组织程序结构提供了全新的视角。OOP 中的基本模块单元是类,AOP 中的基本单元是切面(Aspect)。

  • Aspect(切面),一个可能跨越多个类的关注点,事务管理就是一个很典型的切面。
  • JoinPoint(连接点),程序运行过程中,能够插入切面的地点,例如方法调用、异常处理、字段修改等。Spring AOP中,仅支持方法级的连接点。
  • Pointcut(切入点),”A predicate that matches join points”,用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精确的匹配一般通过切入点的正则表达式来定义。
  • Advice(通知),切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)和环绕通知(Around)5种。Spring AOP中,通知被实现为拦截器,且针对某个连接点,维护了一个通知拦截器列表。
  • Introduction(引入),为某个类型声明新的方法或属性。例如,通过引入使某个Bean实现IsModified接口。在AspectJ中,引入也被称为类型间声明。
  • TargetObject(目标对象,target),被一个或多个切面通知的对象,因此也常称为“被通知对象”(AdvisedObject)。Spring AOP通过运行时代理实现,所以目标对象是代理对象。
  • AOP Proxy(AOP代理,this),为了实现切面,由AOP框架创建的对象。将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象的核心业务逻辑加上切面提供的共有功能。代理对象对使用者而言是透明的,是程序运行过程中的产物。在Spring AOP中,AOP代理要么通过JDK动态代理实现,要么通过CGLIB代理实现。
  • Weaving(编织),将切面与目标对象关联起来生成新的对象的过程。编织可以发生在编译时、加载时或运行时。Spring AOP的编织发生在运行时。

[1] Aspect Oriented Programming with Spring

02-Spring AOP

AOP is used in the Spring Framework to: Provide declarative enterprise services. The most important such service is declarative transaction management. Let users implement custom aspects, complementing their use of OOP with AOP.

02.1-代理机制

Spring AOP代理通过JDK动态代理或者CGLIB实现。如果目标对象实现了至少一个接口,则使用JDK动态代理;否则,使用CGLIB。使用后者时,要注意如下问题:

  1. CGLIB中,final修饰的方法无法被通知到,原因是无法被重载。
  2. 从Spring 4.0开始,目标对象的构造器不会再被调用两次。

Spring AOP是基于代理实现的,使用方式如下Main类所示:

public class SimplePojo implements Pojo {

    public void foo() {
				
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

image.png图1. 不通过代理

image.png图2. 通过代理

public class Main {
    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}

当调用pojo.foo()时,实际上先调用的是代理中的foo()方法。这样存在一个问题,就是foo()方法中对this.bar()的调用并不会通过代理(切面无法通知),调用的还是目标对象的方法。解决这个问题,有两种方式:

  • 修改代码,不使用this.bar()这种方式。不过,比较费力。

  • 使用Spring框架代码,缺点是与Spring AOP强耦合,非常不建议使用

    ((Pojo) AopContext.currentProxy()).bar();
    

AspectJ不存在这个问题,因为其不是基于代理实现的。

[1] Cglib和jdk动态代理的区别

[2] 动态代理的实现; JDK Proxy 和 CGLib 的区别

[3] 使用HSDB查看动态生成代理类内容

02.2-@AspectJ

@Aspect是一种切面定义风格,是指通过带注解的普通Java类来声明(定义)切面。这种风格由AspectJ 5引入。Spring AOP使用AspectJ提供的jar包来解析和匹配切入点,但AOP运行时仍然是纯Spring AOP,并不需要依赖AspecctJ来编译或织入。

  1. 开启@AspectJ支持。确保类路径中包含aspectjweaver-*.jar包,在配置类上添加注解@EnableAspectJAutoProxy

  2. 定义切面。带有@Aspect注解的Bean,托管在容器中会被自动找到。注意,@Aspect并不具备@Component的功能,如果需要Spring IoC容器创建实例,需要额外的标注具备@Component功能的注解。

  3. 定义切入点。包含两部分内容:签名(名和参数)和切入点表达式(确定了一组连接点)。

    1. 签名,@AspectJ中通过方法定义实现,方法的返回值必须为void。
    2. 切入点表达式,@AspectJ中通过@Pointcut注解实现。关于表达式更详细的信息,请参考[1,2,3]。
    3. Spring AOP支持的符号(PCD):execution/within/this/target/args/@target/@args/@within/@annotation,除此之外,AspectJ还支持其他的符号,例如call/get等。但这些符号在Spring AOP中使用,会抛IllegalArgumentException异常。
    4. Spring AOP还支持一个特殊的PCD,bean。用来指定切入点到某个或某类特定名称的Bean上,支持通配符*,和&& || !
  4. 定义通知。与某个切入点相关联,在匹配的方法执行之前、之后、或前后执行。

    1. @Before, runs before a matched method execution.

    2. @AfterReturning, runs when a matched method execution returns normally. returning可以拿到返回值,并会限制匹配满足返回类型的方法。例如,下述方法仅会匹配返回值为Object类型的方法:

      @Aspect
      public class AfterReturningExample {
      
          @AfterReturning(
              pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
              returning="retVal")
          public void doAccessCheck(Object retVal) {
              // ...
          }
      }
      
    3. @AfterThrowing, runs when a matched method execution exits by throwing an exception. throwing可以指定抛异常的类型,与returning有类似的效果。

    4. @After, runs after a matched method execution.

    5. @Around, runs “around” a matched method’s execution.第一个参数必须时ProceedingJoinPoint对象,调用其proceed方法,可以致使底层方法执行。

      Always use the least powerful form of advice that meets your requirements (that is, do not use around advice if before advice would do).

      @Aspect
      public class AroundExample {
      
          @Around("com.xyz.myapp.CommonPointcuts.businessService()")
          public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
              // start stopwatch
              Object retVal = pjp.proceed();
              // stop stopwatch
              return retVal;
          }
      }
      

      返回值是底层方法的返回值,可以被调用者拿到。

    6. 通知排序。

      The highest precedence advice runs first "on the way in”. "On the way out" from a join point, the highest precedence advice runs last.

      除非特殊指定,否则两个切面中定义的不同通知的执行顺序是未定义的。可通过实现Ordered接口或5注解定义优先级,返回值越小的优先级越高。

  5. Introduction(引入)。AspectJ中称为“inter-type declaration”。为被通知对象引入新的接口实现。

    1. @DeclareParents, to declare that matching types have a new parent.
  6. 切面实例化模型。默认情况下,每个应用上下文中,一个切面仅有一个实例。AspectJ称其为单例实例化模型。不过,AspectJ仍然预留了修改切面实例生命周期的接口。Spring AOP目前支持perthis/pertarget,尚不支持percflow/percflowbelow/pertypewithin

    1. perthis,为每个执行特定业务逻辑的服务对象创建一个切面实例。

03-总结

希望今天的内容能够帮助到你。