AOP开发详解

通过上文的术语介绍,我们更加容易理解 AOP。本节先介绍如何使用每一个知识点,然后对每个知识点做详细的介绍,包括源码说明。

连接点与两种代理

在 AOP 中,首先需要知道匹配哪个方法进行增强,才能添加通知,所以确定连接点是第一步。

源码介绍

我们先打开源码,这里是切点的注解,源码如下所示。

package org.springframework.aop;
public interface Pointcut {
   Pointcut TRUE = TruePointcut.INSTANCE;
   ClassFilter getClassFilter();
   MethodMatcher getMethodMatcher();
}

这段源码中有成员变量,是默认的 Pointcut 实例,匹配任何的方法时结果都会返回相同的值。还有两种方法,第一种方法是 getClassFilter,在程序中一个类会被多个代理进行代理,因此 Spring 中会引入责任链模式,在这里返回一个类过滤器;第二种方法是 getMethodMatcher,这个方法返回一个方法匹配器。

方法匹配器返回一个 MethodMatcher,我们关心的是 AOP 如何增强方法,所以在这里看看 MethodMatcher 的源码,代码如下所示。

package org.springframework.aop;
public interface MethodMatcher {
   MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
   boolean matches(Method var1, @Nullable Class<?> var2);
   boolean isRuntime();
   boolean matches(Method var1, @Nullable Class<?> var2, Object... var3);
}

在这个接口中有两个 matches,两个方法的区别在于多了参数 var3,其实这里是定义了两种匹配器,一种为静态匹配器,另一种为动态匹配器。静态匹配器,对方法的名称和入参类型进行一次判断匹配;动态匹配器,因为参数的不同,每次增强方法都会进行判断。

这里还有一种方法 isRuntime,这种方法的返回值 boolean 决定使用静态匹配器还是动态匹配器。返回 true 则使用动态匹配器,反之使用静态匹配器。

实例

在 Spring Boot 中,有两种代理,一种是 JDK 代理,另一种是 cglib 代理。在开始时我们引入了 spring-boot-starter-aop,在引入依赖之后,则默认启动 AOP,如果不想启用 AOP,可以在 application.properties 中使用配置项,即 spring.aop.auto=false。

在代码中,可以使用配置项 spring.aop.proxy-target-class 进行配置,默认为 false,这意味着使用 JDK 代理。但具体使用哪种代理,我们一般要根据代理的类是否实现了接口决定。如果代理的类没有实现接口,就算设置 spring.aop.proxy-target-class 为 false,也照样会使用 cglib 代理。

下面通过示例来说明,程序为5.1小节的程序,这里不再重复。第一次,我们使用 JDK 代理。首先,在 application.properties 中设置配置项,如下所示。

spring.aop.proxy-target-class=false

然后,展示测试类,代码如下所示。

package com.springBoot.aop.test;

@SpringBootApplication
@PropertySource(value = "classpath:application.properties",ignoreResourceNotFound = true)
public class AopTestDemo {
   private static final Logger log=LoggerFactory.getLogger(IocTestDemo. class);
   public static void main(String[] args) {
        ApplicationContext context=new AnnotationConfigApplicationC ontext(MySpringBootConfig.class);
// MyLogPrint myLogPrint=context.getBean(MyLogPrint.class);
// myLogPrint.doPrint();
          String logPrintClassname=context.getBean("myLogPrint"). getClass().getName();
      log.info("logPrintClassname: "+logPrintClassname);
   }
}

在上面的代码中,建议写上 @PropertySource 注解,因为有时默认的 application.properties 没有被加载。最后,查看执行效果。

18:46:25.742 [main] INFO com.springBoot.ioc.test.IocTestDemo logPrintClassname: com.sun.proxy.$Proxy46

从中我们可以看到使用的是 JDK 代理。第二次,我们让程序使用 cglib 代理。首先,我们的配置项继续为 false,但是代理类将不再实现接口,代码如下所示。

package com.springBoot.aop.pojo.impl;
//在这个地方,需要使用@Component进行扫描
@Component("myLogPrint")
public class MyLogPrint {
   public void doPrint() {
      System.out.println("log print");
   }
}

最后,我们的测试类也不修改,直接运行,观察效果,如下所示。

18:51:52.745 [main] INFO com.springBoot.ioc.test.IocTestDemo- logPrintClassname: com.springBoot.aop.pojo.impl.MyLogPrint$$EnhancerBySpringCGLIB$$aeff3e9e

@EnableAspectJAutoProxy

首先看代码,测试类代码如下所示。

package com.springBoot.aop.test;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true,exposeProxy = true)
@PropertySource(value = "classpath:application.properties",ignoreResourceNotFound = true)
public class AopTestDemo {
   private static final Logger log=LoggerFactory.getLogger(IocTestDemo. class);
   public static void main(String[] args) {
      ApplicationContext context=new AnnotationConfigApplicationContext(MySpringBootConfig.class);
      MyLogPrint myLogPrint=context.getBean(MyLogPrint.class);
      myLogPrint.doPrint();
   }
}

