一、概述
-
定义
: AOP (Aspect Oriented Programming 面向切面编程) ,一种面向方法编程的思想 -
功能
:管理 bean 对象的过程中,通过底层的动态代理机制对特定方法进行功能的增强或改变 -
实现方式
:动态代理技术,即创建目标对象的代理对象,并对目标对象中的方法的功能进行增强的技术 -
优点
- 代码无侵入:不修改原有代码的基础上对原有方法的功能进行增强
- 减少重复代码:一次性对一类方法进行功能增强
- 提高开发效率
- 维护方便
-
AOP 的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
代理方式
- 没有切面的对象:不需要代理,Spring 会直接注入目标对象(原始类的实例)
- **接口类型的目标对象:**Spring 使用 JDK 动态代理 创建代理对象
- 非接口类型的目标对象 (如纯类):Spring 则使用 CGLIB(子类代理) 创建代理对象
二、核心概念
JoinPoint
: 连接点,客观可以被 Proxy 的方法(实际上所有的方法都是 JoinPoint,都可以被代理)PointCut
: 切入点,主观需要被 Proxy 的方法(满足匹配条件的 JoinPoint),实际被 Advice 控制的方法Advice
: 通知,共性的功能 (最终体现为一个方法),是对目标 PointCut 的相同处理逻辑Aspect
: 切面,即 PointCut + Advice ,描述 "通知" 与 "切入点" 对应关系Target
: Advice 作用的目标对象,PointCut 所在的类
Aspect 类 (AOP 类)
- 定义 : 切面,即 PointCut + Advice ,描述 "通知" 与 "切入点" 对应关系
- 功能:用于实现特定切面功能,动态代理某些目标类或目标方法的切面类
- 组成
- Aspect 类:通过 @Aspect 注解标注,告知 Spring 这是一个 AOP 类
- PointCut 切入点:通过 @PointCut 注解标注,指定代理的目标方法,减少 Advice 类型注解中的代码冗余
- Advice 方法:通过 Advice 注解标注(如 @Before、@After、@Around ),告知 Spring 这个 Aspect 方法将代理哪些目标方法
PointCut (切入点)
- 定义 : 切入点,主观需要被 Proxy 的方法(满足匹配条件的 JoinPoint),实际被 Advice 控制的方法
- 功能:用于指定 Aspect 代理的具体方法,抽取出公共切入点减少代码冗余(实际上也可以不单独声明,直接在 Advice 里面声明)
- 实现方式:通过 execution 和 annotation 两种方式声明
- 书写建议
- 建议抽取公共的 PointCut 进行复用(也可以不声明单独的 PointCut,而是在 Advice 方法上的注解中声明 PointCut)
- 尽量缩小切入点范围,(比如 : 尽量不用 .. 进行包名匹配,因为范围越大匹配效率越低)
- 基于接口描述切入点,而不是直接描述实现类,增强拓展性
execution 表达式
-
格式
execution ( [访问修饰符] 返回值 [包名.类名.] 方法名(方法形参) [throws 异常] )
-
使用方法
- 将目标方法 (被代理的方法) 写到 execution 表达式中
- 在 Aspect 类中定义一个空参空返回值方法,给其加上 @PointCut( "execution ..." ) 注解
-
书写格式
*
:通配1个或0个单独的任意符号,可以通配返回值、包名、类名、方法名、任意类型的一个参数,或者包名、类名、方法名的一部分..
:通配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数|| && !
:可以用逻辑运算符组合复杂的 PointCut 表达式
-
示例
@Pointcut("execution(* com.tlias.service.impl.DeptServiceImpl.*(..))") private void myPointCut();
- 第一个 * :前面为任意类型的返回值
- 包名: PointCut 所在的包名类名方法名
- 最后一个 * :为任意方法
- (..) :为任意形参
annotation 表达式
-
格式
@annotation(包名.类名.注解名)
-
使用方法
- 在目标方法 (被代理的方法) 上加上自定义的注解 @MyPointCut
- 在 Aspect 类中定义一个空参空返回值方法,给其加上 @PointCut( "@annotation(com.xxx.aop.MyPointCut)" ) 注解
- 示例
-
目标:自定义 annotation,被这个 annotation 标注的方法都会被 Aspect 类代理
-
annotation
@Target(ElementType.METHOD) // 设置注解使用的位置,ElementType.METHOD 表示这个注解用来标注方法 @Retention(RetentionPolicy.RUNTIME) // 设置注解生效的时间,RetentionPolicy.RUNTIME 表示运行时生效 public @interface MyPointCut { }
-
PointCut
@Slf4j @Component @Aspect public class MyAspect{ @Pointcut("execution(* com.tlias.service.impl.DeptServiceImpl.*(..)) && @annotation(com.tlias.annotation.MyPointCut)") private void myPointCut(); @Around("myPointCut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ log.info("around before ..."); Object result = proceedingJoinPoint.proceed(); log.info("around after ..."); return result; } }
-
Advice (通知)
-
定义:通知(Advice)是自定义的处理逻辑,当被 PointCut 选中的方法在指定时机(Before/Around/After)执行时会触发对应的 Advice 方法
-
功能 : 明确 PointCut 指定的方法的切面处理逻辑,此方法中会通过 ProceedingJoinPoint 类来代理目标方法
-
基本类型
注解类型 说明 @Before
前置通知,Advice 方法在目标方法开始前执行 @Around
环绕通知,Advice 方法在目标方法前后都会执行,期间需要通过 proceed() 方法主动调用目标方法 @After
后置通知,Advice 方法在目标方法结束后执行,有无异常都会执行 @AfterReturning
返回后通知,Advice 方法在目标方法后被执行,有异常则不会执行 @AfterThrowing
异常后通知,Advice 方法在目标方法发生异常后执行 -
对比总结
切面注解 运行时机 可修改目标方法行为 可获取返回值 可获取异常 @Before
目标方法执行前 否 否 否 @Around
方法执行前、执行后均可运行 ☑️ ☑️ ☑️ @After
目标方法执行后(无论成功或失败) 否 否 否 @AfterReturning
目标方法成功返回后 否 ☑️ 否 @AfterThrowing
目标方法抛出异常时 否 否 ☑️ -
自定义优先级:通过
@Order(x)
给 Advice 指定优先级,注解中 x 越大,则 Before 越先执行,After 越后执行
-
示例
@Slf4j @Component @Aspect public class MyAspect{ // 声明一个 PointCut 切入点, 用于后续 Advice 方法中的切入点复用 @Pointcut("execution(* com.tlias.service.impl.DeptServiceImpl.*(..)) && @annotation(com.tlias.annotation.MyPointCut)") private void myPointCut(); @Before("myPointCut()") public void before(){ log.info("before..."); } @Around("myPointCut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ log.info("around before ..."); Object result = proceedingJoinPoint.proceed(); log.info("around after ..."); return result; } @After("myPointCut()") public void after(){ log.info("after ..."); } @AfterReturning("myPointCut()") public void afterReturning(){ log.info("afterReturning ..."); } @AfterThrowing("myPointCut()") public void afterThrowing(){ log.info("afterThrowing ..."); } }
JoinPoint(连接点)
-
定义
- JoinPoint:目标对象中所有可以被增强的方法。这些方法在运行时都可以被 AOP 框架拦截并添加额外的处理逻辑
- ProceedingJoinPoint:继承自 JoinPoint 类,专门给环绕通知 (Around) 使用的类,用于代理原有方法的类 (对被拦截方法的一个包装)
-
功能:拿到 ProceedingJoinPoint 类相当于拿到了原方法,可以调用 proceed() 方法执行原方法
-
常用方法
方法 说明 proceed()
执行被代理的方法 getArgs()
获取传递给目标方法的参数 getSignature()
获取被拦截方法的信息,如方法名、返回类型等(通过反射获取) getTarget()
获取被拦截方法所属的目标对象(Target Object) getThis()
获取代理对象(Proxy Object)
-
示例
@Around("execution (* com.tlias.service.*.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ // 获取目标对象类名 String className = joinPoint.getTarget().getClass().getName(); log.info("目标对象的类名:{}", className); // 获取目标方法的方法名 String methodName = joinPoint.getSignature().getName(); log.info("目标方法的方法名:{}", methodName); // 获取目标方法运行时传入的参数 Object[] args = joinPoint.getArgs(); log.info("目标方法运行时传入的参数:{}", Arrays.toString(args)); // 获取目标方法运行的返回值 Object result = joinPoint.proceed(); log.info("目标方法运行的返回值:{}", result); // 最后一定要将结果返回回去,因为此时 around 函数代理了 JoinPoint 函数 // 不返回的话 controller 层拿不到返回结果 return result; }
三、执行流程
@Around 流程
- 声明切入点:Aspect 类声明将会代理的目标方法(根据 @Around 注解配置的 PointCut 信息决定代理哪些方法)
- 依赖注入:Spring 通过动态代理技术,注入代理对象 XxxServiceProxy(如果没有配置 Aspect,则直接注入目标对象 XxxService)
- 接收请求:服务器接收客户端 "调用了被代理的方法(目标方法)" 的请求
- 代理方法:XxxServiceProxy 代理类接收请求,通过 ProceedingJoinPoint 参数获取 PointCut 方法的上下文信息
- 执行前置代理逻辑:运行 Aspect 前置代码部分 ( Advice 部分 )
- 执行目标方法:通过 joinPoint.proceed() 执行被代理的方法,同时用 Object result 接收被代理方法的返回值
- 执行后置代理逻辑:运行 Aspect 后置代码部分 ( Advice 部分 )
- 返回结果值
@Before / @After 流程
- 声明切入点:Aspect 类声明将会代理的目标方法(根据 @Before、@After 等注解配置的 PointCut 信息决定代理哪些方法)
- 依赖注入:Spring 通过动态代理技术,注入代理对象 XxxServiceProxy(如果没有配置 Aspect,则直接注入目标对象 XxxService)
- 接收请求:服务器接收客户端 "调用了被代理的方法(目标方法)" 的请求
- 代理方法:XxxServiceProxy 接收请求
- 运行 Before Advice:运行 @Before 注解的方法逻辑
- 执行目标方法:代理对象自动执行目标方法
- 运行 After Advice:运行 @After 注解的方法逻辑
- 返回结果值
四、使用场景
- 记录操作日志
- 权限控制
- 事务管理
- 自动填充数据库字段
五、示例
方法用时统计
- 目标 : 统计各个业务层方法执行耗时
- 步骤
-
导入依赖:在 pom.xml 中导入 AOP 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
创建包和类 : com.tlias.aop.TimeAspect
-
编写 AOP 程序
@Aspect // 声明这是一个AOP类 @Component public class TimeAspect{ @PointCut("execution (* com.tlias.service.*.*(..))") private void myPointCut(){} @Around("myPointCut()") // 声明这个Aspect将代理哪些方法 public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ long begin = System.currentTimeMills(); //1. 记录开始时间 Object result = proceedingJoinPoint.prceed(); //2. 调用被代理的方法 long end = System.currentTimeMills(); //3. 记录结束时间 log.info(prceedingJoinPoint.getSignature() + "方法执行共耗时" + end-begin + "毫秒"); //4. 计算时间差并记录 return result; }
-
用户权限校验
- 目标 : 保证 @authCheck 注解标注的方法都必须具备特定权限才可调用该方法
- 步骤
-
导入依赖:在 pom.xml 中导入 AOP 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
编写注解(com.yupi.yudada.annotation.AuthCheck)
@Target(ElemType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthCheck { /** * 被 AuchCheck 标注就必须具备的用户权限等级 * 示例: @AuthCheck("ADMIN") 表示当前方法必须拥有管理员权限才可执行 **/ String mustRole() default "";
-
编写 AOP 程序(com.yupi.yudada.aop.AuthInterceptor)
@Aspect // 声明这是一个AOP类 @Component public class AuthInterceptor{ @Resource private UserService userService; @Around("@annotation(authCheck)") public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { String mustRole = authCheck.mustRole(); RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); } }
-