AOP 进阶

AOP 进阶

AOP 主要分为 3 个部分:

  1. 通知类型
  2. 通知顺序
  3. 切入点表达式

1. 通知类型

类型说明
@Around环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before前置通知,此注解标注的通知方法在目标方法前被执行
@After最终通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing异常后通知,此注解标注的通知方法发生异常后执行
@Slf4j
@Component
@Aspect
public class MyAspect1 {
  // 环绕通知
  @Around("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
  public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    log.info("around before ...");

    // 调用目标对象的原始方法执行
    Object result = proceedingJoinPoint.proceed();

    // 原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了

    log.info("around after ...");
    return result;
  }

  // 前置通知
  @Before("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
  public void before(JoinPoint joinPoint){
    log.info("before ...");

  }

  // 最终通知
  @After("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
  public void after(JoinPoint joinPoint){
    log.info("after ...");
  }

  // 返回后通知(程序在正常执行的情况下,会执行的后置通知)
  @AfterReturning("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
  public void afterReturning(JoinPoint joinPoint){
    log.info("afterReturning ...");
  }

  // 异常通知(程序在出现异常的情况下,执行的后置通知)
  @AfterThrowing("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
  public void afterThrowing(JoinPoint joinPoint){
    log.info("afterThrowing ...");
  }
}

程序发生异常的情况下:

  1. @AfterReturning 标识的通知方法不会执行,@AfterThrowing 标识的通知方法执行了
  2. @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)

在使用通知时的注意事项:

  1. @Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
  2. @Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。

1.1 代码优化

由于可能会为一个类、方法等设置多种通知类型,如同上述代码一样,每一个注解都使用一样的切面表达式,此时代码中存在大量重复性的代码,此时就可以使用 Spring 提供的 @PointCut 注解对代码进行优化。

@Slf4j
@Component
@Aspect
public class MyAspect1 {
  // 切入点方法(公共的切入点表达式)
  @Pointcut("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
  public void pt(){}

  // 环绕通知
  @Around("pt()") // 使用公共切面表达式
  public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    log.info("around before ...");

    // ...
  }

  // 前置通知
  @Before("pt()") // 使用公共切面表达式
  public void before(JoinPoint joinPoint){
    log.info("before ...");

  }
  // ...
}

需要注意的是,切入点方法的使用范围受其修饰符的影响。

2. 通知顺序

当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。

此时我们就有一个疑问,这多个通知方法到底哪个先运行,哪个后运行?

默认按照切面类的类名字母排序:

  1. 目标方法前的通知方法:字母排名靠前的先执行
  2. 目标方法后的通知方法:字母排名靠前的后执行

如果想要手动控制其执行顺序,可以使用以下两种方式:

  1. 修改切面类的类名(这种方式非常繁琐、而且不便管理)
  2. 使用 Spring 提供的 @Order 注解

这里主要说明以下以注解的方式修改其顺序。

使用 @Order 注解,控制通知的执行顺序:

@Slf4j
@Component
@Aspect
@Order(2)  //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
  @Pointcut("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
  public void pt(){}

  //前置通知
  @Before("pt()")
  public void before(){
    log.info("MyAspect2 -> before ...");
  }

  //后置通知 
  @After("pt()")
  public void after(){
    log.info("MyAspect2 -> after ...");
  }
}
@Slf4j
@Component
@Aspect
@Order(3)  //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect3 {
  //前置通知
  @Before("pt()") // 由于切入点方法是使用 public 修饰,可以在同一个包下共用
  public void before(){
    log.info("MyAspect3 -> before ...");
  }

  //后置通知
  @After("pt()")
  public void after(){
    log.info("MyAspect3 ->  after ...");
  }
}

重启 SpringBoot 项目后,无论这么修改数字的大小,可以发现数字小的一定先执行,因此可以得出结论:

  1. 前置通知:数字越小先执行;
  2. 后置通知:数字越小越后执行

3. 切入点表达式

前面代码我们一直在使用切入点表达式,那什么又是切入点表达式哪?

切入点表达式:描述切入点方法的一种表达式

  • 作用:主要用来决定项目中的哪些方法需要加入通知
  • 常见形式:
    1. execution(...):根据方法的签名来匹配
    2. @annotation(...):根据注解匹配

3.1 execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

其中带 ? 的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)
  • 包名.类名: 可省略
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

可以使用通配符描述切入点:

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

举例说明使用通配符描述切入点:

  1. 省略方法的修饰符号

    execution(void com.tlias.service.impl.EmpServiceImpl.delete(java.lang.Integer))
    

    完整写法,通常用于指定一个具体的方法

  2. 使用 * 代替返回值类型

    execution(* com.tlias.service.impl.EmpServiceImpl.delete(java.lang.Integer))
    

    表示任意返回值的方法

  3. 使用 * 代替包名(一层包使用一个 *

    execution(* com.tlias.*.*.EmpServiceImpl.delete(java.lang.Integer))
    

    表示任意返回值、com.tlias 包下的任意两层包下的 EmpServiceImpl 类下的方法

  4. 使用 .. 省略包名

    execution(* com..EmpServiceImpl.delete(java.lang.Integer))  
    

    表示任意返回值、com 包下的任意层包下的 EmpServiceImpl 类下的方法

  5. 使用 * 代替类名

    execution(* com..*.delete(java.lang.Integer))
    

    表示任意返回值、com 包下的任意层包下的任意类中的方法

  6. 使用 * 代替方法名

    execution(* com..*.*(java.lang.Integer))
    

    表示任意返回值、com 包下的任意层包下的任意类中的任意的方法

  7. 使用 * 代替参数

    execution(* com.tlias.service.impl.EmpServiceImpl.delete(*))
    

    表示任意返回值且有一个参数的指定包下的方法

  8. 使用 .. 省略参数

    execution(* com..*.*(..))
    

    表示任意返回值、com 包下的任意层包下的任意类中的任意且有 0 个或多个参数的方法(基本上不会使用)

注意事项:

  1. 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。

  2. 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update 开头

  3. 匹配 EmpServiceImpl 类中以 find 开头的方法

    execution(* com.tlias.service.impl.EmpServiceImpl.find*(..))
    
  4. 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性

  5. 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包

3.2 @annotation

实现步骤:

  1. 编写自定义注解
  2. 在业务类要做为连接点的方法上添加自定义注解

自定义注解 LogOperation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{ }

在需要标注的方法上使用自定义注解:

@Override
@LogOperation // 自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
  List<Dept> deptList = deptMapper.list();
  return deptList;
}
@Override
@LogOperation // 自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
  //1. 删除部门
  deptMapper.delete(id);
}

配置切面类

@Slf4j
@Component
@Aspect
public class MyAspect6 {
  //前置通知
  @Before("@annotation(com.itheima.anno.LogOperation)")
  public void before(){
    log.info("MyAspect6 -> before ...");
  }

  //后置通知
  @After("@annotation(com.itheima.anno.LogOperation)")
  public void after(){
    log.info("MyAspect6 -> after ...");
  }
}

3.3 总结

常见的切入点表达式:

  1. execution 切入点表达式
    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过 execution 切入点表达式描述比较繁琐
  2. annotation 切入点表达式
    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

根据业务需要,可以使用 &&||! 来组合比较复杂的切入点表达式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值