横切关注点代码如下所示。

package com.springBoot.aop.pojo.impl;
//在这个地方,需要使用@Component进行扫描
@Component("myLogPrint")
public class MyLogPrint implements LogPrint {
   @Override
   public void doPrint() {
      System.out.println("log print. "+ AopContext.currentProxy(). getClass());
   }
}

这里可以使用 @Enable AspectAutoProxy 注解,表示启动 AOP,并且优先级会高于 application.properties。这里第一个参数表示使用什么代理,第二个参数表示在横切关注点中可以使用 AopContext 这个类,在上面的代码中可以参考使用。执行结果如下所示。

before log
log print. class com.springBoot.aop.pojo.impl.MyLogPrint$$EnhancerBySpringCGLIB$$b583ac19
after log
afterReturning log

切面

有了需要增强的方法,即连接点后,我们需确定如何增强,这时就需要一个切面,描述 AOP 的其他信息。实例代码如所示。

package com.springBoot.aop.aspect;
@Aspect
@Component
public class LogAscpect {
      private Logger logger=LoggerFactory.getLogger(LogAscpect. class);
   //
      @Before("execution( * com.springBoot.aop.pojo.impl.MyLogPrint. doPrint(..))")
   public void before(){
      System.out.println("before log");
   }
      @After("execution( * com.springBoot.aop.pojo.impl.MyLogPrint. doPrint(..))")
   public void after(){
      System.out.println("after log");
   }
      @AfterReturning("execution( * com.springBoot.aop.pojo.impl. MyLogPrint.doPrint(..))")
   public void afterReturning(){
      System.out.println("afterReturning log");
   }
      @AfterThrowing("execution( * com.springBoot.aop.pojo.impl. MyLogPrint.doPrint(..))")
   public void afterThrowing(){
      System.out.println("afterThrowing log");
   }
}

在切面的开发中,可分为两个步骤,第一步定义一个切面,第二步在切面中对切点进行增强。

(1)定义切面。在定义切面时,需要两个注解,缺一不可。首先,我们需要 @Aspect 说明注解的类是一个切面,这样一个切面就注册完成;然后需要将切面交给 Spring 的 IOC 容器进行管理,所以就需要 @Component 注解。

(2)增强切点。在切面中,我们需要对切点做一些增强操作:前置增强、环绕增强、后置增强等。

切点

根据前面的术语介绍,我们知道切点就是一些连接点的集合。这里主要讲解两个知识点:其一,在每个注解上,都写了匹配的正则表达式,看起来比较复杂,我们通过 @Pointcut 注解进行简化;其二,关于切点声明,一个切点声明有两个重要部分:签名(包含名字和参数)、切点表达式。

@Pointcut

具体的使用方式,看下面的代码。

package com.springBoot.aop.aspect;
@Aspect
@Component
public class LogAscpect {
      private Logger logger=LoggerFactory.getLogger(LogAscpect. class);
   @Pointcut("execution( * com.springBoot.aop.pojo.impl.MyLogPrint. doPrint(..))")
   public void pointCut(){
   }
   @Before("pointCut()")
   public void before(){
      System.out.println("before log");
   }
   @After("pointCut()")
   public void after(){
      System.out.println("after log");
   }
   @AfterReturning("pointCut()")
   public void afterReturning(){
      System.out.println("afterReturning log");
   }
   @AfterThrowing("pointCut()")
   public void afterThrowing(){
      System.out.println("afterThrowing log");
   }
}

在上面的代码中,使用 @Pointcut 注解在 pointCut 方法上进行标注,然后在后面的通知注解上使用被注解的方法即可。这种方式可以不用重复写相同的代码,而且比较清晰。

注意:切点签名的方法需要返回 void 类型。

切点表达式

切点表达式的语法:execution([可见性] 返回类型 [声明类型].方法名(参数)[异常])。常见通配符和运算符如下。

  • *:匹配所有的字符。

  • ..:匹配多个参数。

  • +:匹配类及其子类。

  • &&、||、!:运算符。

指示符包含以下几种。

execution:匹配执行连接点指示符,是最常使用的指示符,使用方式如下所示。

@Before("execution( * com.springBoot.aop.pojo..*.*(..))")
public void before(){
   System.out.println("before log");
}

这段代码意思是匹配 com.springBoot.aop.pojo 包及其子包所有类中的所有方法,返回是任意类型,方法的参数也是任意的。

within:匹配连接点的类或者包,使用方式如下所示。

@Before("within(com.springBoot.aop.pojo.impl.MyLogPrint)")
public void before(){
   System.out.println("before log");
}

