
概念 | 含义 |
|---|---|
切面(Aspect) | 横切逻辑的模块(一个类),切面 = 切点 + 通知 + 连接点 |
通知(Advice) | 就是具体要做的工作,比如上面的 "记录代码耗时" |
切点(Pointcut) | 指定哪些方法要被拦截 比如 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式 |
连接点(JoinPoint) | 满足切点表达式规则的方法,就是连接点比如切点表达式指定的 com.example.demo.controller 路径下的方法,都是连接点 |

切点和连接点的关系:
比如:
注解 | 含义 |
|---|---|
@Before | 在目标方法执行前执行 |
@After | 在目标方法执行后(有异常也会执行) |
@AfterReturning | 在方法返回后执行(有异常不会执行) |
@AfterThrowing | 在方法抛出异常时执行 |
@Around | 环绕通知,可控制方法执行前后时机,最强大 |
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
// 前置通知
@Before("execution(* com.example.demo.controller.*.*(..))")
public void doBefore() {
log.info("执行 Before 方法");
}
// 后置通知
@After("execution(* com.example.demo.controller.*.*(..))")
public void doAfter() {
log.info("执行 After 方法");
}
// 返回后通知
@AfterReturning("execution(* com.example.demo.controller.*.*(..))")
public void doAfterReturning() {
log.info("执行 AfterReturning 方法");
}
// 抛出异常后通知
@AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
public void doAfterThrowing() {
log.info("执行 doAfterThrowing 方法");
}
// 添加环绕通知
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around 方法开始执行");
Object result = joinPoint.proceed();
log.info("Around 方法结束执行");
return result;
}
}写一些测试程序:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
int a = 10 / 0;
return true;
}
}观察日志:

@AfterThrowing 标识的通知方法不会执行!@Around 标识的通知方法包含两部分,一个 "前置逻辑",一个 "后置逻辑"。其中 "前置逻辑" 先于 @Before 标识的通知方法执行,"后置逻辑" 晚于 @After 标识的通知方法执行!
2. 异常时的情况:http://127.0.0.1:8080/test/t2
观察日志:

程序发生异常的情况下:
@AfterReturning 标识的通知方法在出现异常时候不会执行。@Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)
💥注意事项:
@Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,而其它通知类型不需要考虑目标方法的执行。@Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。@PointCut上面代码存在一个问题,就是存在大量重复的切点表达式 execution(* com.example.demo.controller.*.*(..)),Spring 提供了 @PointCut 注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可。
上述代码就可以修改为:
@Slf4j
@Aspect
@Component
public class AspectDemo {
// 定义切点(公共的切点表达式)
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
// 前置通知
@Before("pt()")
public void doBefore() {
//...代码省略
}
// 后置通知
@After("pt()")
public void doAfter() {
//...代码省略
}
// 返回后通知
@AfterReturning("pt()")
public void doAfterReturning() {
//...代码省略
}
// 抛出异常后通知
@AfterThrowing("pt()")
public void doAfterThrowing() {
//...代码省略
}
// 添加环绕通知
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//...代码省略
}
}private 修饰时,仅能在当前切面类中使用。private 改为 public,引用方式为:全限定类名.方法名()@Slf4j
@Aspect
@Component
public class AspectDemo2 {
@Before("com.example.demo.aspect.AspectDemo.pt()") // 引用别的文件中的切点!
public void doBefore() {
log.info("执行 AspectDemo2 -> Before 方法");
}
}@Order当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?
AspectDemo2:
@Component
public class AspectDemo2 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo2 -> Before 方法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo2 -> After 方法");
}
}AspectDemo3:
@Component
public class AspectDemo3 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo3 -> Before 方法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo3 -> After 方法");
}
}AspectDemo4:
@Component
public class AspectDemo4 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo4 -> Before 方法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo4 -> After 方法");
}
}运行程序,访问接口:http://127.0.0.1:8080/test/t1
观察日志:

