本篇文章旨在整理个人对SpringAOP模块的一个整体掌握。主要会围绕AOP的基本概念、专业术语、使用方法、使用特性等方面进行展开介绍。

基本概念

OOP(面向对象编程):OOP的关注点是对象,主要考虑的是对象的属性和行为
AOP(面向切面编程):AOP是OOP的一个补充,关注点是那些业务无关但是多个类共同拥有的行为逻辑,AOP主要作用就是将这些共同拥有的行为逻辑抽象成为一个模块,以减少代码重复。抽象出来的模块又被称之为切面。

专业术语

  • 切面(Aspect):是多个类公共行为逻辑的一个模块化,在Spring中由@Component@Aspect共同标记的类即为切面,切面中包含切入点(Pointcut)通知(Advice)
  • 连接点(Join point):所有可能被织入通知的点,在SpringAOP中所有运行中的方法都是可以被织入通知的,在SpringAOP中连接点就是所有运行中的方法
  • 切入点(Pointcut):切入点是真正被织入通知的方法。在SpringAOP中通过@Pointcut指定规则,定义哪些方法是切入点
  • 通知(Advice):定义公共行为的代码,织入到切入点,SpringAOP中有5种通知类型,可以织入到切入点的不同地方,
    • 前置通知(@Before):在方法执行前执行
    • 后置通知(@AfterReturning):在方法正常返回后执行
    • 环绕通知(@Around):可以方法的执行前后定义逻辑,也可以控制方法执行
    • 异常通知(@AfterThrowing):在方法抛出异常时执行
    • 最终通知(@After):最终执行的方法不管方法是否正常退出
  • 目标对象(Target object):被AOP代理对象所代理的对象,目标对象中的方法是需要织入通知的
  • AOP代理(AOP proxy):为了能够让目标对象的方法织入通知,需要生成目标对象的代理对象,这个代理对象目的是完成AOP操作,所以叫AOP代理
  • 织入(Weaving):将通知增强到切入点中的动作。有以下三种织入时机

    • 编译期:类编译成字节码时就将通知代码增强到目标方法上。如:AspectJ的织入编译器
    • 类加载期:在字节码加载到JVM的过程中,将通知代码增强盗目标方法。如:AspectJ5可使用这种方式
    • 运行期:在运行时期为目标方法所在的对象生成AOP代理对象。如:SpringAOP

      使用方法

      使用SpringAOP有两种方式:

    • 基于@AspectJ风格注解

    • 基于XML配置

      基于@AspectJ风格注解

      依赖准备

      使用@AspectJ风格注解需要引入aspectjweaver.jar包

      开启@AspectJ风格注解

      开启@AspectJ风格注解有两种方式:
  1. 基于XML方式:
  2. 基于注解方式:@EnableAspectJAutoProxy

    切面、切入点、通知定义

    ```yaml @Component @Aspect public class MyAspect { /**

    • 切入点规则定义 / @Pointcut(“execution( com.gao..(..))”) public void myPointCut(){ }

      @Before(“execution( com.gao..*(..))”) public void before(){ System.out.println(“before advice”); }

      @After(value = “myPointCut()”) public void after(){ System.out.println(“after”); }

      @AfterReturning(“myPointCut()”) public void afterReturning(){ System.out.println(“after returning”); }

      @AfterThrowing(“myPointCut()”) public void afterThrowing(){ System.out.println(“after throwing”); }

  1. @Around("myPointCut()")
  2. public void around(ProceedingJoinPoint proceedingJoinPoint){
  3. System.out.println("around 前");
  4. try {
  5. proceedingJoinPoint.proceed();
  6. System.out.println("around try 后");
  7. } catch (Throwable throwable) {
  8. throwable.printStackTrace();
  9. System.out.println("around throw");
  10. }finally {
  11. System.out.println("around finally");
  12. }
  13. System.out.println("around 后");
  14. }

}

  1. <a name="WMsUl"></a>
  2. ## 基于XML配置
  3. 由于现在都是SpringBoot,基于XML方式这里就不费时间写了
  4. <a name="ZcTMc"></a>
  5. # 使用特性
  6. <a name="ahweH"></a>
  7. ## 执行顺序
  8. 切面的执行顺序可以通过`@Order`指定,下面来看下通知的执行顺序
  9. 1. 单切面
  10. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1609516/1651493496270-82435aab-530f-43a5-99d1-96d7a7a60543.png#clientId=u933ebcbd-74c1-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=525&id=u6a163202&margin=%5Bobject%20Object%5D&name=image.png&originHeight=656&originWidth=762&originalType=binary&ratio=1&rotation=0&showTitle=false&size=115639&status=done&style=none&taskId=u3d55ca22-81fc-4fc9-8685-0c903a1b2f0&title=&width=609.6)
  11. 2. 多切面
  12. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1609516/1651493937863-aeaf0b55-42f5-4064-aed9-29b459b337c1.png#clientId=u933ebcbd-74c1-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=349&id=u363201f1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=436&originWidth=866&originalType=binary&ratio=1&rotation=0&showTitle=false&size=99204&status=done&style=none&taskId=uc0080e14-fbb2-4ccb-bc31-b56edc504b2&title=&width=692.8)
  13. <a name="UVfnO"></a>
  14. ## 如何修改入参
  15. 1. 对于@Around通知,可以控制方法的运行和传入的参数,所以很好解决
  16. 2. 对于其他通知类型
  17. 1. 我们可以拿到目标方法的参数但是无法修改其值
  18. 2. 若是引用类型我们是可以改对象的里面属性的,但是无法直接将其对象替换
  19. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1609516/1651500595753-5b0686fb-36e3-4d82-8dde-87daf8353285.png#clientId=u933ebcbd-74c1-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=205&id=ucf90aab6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=256&originWidth=1295&originalType=binary&ratio=1&rotation=0&showTitle=false&size=72985&status=done&style=none&taskId=u2f2e2a76-6a35-4c87-9a81-0a765dffa68&title=&width=1036)
  20. <a name="ZtYF4"></a>
  21. ## 如何获取入参
  22. 获取入参的方式官方提供了1个,这边我们还可以通过自定义注解+JoinPoint+SpEL表达式的方式获取
  23. 1. 方式一:args表达式
  24. ![](https://cdn.nlark.com/yuque/0/2022/png/1609516/1651500118080-530e145c-635f-49f1-80f3-bad6db7d4198.png#crop=0&crop=0&crop=1&crop=1&from=url&id=puJHw&margin=%5Bobject%20Object%5D&originHeight=186&originWidth=875&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  25. 2. 方式二:自定义注解+JointPoint+SpEL表达式
  26. 1. 自定义注解中声明一个字段用于指定SpEL表达式
  27. 2. 将JoinPoiont的所有参数名和参数值放入SpELContext中
  28. 3. 通过注解中的SpEL表达式获取参数中的值
  29. ```yaml
  30. package com.yurun.micro.dms.annotations;
  31. import java.lang.annotation.Documented;
  32. import java.lang.annotation.ElementType;
  33. import java.lang.annotation.Retention;
  34. import java.lang.annotation.RetentionPolicy;
  35. import java.lang.annotation.Target;
  36. /**
  37. * Sap接口注解
  38. *
  39. * @author GaoXi
  40. * @date 2022/4/28 19:38
  41. */
  42. @Documented
  43. @Target(ElementType.METHOD)
  44. @Retention(RetentionPolicy.RUNTIME)
  45. public @interface SapInterface {
  46. /**
  47. * 指定全量标识字段: #参数名.全量标识字段
  48. */
  49. String fullSign() default "";
  50. }
  1. @SapInterface(fullSign = "#maraMasterRequest.zzall")
  2. @Override
  3. public R<List<SapOtMara>> maraMasterList(MaraMasterRequest maraMasterRequest) {
  4. return remoteSapDmsService.maraMasterList(maraMasterRequest);
  5. }
  1. /**
  2. * 获取是否全量标识字符串
  3. *
  4. * @param joinPoint 连接点
  5. * @param fullSign #参数名.字段名
  6. * @return 0-非全量 1-全量
  7. */
  8. private String getIsFullSign(ProceedingJoinPoint joinPoint, String fullSign) {
  9. // 参数值
  10. Object[] args = joinPoint.getArgs();
  11. // 参数名
  12. String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
  13. // SpELContext初始化并解析表达式
  14. Expression expression = spelExpressionParser.parseExpression(fullSign);
  15. EvaluationContext context = new StandardEvaluationContext();
  16. for (int i = 0; i < args.length; i++) {
  17. context.setVariable(argNames[i], args[i]);
  18. }
  19. return Objects.requireNonNull(expression.getValue(context)).toString();
  20. }