上面的这段代码是匹配 MyLogPrint 类中的所有方法。又例如以下代码。

@Before("within(com.springBoot.aop.pojo..*)")
public void before(){
   System.out.println("before log");
}

上面的代码是匹配 pojo 包以及子包的所有方法。

this:向通知方法传入代理对象,使用方式如下所示。

@Before("before() && this(proxy)")
public void beforeAdvise(JoinPoint point,Object proxy){
   System.out.println("before log");
}

target:向通知方法中传入目标对象,使用方式如下所示。

@Before("before() && target(target)")
   public void beforeAdvise(JoinPoint point,Object proxy){
      System.out.println("before log");
   }

args:根据目标方法的参数进行匹配,使用方式如下所示。

@Before("args(username)")
public void beforeAdvise(JoinPoint point,String username){
   System.out.println("before log");
}

上面的代码是有且仅有一个 String 类型的参数的方法。@within:在类上的匹配注解类型,使用方式如下。

@Before("@within(com.springBoot.aop.pojo.AdviceAnnotation)")
   public void beforeAdvise(JoinPoint point,String username){
      System.out.println("before log");
   }

上面的代码,被 AdviceAnnotation 标注的类都将会被匹配。

n():限定带有指定注解的连接点,使用方式如下。

@annotation("org.springframework.transaction.annotation. Transactional")
   public void beforeAdvise(JoinPoint point,String username){
      System.out.println("before log");
   }

上面的程序是匹配标注了 @Transactional 注解的方法。

多切面与@Order

前文讲解了单个切面的使用,如果是多个切面,该如何使用?同时,多个切面的顺序如何确定?下面通过实例来展示多个切面的使用方式,程序结构如图5.3所示。

image 2024 03 31 16 52 51 778
Figure 1. 图5.3 程序结构示意图

在 aspect 包中主要有 4 个类,我们先将 LogAspect 类中的 @Component 注解注释掉,暂时不使用这个类进行试验。

多切面的使用

我们先看 LogAspect1 中的程序,代码如下所示。

package com.springBoot.aop.aspect;
@Aspect
@Component
public class LogAscpect1 {
      private Logger logger=LoggerFactory.getLogger(LogAscpect1. class);
   @Pointcut("execution( * com.springBoot.aop.pojo.impl.MyLogPrint. doPrint(..))")
   public void pointCut(){
   }
   @Before("pointCut()")
   public void before(){
      System.out.println("before log 1");
   }
   @After("pointCut()")
   public void after(){
      System.out.println("after log 1");
   }
}

然后,继续复制两份上文代码,并以不同的类名命名,在注解的方法中,稍微有些改动即可。为了方便比较,再展示 LogAspect2 的代码。

package com.springBoot.aop.aspect;
@Aspect
@Component
public class LogAscpect2 {
      private Logger logger=LoggerFactory.getLogger(LogAscpect2. class);
   @Pointcut("execution( * com.springBoot.aop.pojo.impl.MyLogPrint. doPrint(..))")
   public void pointCut(){
   }
   @Before("pointCut()")
   public void before(){
      System.out.println("before log 2");
   }
   @After("pointCut()")
   public void after(){
      System.out.println("after log 2");
   }
}

我们来看执行的效果,如下所示。

before log 1
before log 2
before log 3
log print. class com.springBoot.aop.pojo.impl.MyLogPrint$$EnhancerBySpringCGLIB$$8b00720a
after log 3
after log 2
after log 1

@Order

上文的执行顺序怎样?如果这个执行的顺序不是我们想要的,是否可以自定义切面的顺序?这时可以使用 @Order 进行注解,使用方式如下所示。

package com.springBoot.aop.aspect;
@Aspect
@Component
@Order(3)
public class LogAscpec {
}

为了方便对比,我们把 MyLogPrint1 上的注解顺序设置为 3,MyLogPrint2 的注解顺序设置为 2,MyLogPrint3 上的注解顺序设置为 1,其他的就不再展示,自己修改即可。代码如下所示。

package com.springBoot.aop.aspect;
@Aspect
@Component
@Order(1)
public class LogAscpect3 {
      private Logger logger=LoggerFactory.getLogger(LogAscpect3. class);
   @Pointcut("execution( * com.springBoot.aop.pojo.impl.MyLogPrint. doPrint(..))")
   public void pointCut(){
   }
   @Before("pointCut()")
   public void before(){
      System.out.println("before log 3");
   }
   @After("pointCut()")
   public void after(){
      System.out.println("after log 3");
   }
}

测试类程序不变。最后,执行结果如下所示。

before log 3
before log 2
before log 1
Disconnected from the target VM, address: '127.0.0.1:36441', transport: 'socket'
log print. class com.springBoot.aop.pojo.impl.MyLogPrint$$EnhancerB ySpringCGLIB$$96f34259
after log 1
after log 2
after log 3