Spring Aop官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop

Spring的AOP理解

在我们日常开发当中都是面向对象(OOP),允许开发者是自上而下调用关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。

AOP,一般称为面向切面,作为面向对象的一种扩展,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的冗余代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务、限流处理。

AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

基本使用

依赖配置

jar 说明:aspectjweaver.jar 用于面向方面的编程或AOP,编织器实际上是将用每个实例中要执行的代码定义的方法/切入点/连接点“编织”在一起的部分。

  1. <properties>
  2. <java.version>1.8</java.version>
  3. <spring.version>5.1.5.RELEASE</spring.version>
  4. <aspectjweaver.version>1.9.5</aspectjweaver.version>
  5. </properties>
  6. <dependencies>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-context</artifactId>
  10. <version>${spring.version}</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.aspectj</groupId>
  14. <artifactId>aspectjweaver</artifactId>
  15. <version>${aspectjweaver.version}</version>
  16. </dependency>
  17. </dependencies>

配置类AopConfig

  1. @Configuration
  2. // 开启AOP支持
  3. // 该注解中会使用Import注解导入后置处理器及注册自定义Bean用来完成AOP功能
  4. @EnableAspectJAutoProxy
  5. @ComponentScan("com.zlp.spring.aop")
  6. public class AopConfig {
  7. }

计算器测试类MathService

  1. @Service
  2. public class MathService {
  3. public int division(int i,int j){
  4. System.out.println(String.format("division.req ===>i=%d,j=%d",i,j));
  5. return i / j;
  6. }
  7. }

切面AopLog

  1. /**
  2. * Aop 日志切面
  3. * @date: 2022/2/25 11:11
  4. */
  5. @Aspect
  6. @Component
  7. public class AopLog {
  8. @Pointcut("execution(* com.zlp.spring.aop.service..*.*(..) )")
  9. public void pointCut() { }
  10. // 方法执行前通知
  11. @Before("pointCut()")
  12. public void beforeLog(JoinPoint joinPoint) {
  13. Object[] args = joinPoint.getArgs();
  14. System.out.println("运行 Before 日志记录... 参数为:" + Arrays.asList(args));
  15. }
  16. // 方法执行完后通知
  17. @After("pointCut()")
  18. public void afterLog(JoinPoint joinPoint) {
  19. System.out.println("运行 After 日志记录");
  20. }
  21. // 执行成功后通知
  22. @AfterReturning(value = "pointCut()", returning = "result")
  23. public void afterReturningLog(JoinPoint joinPoint,Object result) {
  24. System.out.println("运行 AfterReturning 日志记录,结果为:" + result);
  25. }
  26. // 抛出异常后通知
  27. @AfterThrowing(value = "pointCut()", throwing = "exception")
  28. public void logException(JoinPoint joinPoint, Exception exception) {
  29. System.out.println(joinPoint.getSignature().getName() + "运行AfterThrowing... 异常信息:" + exception);
  30. }
  31. // 环绕通知
  32. @Around("pointCut()")
  33. public Object aroundLog(ProceedingJoinPoint joinpoint) {
  34. // // 获取被增强的目标对象,然后获取目标对象的class
  35. // Class<?> targetClass = joinpoint.getTarget().getClass();
  36. // System.out.println("执行Around,被增强的目标类为:" + targetClass);
  37. // // 方法名称
  38. // String methodName = joinpoint.getSignature().getName();
  39. // System.out.println("执行Around,目标方法名称为:" + methodName);
  40. // // 目标方法的参数类型
  41. // Class[] parameterTypes = ((MethodSignature) joinpoint.getSignature()).getParameterTypes();
  42. // // 目标方法的入参
  43. // Object[] args = joinpoint.getArgs();
  44. // System.out.println("执行Around,方法入参为:" + Arrays.toString(args));
  45. Object result = null;
  46. try {
  47. System.out.println("环绕通知 Before 日志记录");
  48. long start = System.currentTimeMillis();
  49. //有返回参数 则需返回值
  50. result = joinpoint.proceed();
  51. long end = System.currentTimeMillis();
  52. System.out.println("总共执行时长" + (end - start) + " 毫秒");
  53. System.out.println("环绕通知 After 日志记录");
  54. } catch (Throwable t) {
  55. System.out.println("出现错误");
  56. }
  57. return result;
  58. }
  59. }

测试类 AopTest

  1. public class AopTest {
  2. public static void main(String[] args) {
  3. ApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
  4. MathService mathService = ac.getBean("mathService", MathService.class);
  5. mathService.division(1, 1);
  6. }
  7. }

运行成功结果:

  1. 环绕通知 Before 日志记录
  2. 运行 Before 日志记录... 参数为:[1, 1]
  3. target method division.req ===>i=1,j=1
  4. 总共执行时长111 毫秒
  5. 环绕通知 After 日志记录
  6. 运行 After 日志记录
  7. 运行 AfterReturning 日志记录,结果为:1