如何定义一个切入点

  1. execution:用于定义匹配某些方法
    1. public 包名.类名.方法名(参数类型,参数类型) 声明异常类型
  2. within:用于定义在某些类下的方法
    1. 包名.类名
  3. this:暂不理解
    1. 类全限定名
  4. target:暂不理解
    1. 类全限定名
  5. args:匹配方法传入参数类型为指定类型的方法
    1. 类型,类型
  6. @target:暂不理解
  7. @args:指定方法参数中有指定注解的
  8. @within:类上有指定注解的类下所有方法
  9. @annotation:匹配有特定注解的方法

常见特殊符号

  • ..:所有参数类型
  • .*:一般指下一级的所有东西
  • ..*:一般指所有下一级下的所有东西

    如何定义通知

    Before通知

    before通知没有什么特别的,正常定义就好
    image.png

    AfterReturning通知

    afterReturning可以接收一个返回值
    image.png

    AfterThrowing通知

    afterThrowing通知可以接收一个异常
    image.png

    After通知

    image.png

    Around通知

    around的可以控制方法的执行,需要在参数中加入ProcessJoinPoint且在参数的首位
    image.png

JoinPoint方法

所有的通知都可以将JoinPoint类放在方法的首个参数中。JoinPoint提供了如下方法

  1. getArgs(): 获取目标方法的全部入参
  2. getThis(): 获取这个AOP代理对象
  3. getTarget(): 获取目标对象
  4. getSignature():获取方法签名

    如何参数传入通知

  5. 方式一:通过args传入方法入参

image.png

  1. 方式二:通过@annotation传入方法上的注解

image.png

动态代理实现方式选择

Spring的AOP动态代理实现方式有两种:CGLIB和JDK动态代理

  1. 若被代理的目标对象没有实现接口,则使用CGLIB
  2. 若被代理的目标对象实现了接口,则使用JDK动态代理
  3. 可以通过强制使用CGLIB动态代理,其方式是将proxyTargetClass设置为true@EnableAspectJAutoProxy(proxyTargetClass = true)

参考文章