当前位置: 首页 > article >正文

结合代码理解Spring AOP的概念(切面、切入点、连接点等)

前情回顾

  • 对AOP的理解

    • 我这篇文章介绍了为什么要有AOP(AOP解决了什么问题)以及如何实现AOP。但在实现AOP的时候,并未探讨AOP相关概念,例如:切面、切入点、连接点等。
    • 因此,本篇文章希望结合代码去理解Spring AOP的相关概念。

Talk is cheap, show me the code.

背景

  • 在使用AOP时,我们大概率遇到了这样的场景:我现在有多个方法,在这多个方法执行前/执行后要做一些统一的操作。

  • 例如:

  • @RequestMapping("/user")
    @RestController
    public class UserController {
        @GetMapping("/query")
        public String queryUser() {
            return "I am a user";
        }
    }
    
    @RequestMapping("/student")
    @RestController
    public class StudentController {
        @GetMapping("/query")
        public String queryStudent() {
            return "I am a student";
        }
    }
    
    • 我希望在执行这两个方法前,打印一行日志:start execute。
  • 为多个方法增加逻辑,这些代码写在哪里呢?当然是写到一个类里啊(Java嘛,万事万物皆对象,要封装到类里)。

  • public class LogAspect {
        public void log() {
            System.out.println("start execute");
        }
    }
    
    • 这样显然是不够的,因为,Spring并不知道这个类是特殊的类,这些代码要为谁增强。因此,我们要遵循Spring规范,提供一些标记。

      @Aspect
      public class LogAspect {
          public void log() {
              System.out.println("start execute");
          }
      }
      
      • 查看下@Aspect这个注解:

      • @Retention(RetentionPolicy.RUNTIME)
        @Target({ElementType.TYPE})
        public @interface Aspect {
            String value() default "";
        }
        
        • 非常的简单,就是告知Spring这个类是一个切面类。切面类中的方法,是其他方法的补充逻辑。
        • 然而,仅仅打上@Aspect这个注解是不够的(因为LogAspect没有注入到Spring容器中),还需要打上@Component注解,告诉Spring帮我管理这个Bean。
        • 在Spring容器中,Spring管理着UserController和StudentController这些bean,可以为它们分别生成代理类,然后将Spring容器中的LogAspect合适地织入到代理类中,从而增强了UserController和StudentController的功能。

切面 + 切入点 + 连接点 + 通知

  • 这个切面类中的方法,给谁用呢?显然,这也需要告知Spring。

  • 开发者自己是知道要给谁用的,例如:给UserController的queryUser方法和StudentController的queryStudent方法用。这些方法可以被通俗地理解为一个个 连接点(Joinpoint) 。LogAspect的log方法是给多个连接点使用的,这多个连接点又称为 切入点(Pointcut)

    @Aspect
    @Component
    public class LogAspect {
        @Pointcut("execution(* com.forrest.learn.springboot.example5.controller.*.*(..))")
        private void example5Controller() {}
    
    
        public void log() {
            System.out.println("start execute");
        }
    }
    
    • execution(* com.forrest.learn.springboot.example5.controller..(..))​切入点表达式,不太好写,而且容易过度拦截连接点。我们只想拦截UserController的queryUser方法和StudentController的queryStudent方法。这时候怎么办?用注解。

    • @Retention(RetentionPolicy.RUNTIME)
      @Target({ElementType.METHOD})
      public @interface LogController {
      
      }
      
      @RequestMapping("/student")
      @RestController
      public class StudentController {
          @LogController
          @GetMapping("/query")
          public String queryStudent() {
              return "I am a student";
          }
      }
      
      @RequestMapping("/user")
      @RestController
      public class UserController {
          @LogController
          @GetMapping("/query")
          public String queryUser() {
              return "I am a user";
          }
      }
      
      @Aspect
      @Component
      public class LogAspect {
          @Pointcut("@annotation(com.forrest.learn.springboot.example5.annotation.LogController)")
          private void example5Controller() {}
      
      
          public void log() {
              System.out.println("start execute");
          }
      }
      
      • 只要方法打上了@LogController注解,就要被拦截。又引出了另一个问题,什么时候拦截呢?是方法执行前拦截?还是执行后拦截?显然,需要 通知(Advise) ​。

        • @Before​: 拦截方法,在方法执行前增强
        • @AfterReturning​: 拦截方法,在方法执行并正常返回后增强
        • @AfterThrowing​: 拦截方法,在方法执行并异常返回后增强
        • @After​: 拦截方法,在方法执行后增强
        • @Around​:拦截方法,用户自行决定在方法前/后进行增强,也就是包含了前面4个注解的功能了,是最自由的增强。
      • @Aspect
        @Component
        public class LogAspect {
            @Pointcut("@annotation(com.forrest.learn.springboot.example5.annotation.LogController)")
            private void example5Controller() {}
        
            @Before("example5Controller()")
            public void log() {
                System.out.println("start execute");
            }
        }
        

        image

        • 很清楚地知道了,给哪些连接点增强了。
  • 在Spring Boot应用中,通常不需要手动添加@EnableAspectJAutoProxy​注解来启用AOP功能。这是因为Spring Boot已经为你自动配置了AOP支持。

    • Spring Boot通过@SpringBootApplication​注解(它包含了@EnableAutoConfiguration​)自动开启了AOP功能。具体来说,Spring Boot会自动扫描项目中的@Aspect​注解类,并将其注册为切面(Aspect),同时启用AspectJ代理机制。