运行异常结果:

  1. 环绕通知 Before 日志记录
  2. 运行 Before 日志记录... 参数为:[1, 0]
  3. target method division.req ===>i=1,j=0
  4. 运行 After 日志记录
  5. division运行AfterThrowing... 异常信息:java.lang.Exception: 出现错误/ by zero

就这样简单,一个AOP的使用示例就成功运行完成了,那么示例中的 @Aspect,@PointCut,@Before等都是什么鬼玩意呢?请继续向下看。

Spring的AOP中的概念

切面

切面指的是由一系列切点及增强动作组成的模块对象,在使用过程中,如果有多个切面,可以通过Spring提供的Order接口或者 @Order 注解来定义切面的优先级,从而可以按照指定的顺序来对应用功能进行增强。例如上例中的LogAop测试类就是一个切面,使用注解版的切面类,切面需要使用注解 @Aspect 来进行标注。
经常使用的场景:接口性能监控,事务管理,日志记录,限流,读写分离等。

切点

切点也是一个抽象的概念,可以简单理解为数据库中的查询条件。例如在数据库中,某条数据满足一定的查询条件才会被查询出来,而如果某一个类中的方法满足切点中定义的条件时,切点对应的增强动作才会被执行。而这个切点对应的条件,成为切点表达式,切点表达式通常有如下的几种类型:

(1)execution表达式

该表达式的最小粒度为方法,使用语法如下:

  1. execution(modifier returnType package.method(param) exception);

参数说明:

  1. modifier:方法修饰符,例如:publicprivate等,可以省略
  2. returnType:方法返回值类型,如:intvoid
  3. package:方法所在类的包名,可以省略
  4. method:方法名称
  5. param:方法参数
  6. exception:方法抛出的异常,可以省略

示例1:
下面表达式会匹配到使用 public 修饰,返回值为int(如果写为*,则表示匹配任意返回值),类名称为MathService中的所有返回int方法,方法中可以带有参数,也可以不带参数

  1. execution(public int com.zlp.spring.aop.service.MathService.*(..))

示例2:
下面表达式会匹配到返回值为任意类型,在 com.zlp.spring.aop.service.MathService 类中,并且不带参数的方法

  1. execution(* com.zlp.spring.aop.service.MathService.*())

示例3:
下面表达式会匹配到返回值为任意类型,并且在com.zlp.spring包及其子包下的所有类中名称为testService的没有参数的方法:
..通配符值的是匹配0个或者多个。

  1. execution(* com.zlp.spring..*.testService())

示例4:
下面表达式会匹配到返回值为任意类型,并且以Math开头的所有类中的所有第一个参数为String类型的的所有方法

  1. execution(* com.zlp.spring.aop.service.Math*.*(java.lang.String, ..))

(2)within类型


within声明的切点表达式最小粒度为类,匹配到表达式的所有类中的所有方法都会被增强,使用方法如下:
within(match-pattern)
示例1:
如下的表达式会匹配 com.zlp.spring.aop.service.MathService 类下的所有方法

  1. within(com.zlp.spring.aop.service.MathService)

示例2:

如下的表达式会匹配到 com.zlp.spring.aop 包下的所有类下的所有方法,但是不包括子包中的类

  1. within(com.zlp.spring.aop.*)

示例3:
如下的表达式会匹配到com.zlp.spring.aop包及其子包下的所有类中的所有方法

  1. within(com.zlp.spring.aop..*)

(3)args类型

args类型不用关注类名和方法名,只需要关注方法中的参数类型和参数个数,但是如果指定参数类型时,需要指定类型对应的全路径,语法如下:

  1. args(match-pattern)

示例1:
下面表达式可以匹配到只有一个String类型的参数对应的方法

  1. args(java.lang.String)

示例2:
下面表达式会匹配到第一个参数为String类型,最后一个类型为Long类型的所有方法

  1. args(java.lang.String,..,java.lang.Long)

(4)@within类型

within表示匹配指定的类,@within表示匹配带有指定注解的类。语法如下:

  1. @within(annotation-pattern)

示例:
下面示例表示匹配所有标注有com.zlp.spring.aop.aop.MyAnnotation的类

  1. @within(com.zlp.spring.aop.aop.MyAnnotation)

只要切面中使用 @within声明了切入 @MyAnnotation 注解,则只要标注有该注解的类下面的方法,在运行期间都会被切到,一般用于类上面。

(5)@annotation类型

@annotation和@within类似,只是@annotation中指定的注解一般用于某个类中的具体方法上,只要标注了@annotation中指定的注解,那么该方法在运行期间就会被切面拦截到。语法如下:

  1. @annotation(annotation-pattern)

示例1:

下面示例表示匹配所有标注有 com.wb.spring.aop.MyAnnotation 的方法

  1. @annotation(com.wb.spring.aop.MyAnnotation)

示例2:
例如下面的div方法执行时会被环绕增强,因为方法上方标注有@MyAnnotation注解,而切面表达式刚好是匹配到该注解。

注解类 MyAnnotation

  1. @Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface MyAnnotation {
  4. }

测试类MyService

  1. @Component
  2. public class MyService {
  3. @MyAnnotation
  4. public int div(int a, int b) {
  5. return a + b;
  6. }
  7. }