通过上述程序的运行结果,可以看出:
存在多个切面类时,默认按照切面类的类名字母排序:
@Before 通知:字母排名靠前的先执行@After 通知:字母排名靠前的后执行但这种方式不方便管理,我们的类名更多还是具备一定含义的。
所以 Spring 提供了一个新的注解,控制这些切面通知的执行顺序:@Order
@Aspect
@Component
@Order(2)
public class AspectDemo2 {
//...代码省略
}
@Aspect
@Component
@Order(1)
public class AspectDemo3 {
//...代码省略
}
@Aspect
@Component
@Order(3)
public class AspectDemo4 {
//...代码省略
}重新运行程序,访问接口:http://127.0.0.1:8080/test/t1
观察日志:

@Order 注解标识的切面类,执行顺序如下:
@Before 通知:数字越小先执行@After 通知:数字越大先执行总结:@Order 控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。
切点表达式常见有两种表达方式
execution():根据方法的签名来匹配@annotation():根据注解匹配execution() 是最常用的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)其中【访问修饰符】和【异常】可以省略!

切点表达式支持通配符表达:
*:匹配任意字符,只匹配一个元素(返回类型、包、类名、方法、方法参数)* 表示任意包(一层包使用一个 *)* 表示任意类* 表示任意返回值类型* 表示任意方法* 表示一个任意类型的参数..:匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数.. 配置包名,标识此包以及此包下的所有子包.. 配置参数,任意个任意类型的参数切点表达式示例:
TestController 下的 public 修饰,返回类型为 String 方法名为 t1 的无参方法:
execution(public String com.example.demo.controller.TestController.t1())省略访问修饰符:
execution(String com.example.demo.controller.TestController.t1())匹配所有返回类型:
execution(* com.example.demo.controller.TestController.t1())匹配 TestController 下的所有无参方法:
execution(* com.example.demo.controller.TestController.*())匹配 TestController 下的所有方法:
execution(* com.example.demo.controller.TestController.*(..))匹配 controller 包下所有的类的所有方法:
execution(* com.example.demo.controller.*.*(..))匹配所有包下面的 TestController:
execution(* com..TestController.*(..))匹配 com.example.demo 包下,子孙包下的所有类的所有方法:
execution(* com.example.demo..*(..))execution() 更适用有规则的,如果我们要匹配多个无规则的方法呢,比如:TestController 中的 t1() 和 UserController 中的 u1() 两个方法,这个时候使用 execution() 这种切点表达式来描述就不是很方便了.
我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点!
实现步骤:
@annotation 表达式来描述切点首先提供测试用例:
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
return true;
}
}@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1(){
return "u1";
}
@RequestMapping("/u2")
public String u2(){
return "u2";
}
}import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}@Target:标识 Annotation 所修饰的对象范围,即该注解可以用在什么地方。ElementType.TYPE:用于描述类、接口或 enum 声明ElementType.METHOD:描述方法ElementType.PARAMETER:描述参数ElementType.TYPE_USE:可以标注任意类型@Retention:标明注解的生命周期RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。比如 @SuppressWarnings,以及 lombok 提供的注解 @Data、@Slf4j 等。RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取。通常用于一些框架和工具的注解。RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时中。这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如 Spring 的 @Controller、@ResponseBody 等。使用 @annotation 切点表达式定义切点,只对自定义注解 @MyAspect 生效:
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
// 前置通知
@Before("@annotation(com.example.demo.aspect.MyAspect)")
public void before(){
log.info("MyAspect -> before ...");
}
// 后置通知
@After("@annotation(com.example.demo.aspect.MyAspect)")
public void after(){
log.info("MyAspect -> after ...");
}
}在 TestController 中的 t1() 和 UserController 中的 u1() 这两个方法上添加自定义注解 @MyAspect,其他方法不添加:
@MyAspect
@RequestMapping("/t1")
public String t1() {
return "t1";
}@MyAspect
@RequestMapping("/u1")
public String u1(){
return "u1";
}运行程序,测试接口
http://127.0.0.1:8080/test/t1
观察日志:

可以看到切面通知被执行了。
继续测试:
http://127.0.0.1:8080/test/t2,切面通知未执行
http://127.0.0.1:8080/user/u1,切面通知执行
Spring AOP的实现方式(常见面试题)
参考:https://cloud.tencent.com/developer/article/2032268
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。