前情回顾
-
对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");} }
- 很清楚地知道了,给哪些连接点增强了。
-
-
-
在Spring Boot应用中,通常不需要手动添加
@EnableAspectJAutoProxy
注解来启用AOP功能。这是因为Spring Boot已经为你自动配置了AOP支持。- Spring Boot通过
@SpringBootApplication
注解(它包含了@EnableAutoConfiguration
)自动开启了AOP功能。具体来说,Spring Boot会自动扫描项目中的@Aspect
注解类,并将其注册为切面(Aspect),同时启用AspectJ代理机制。
- Spring Boot通过
小结
@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来帮助落地。