切面类MyAspect

  1. /**
  2. * 自定义切面
  3. * @date: 2022/2/25 14:23
  4. */
  5. @Aspect
  6. @Component
  7. public class MyAspect {
  8. /**
  9. * <p>
  10. * 1. @within 声明了切入 @MyAnnotation 注解,则只要标注有该注解的类下面的方法,在运行期间都会被切到,一般用于类上面。
  11. * eg: @Around("@within(com.zlp.spring.aop.aop.MyAnnotation)")
  12. * 2. @annotation中指定的注解一般用于某个类中的具体方法上,只要标注了@annotation中指定的注解,那么该方法在运行期间就会被切面拦截到。
  13. * eg: @Around("@@annotation(com.zlp.spring.aop.aop.MyAnnotation)")
  14. * </p>
  15. * @param proceed
  16. * @date: 2022/2/25 14:18
  17. * @return: java.lang.Object
  18. */
  19. @Around("@annotation(com.zlp.spring.aop.aop.MyAnnotation)")
  20. public Object around(ProceedingJoinPoint proceed) throws Throwable {
  21. System.out.println("around... invoke ...");
  22. return proceed.proceed();
  23. }
  24. }

(6)@args类型

@args表示使用了指定注解的类作为某个方法的入参,在这个方法被调用的时候,方法会被增强。语法如下:

  1. @args(annotation-pattern)

示例1:

下面的示例表示匹配使用MyAnnotation注解标注的类作为参数的方法

  1. @args(com.zlp.spring.aop.aop.MyAnnotation)

示例2:

使用切面功能对方法入参中包括Pen类型参数的方法进行增强。
测试类Pen

  1. @Component
  2. public class Pen {
  3. @MyAnnotation
  4. public String writeText() {
  5. return "Hello world";
  6. }
  7. }

测试类MyClass

  1. @Component
  2. public class MyClass {
  3. public String write(Pen pen) {
  4. return pen.writeText();
  5. }
  6. }

切面类MyAspect

  1. @Aspect
  2. @Component
  3. public class MyAspect {
  4. @Around("@annotation(com.zlp.spring.aop.aop.MyAnnotation)")
  5. public Object around(ProceedingJoinPoint proceed) throws Throwable {
  6. System.out.println("around... invoke ...");
  7. return proceed.proceed();
  8. }
  9. }

测试类AopTest

  1. public class AopTest {
  2. public static void main(String[] args) {
  3. ApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
  4. MyClass myClass = ac.getBean(MyClass.class);
  5. Pen pen = ac.getBean(Pen.class);
  6. myClass.write(pen);
  7. }
  8. }

增强动作

增强动作指的是程序执行过程中,如果所执行的方法满足某一个切点定义的条件之后,与切点对应的增强方法就会被触发而执行。有如下几种:
(1)@Around,环绕通知
这个注解的功能最丰富,使用这个注解标注的方法是用来实现业务逻辑代码的环绕增强,入参为ProceedingJoinPoint,可以用来调用业务模块的代码,调用之前的逻辑和调用之后的逻辑都可以在这个方法中实现,而且这个方法可以阻断业务模块的调用。
(2)@Before,前置通知
使用这个注解标注的方法会在业务代码执行之前先执行,不能阻断业务逻辑的执行,除非抛出异常。
(3)@After,后置通知
类似于finally,在所有的增强方法执行之后才会执行,无论业务逻辑是否有异常。
(4)@AfterReturning,返回通知
该注解标注的方法在业务逻辑代码执行之后执行。
(5)@AfterThrowing,异常通知

该注解标注的方法在业务逻辑抛出指定异常之后会执行。

增强动作的执行顺序

  1. ->Around Before[@Before标注的方法方法执行前]
  2. ->Before[@Before标注的方法执行]
  3. ->目标方法[目标方法执行]
  4. ->Around After[目标方法执行之后,@After之前]
  5. ->After[@After标注的方法执行]
  6. -> AfterReturning (如果有异常,则AfterThrowing)[返回或者异常]

至此,SpringAOP 相关的基础用法介绍完毕,本篇文章通过一个简单的示例,说明了SpringAOP中常用的内容,例如切点,切面、切点表达式。后续文章将继续剖析SpringAOP的底层执行过程。欢迎评论转发!


参考文档
1. SpringAOP介绍及基础用法

代码地址(aop项目)
https://gitee.com/gaibianzlp/spring-study-example


Spring底层代理是哪种方式?

https://www.jianshu.com/p/f19a3504337a

[

AnnotationAwareAspectJAutoProxyCreator

](https://mp.weixin.qq.com/s?__biz=MzI4OTE2NTk1NQ==&mid=2649580267&idx=1&sn=2b589e3ca6a5a48687281419a74e795d&chksm=f42a8507c35d0c11e713369768f25cd5bccf6d034a58ad52e8f2195619d5c3d45c93a6ee46f7&token=2139939783&lang=zh_CN#rd)

https://segmentfault.com/a/1190000022372094
Spring中Aop底层实现原理?

image.png