跳到主要内容Spring AOP 核心概念与切点表达式详解 | 极客日志Javajava
Spring AOP 核心概念与切点表达式详解
Spring AOP 的核心概念,包括切点、连接点、通知类型及切面。通过代码示例展示了前置、后置、环绕等通知的执行顺序,并介绍了使用 @Pointcut 简化切点表达式的方法。此外,还讲解了如何通过 @Order 注解控制多个切面的优先级,以及 execution 和 annotation 两种切点表达式的语法与用法。
岁月神偷5 浏览 Spring AOP 核心概念
切点 (Pointcut)
切点 (Pointcut),也称之为'切入点'。
Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述),告诉程序对哪些方法进行功能增强。

上面的表达式 execution(* com.wmh.springaop.controller.*.*(..)) 就是切点表达式。
连接点 (Join Point)
满足切点表达式规则的方法,就是连接点。也就是可以被 AOP 控制的方法。
对于下面的代码,com.wmh.springaop.controller 路径下所有类的所有方法,都是连接点。

切点和连接点的关系
连接点是满足切点表达式的元素。切点可以看做是保存了众多连接点的一个集合。
通知 (Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)。
比如上述代码中记录业务方法的耗时时间,就是通知。

在 AOP 面向切面编程当中,我们把这部分重复的代码逻辑抽取出来单独定义,这部分代码就是通知的内容。
切面 (Aspect)
切面 (Aspect) = 切点 (Pointcut) + 通知 (Advice)。
通过切面就能够描述当前 AOP 程序需要针对于哪些方法,在什么时候执行什么样的操作。
切面既包含了通知逻辑的定义,也包括了连接点的定义。