小结

@Aspect  // 切面(为多个类提供增强逻辑,逻辑由方法实现,方法写在类中)
@Component // 需要将切面类注入到Spring容器中
public class LogAspect {
    // 为哪些方法进行增强?靠定义切入点(一组连接点)
    @Pointcut("@annotation(com.forrest.learn.springboot.example5.annotation.LogController)")
    private void example5Controller() {}

    // 什么时候进行增强?靠通知(Advice)
    @Before("example5Controller()")
    public void log() {
        System.out.println("start execute");
    }
}

连接点的进阶

  • 我需要统计方法执行的耗时,并且打印出方法名、方法入参。
/**
 * 从连接点中获取方法名,而不是通过注解的字段
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MetricTime {
//    String value() default "";  // 用户可以将方法名称传给value
}


@Aspect
@Component
public class MetricTimeAspect {
	// 切入点作为通知的参数
    @Around("@annotation(com.forrest.learn.springboot.example5.annotation.MetricTime)")
    public Object metricTime(ProceedingJoinPoint pjp) throws Throwable {
        long startAt = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            System.out.println(pjp.getSignature().getName() + " cost " + (System.currentTimeMillis() - startAt) + " ms");
            System.out.println("入参:" + Arrays.toString(pjp.getArgs()));
        }
    }
}

/*
queryUser cost 0 ms
入参:[]
*/

  • 切面类中的方法,可以有哪些入参?

    • ProceedingJoinPoint pjp

      • JoinPoint是AOP的核心接口之一,它提供了连接点的信息,例如方法名、参数值等。ProceedingJoinPoint是JoinPoint的子接口,专门用于@Around通知中。在其他通知(如@Before、@After、@AfterReturning、@AfterThrowing)中,通常使用JoinPoint。

        查看源码,就知道ProceedingJoinPoint、JoinPoint提供了哪些方法。

    • 还可以传入注解:

      @Aspect
      @Component
      public class MetricTimeAspect {
          @Around("@annotation(metricTime)")
          public Object metricTime(ProceedingJoinPoint pjp, MetricTime metricTime) throws Throwable {
              long startAt = System.currentTimeMillis();
              try {
                  return pjp.proceed();
              } finally {
                  System.out.println(pjp.getSignature().getName() + " cost " + (System.currentTimeMillis() - startAt) + " ms");
                  System.out.println("入参:" + Arrays.toString(pjp.getArgs()));
              }
          }
      }
      

思路 > 技术细节

  • Spring AOP在技术细节上还有很多知识。等真正需要用到这些知识时,我们可以查看官方文档,借助AI来帮助落地。


http://www.kler.cn/a/612237.html

相关文章:

  • vue watch数据监听
  • 关于spark在yarn上运行时候内存的介绍
  • 【Minio-优化浅谈】
  • CI/CD(六) helm部署ingress-nginx(阿里云)
  • 【后端】【Django】信号使用详解
  • C#面向对象 一些细节
  • 基于C++实现一个平面上的形状编辑程序
  • ChatGPT 4o 更新了图像能力,效果怎么样?
  • 青否数字人直播系统包括六大互动功能,保障直播间能够实现智能化实时互动!
  • RSA算法深度解析:从数学基础到安全实践
  • Docker容器的kafka在VM虚拟机挂起重新运行之后连接异常解决
  • 【人工智能】一部正在书写的传奇,从诞生到未来蓝图
  • 【力扣hot100题】(007)无重复字符的最长子串
  • Rust从入门到精通之进阶篇:17.宏编程基础
  • 排序算法(插入,希尔,选择,冒泡,堆,快排,归并)
  • 漫画|基于SprinBoot+vue的漫画网站(源码+数据库+文档)
  • Docker+Ollama+Xinference+RAGFlow+Dify+Open webui部署及踩坑问题
  • ctfhow——web入门171~175
  • 量子计算与项目管理:2025年颠覆性技术将如何重构任务分解逻辑?
  • 优雅的开始一个Python项目