切面所在的类,我们一般称为切面类(被@Aspect 注解标识的类)。
通知类型
上面我们讲了什么是通知,接下来学习通知的类型。@Around 就是其中一种通知类型,表示环绕通知。
Spring 中 AOP 的通知类型有以下几种:
• @Around: 环绕通知,此注解标注的通知方法在目标方法前、后都被执行
• @Before: 前置通知,此注解标注的通知方法在目标方法前被执行
• @After: 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
• @AfterReturning: 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
• @AfterThrowing: 异常后通知,此注解标注的通知方法发生异常后执行
下面通过代码加深对上面通知类型的理解:
AspectDemo
package com.wmh.springaop.aspect;
lombok.extern.slf4j.Slf4j;
org.aspectj.lang.ProceedingJoinPoint;
org.aspectj.lang.annotation.*;
org.springframework.stereotype.Component;
{
{
log.info();
}
{
log.info();
}
Object Throwable {
log.info();
joinPoint.proceed();
log.info();
result;
}
{
log.info();
}
{
log.info();
}
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
import
import
import
import
@Slf4j
@Component
@Aspect
public
class
AspectDemo
@Before("execution(* com.wmh.springaop.controller..*(..))")
public
void
doBefore
()
"AspectDemo do before...."
@After("execution(* com.wmh.springaop.controller..*(..))")
public
void
doAfter
()
"AspectDemo do after...."
@Around("execution(* com.wmh.springaop.controller..*(..))")
public
doAround
(ProceedingJoinPoint joinPoint)
throws
"AspectDemo do around before...."
Object
result
=
"AspectDemo do around after..."
return
@AfterReturning("execution(* com.wmh.springaop.controller..*(..))")
public
void
doAfterReturning
()
"AspectDemo do after returning...."
@AfterThrowing("execution(* com.wmh.springaop.controller..*(..))")
public
void
doAfterThrowing
()
"AspectDemo do after throwing...."
TestController
package com.wmh.springaop.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1(){
log.info("执行 t1 方法...");
return "t1";
}
@RequestMapping("/t2")
public String t2(){
log.info("执行 t2 方法...");
int a = 10/0;
return "t2";
}
}
通知类型的执行顺序
由此我们知道,发生异常的情况下通知类型的执行顺序是:
从上面我们可以看到,当发生异常时@AfterReturning 表示的通知方法不会执行了,@AfterThrowing 表示的通知方法执行了。
而且,当@Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑就不会执行了,因为原始方法调用出异常了。
关于@Around 表示的方法返回值问题
当@Around 标识的方法有返回值且类型为 Object 时:
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class AspectDemo {
@Around("execution(* com.wmh.springaop.controller..*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("AspectDemo do around before....");
Object result = joinPoint.proceed();
log.info("AspectDemo do around after...");
return result;
}
}
package com.wmh.springaop.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1(){
log.info("执行 t1 方法...");
return "t1";
}
}
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class AspectDemo {
@Around("execution(* com.wmh.springaop.controller..*(..))")
public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("AspectDemo do around before....");
Object result = joinPoint.proceed();
log.info("AspectDemo do around after...");
}
}
• @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行。
• @Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
• 一个切面类可以有多个切点。
@Pointcut
上面代码存在一个问题,就是存在大量重复的切点表达式 execution(* com.wmh.springaop.controller..*(..)),Spring 提供了 @Pointcut 注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可。
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class AspectDemo {
@Pointcut("execution(* com.wmh.springaop.controller..*(..))")
public void pt(){};
@Before("pt()")
public void doBefore(){
log.info("AspectDemo do before....");
}
@After("pt()")
public void doAfter(){
log.info("AspectDemo do after....");
}
@Around("pt()")
public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("AspectDemo do around before....");
Object result = joinPoint.proceed();
log.info("AspectDemo do around after...");
}
@AfterReturning("pt()")
public void doAfterReturning(){
log.info("AspectDemo do after returning....");
}
@AfterThrowing("pt()")
public void doAfterThrowing(){
log.info("AspectDemo do after throwing....");
}
}
当切点定义使用 private 修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把 private 改为 public。引用方式为:全限定类名。方法名()
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
@Order(1)
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Before("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doBefore(){
log.info("AspectDemo2 do before....");
}
@After("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doAfter(){
log.info("AspectDemo2 do after....");
}
}
切面优先级@Order
当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?我们还是通过程序来求证:
定义多个切面类:
为简单化,只写了 @Before 和 @After 两个通知。
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Before("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doBefore(){
log.info("AspectDemo2 do before....");
}
@After("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doAfter(){
log.info("AspectDemo2 do after....");
}
}
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class AspectDemo3 {
@Before("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doBefore(){
log.info("AspectDemo3 do before....");
}
@After("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doAfter(){
log.info("AspectDemo3 do after....");
}
}
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class AspectDemo4 {
@Before("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doBefore(){
log.info("AspectDemo4 do before....");
}
@After("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doAfter(){
log.info("AspectDemo4 do after....");
}
}
package com.wmh.springaop.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1(){
log.info("执行 t1 方法...");
return "t1";
}
}
通过上述程序的运行结果,可以看出:
存在多个切面类时,默认按照切面类的类名字母排序:
• @Before 通知:字母排名靠前的先执行
• @After 通知:字母排名靠前的后执行
但这种方式不方便管理,我们的类名更多还是具备一定的含义的。
Spring 给我们提供了一个新的注解,来控制这些切面通知的执行顺序:@Order
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(1)
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Before("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doBefore(){
log.info("AspectDemo2 do before....");
}
@After("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doAfter(){
log.info("AspectDemo2 do after....");
}
}
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(100)
@Slf4j
@Component
@Aspect
public class AspectDemo3 {
@Before("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doBefore(){
log.info("AspectDemo3 do before....");
}
@After("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doAfter(){
log.info("AspectDemo3 do after....");
}
}
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(25)
@Slf4j
@Component
@Aspect
public class AspectDemo4 {
@Before("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doBefore(){
log.info("AspectDemo4 do before....");
}
@After("com.wmh.springaop.aspect.AspectDemo.pt()")
public void doAfter(){
log.info("AspectDemo4 do after....");
}
}
通过上述程序的运行结果,得出结论:
@Order 注解标识的切面类,执行顺序如下:
• @Before 通知:数字越小先执行
• @After 通知:数字越大先执行
@Order 控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。
切点表达式
上面的代码中,我们一直在使用切点表达式来描述切点。下面我们来介绍一下切点表达式的语法。
切点表达式常见有两种表达方式:
- execution(RR):根据方法的签名来匹配
- @annotation(RR):根据注解匹配
execution 表达式
execution() 是最常用的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符> <返回类型> <包名。类名。方法 (方法参数)> <异常>)
-
- :匹配任意字符,只匹配一个元素 (返回类型,包,类名,方法或者方法参数)
a. 包名使用 * 表示任意包 (一层包使用一个*)
b. 类名使用 * 表示任意类
c. 返回值使用 * 表示任意返回值类型
d. 方法名使用 * 表示任意方法
e. 参数使用 * 表示一个任意类型的参数
- .. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
a. 使用 .. 配置包名,标识此包以及此包下的所有子包
b. 可以使用 .. 配置参数,任意个任意类型的参数
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.*())
execution(* com.example.demo.controller.TestController.*(..))
匹配 controller 包下所有的类的所有方法
execution(* com.example.demo.controller...(..))
execution(* com..TestController.*(..))
匹配 com.example.demo 包下,子包下的所有类的所有方法
execution(* com.example.demo..*(..))
annotation 表达式
execution 表达式更适用有规则的,如果我们要匹配多个无规则的方法呢,比如:TestController 中的 t1() 和 UserController 中的 u1() 这两个方法。
这个时候我们使用 execution 这种切点表达式来描述就不是很方便了。
我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点。
- 编写自定义注解
- 使用 @annotation 表达式来描述切点
- 在连接点的方法上添加自定义注解
package com.wmh.springaop.config;
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 指 Annotation 被保留的时间长短,标明注解的生命周期
@Retention 的取值有三种:
- RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。比如 @SuppressWarnings,以及 lombok 提供的注解 @Data,@Slf4j
- RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取。通常用于一些框架和工具的注解。
- RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时中。这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如 Spring 的 @Controller @ResponseBody
使用 @annotation 切点表达式定义切点,只对 @MyAspect 生效
切面类代码如下:
package com.wmh.springaop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
@Around("@annotation(com.wmh.springaop.config.MyAspect)")
public Object doAround(ProceedingJoinPoint joinPoint){
log.info("do around before...");
Object o = null;
try {
o = joinPoint.proceed();
} catch (Throwable e) {
log.error("发生异常,e:", e);
}
log.info("do around after...");
return o;
}
}
package com.wmh.springaop.controller;
import com.wmh.springaop.config.MyAspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
@MyAspect
@RequestMapping("/t1")
public String t1(){
log.info("执行 t1 方法...");
return "t1";
}
@RequestMapping("/t2")
public String t2(){
log.info("执行 t2 方法...");
int a = 10/0;
return "t2";
}
}
package com.wmh.springaop.controller;
import com.wmh.springaop.config.MyAspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1(){
log.info("执行 u1 方法...");
return "u1";
}
@MyAspect
@RequestMapping("/u2")
public String u2(){
log.info("执行 u2 方法...");
return "u2";
}
}