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

Spring AOP 实战案例+避坑指南

目录

1. AOP的本质:

1.1. 示例:电商“下单接口”

1.2. 真实的业务场景:3个高频的AOP用法

1.2.1. 场景1:接口统一日志(最常用)

1.2.2. 场景2:接口统一权限校验

1.2.3. 场景3:全局异常统一处理(AOP变种)

1.2.3.1. 为什么此处是AOP?

1.2.4. Spring AOP常用注解:

1.2.5. 代码演示仓库:

2. AOP底层原理:动态代理

2.1. JDK代理实例:

2.2. CGLIB动态代理实例:

3. AOP实战坑点:

3.1. this调用不触发Spring AOP

3.1.1. 无法被拦截代码演示:

3.1.2. 可以被拦截演示

3.1.3. 总结:

3.2. 切面执行顺序混乱

3.3. 切入点表达式写错,切不到方法


1. AOP的本质:

AOP本质上其实不是“高大上的概念”,而是“解耦的工具”。其实AOP的核心目的其实就是把“业务逻辑”和“通用功能(如日志、权限、异常处理)”分开写,避免代码冗余。

1.1. 示例:电商“下单接口”

核心业务:“扣库存、生成订单”,此外我们还需要通用功能:

  • 记录请求参数和响应结果(日志)
  • 校验用户是否登陆(权限)
  • 出现异常时统一返回格式(异常处理)

如果我们不使用AOP,那么代码会变成这样:

/*** @className: OrderController* @author: 顾漂亮* @date: 2025/10/17 15:44*/
@RestController
public class OrderController {@PostMapping("/order/create")public Result createOrder(@RequestBody OrderReq req) {// 1. 日志:记录请求(每个接口都要写)log.info("下单请求:{}", JSON.toJSONString(req));try {// 2. 权限:校验登录(每个接口都要写)if (UserContext.getCurrentUser() == null) {return Result.fail("未登录");}// 3. 核心业务:扣库存、生成订单orderService.create(req);// 4. 日志:记录响应(每个接口都要写)log.info("下单成功:{}", req.getOrderId());return Result.success();} catch (Exception e) {// 5. 异常处理:统一返回(每个接口都要写)log.error("下单失败:{}", e.getMessage());return Result.fail("下单失败");}}
}

使用AOP的代码(业务更纯粹):

@RestController
public class OrderController {@PostMapping("/order/create")public Result createOrder(@RequestBody OrderReq req) {// 只留核心业务:扣库存、生成订单orderService.create(req);return Result.success();}
}

总结:AOP的价值:让业务代码变得更纯粹,通用功能可复用、可配置

1.2. 真实的业务场景:3个高频的AOP用法

1.2.1. 场景1:接口统一日志(最常用)
package com.project.springaopdemo.aspect;import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;/*** @className: LogAspect* @author: 顾漂亮* @date: 2025/10/17 16:07*/
@Aspect
@Component
@Slf4j
public class LogAspect {/*** 定义切点,拦截controller包下所有公共方法的执行*/@Pointcut("execution(public * com.project.springaopdemo.controller..*.*(..))")public void logPointcut() {}/*** 环绕通知,记录方法执行时间和结果* * @param joinPoint 连接点* @return 方法执行结果* @throws Throwable 可能抛出的异常*/@Around("logPointcut()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();Object result = joinPoint.proceed();long cost = System.currentTimeMillis() - start;log.info("耗时: {}ms, 结果: {}", cost, JSONUtil.toJsonStr(result));return result;}
}
1.2.2. 场景2:接口统一权限校验
package com.project.springaopdemo.aspect;import com.project.springaopdemo.util.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;/*** 这个切面的作用是:当任何被@NeedLogin注解标记的方法被调用时,会先执行登录检查,确保用户已登录,否则阻止方法执行并抛出异常。*/
@Aspect //标识这是一个切面类,用于定义横切关注点
@Component // 将该类注册为Spring容器管理的组件
@Order(1) // 设置切面执行优先级,数值越小优先级越高
@Slf4j // 使用Lombok的@Slf4j注解,自动生成日志对象log
public class AuthAspect {//@Pointcut: 定义切点,这里使用@annotation表达式匹配被@NeedLogin注解标记的方法@Pointcut("@annotation(com.project.springaopdemo.annotation.NeedLogin)")public void authPointcut() {}//@Before: 前置通知注解,在目标方法执行前执行@Before("authPointcut()")public void checkLogin() {if (!UserContext.getUser().getUsername().equals("ghr") || !UserContext.getUser().getPassword().equals("123456")) {throw new RuntimeException("未登录");}else{log.info("用户已登录");}}
}
1.2.3. 场景3:全局异常统一处理(AOP变种)
package com.project.springaopdemo.exception;import com.project.springaopdemo.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;/*** @className: GlobalExceptionHandler* @author: 顾漂亮* @date: 2025/10/17 16:16*/
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class)public Result<String> handleException(RuntimeException e) {log.error("异常: ", e);return Result.fail(e.getMessage());}
}
1.2.3.1. 为什么此处是AOP?

@ControllerAdvice本质上是Spring AOP的一种特殊形式,对所有Controller方法的异常进行“切面拦截”

1.2.4. Spring AOP常用注解:

注解

所在包

作用

使用位置

@Aspect

org.aspectj.lang.annotation.Aspect

标识一个类是“切面类”

类上

@Pointcut

org.aspectj.lang.annotation.Pointcut

定义切入点表达式(即“切哪些方法”)

方法上(通常为空方法)

@Before

org.aspectj.lang.annotation.Before

前置通知:目标方法执行执行

方法上

@After

org.aspectj.lang.annotation.After

后置通知:目标方法执行执行(无论是否异常)

方法上

@AfterReturning

org.aspectj.lang.annotation.AfterReturning

返回通知:目标方法成功返回后执行

方法上

@AfterThrowing

org.aspectj.lang.annotation.AfterThrowing

异常通知:目标方法抛出异常后执行

方法上

@Around

org.aspectj.lang.annotation.Around

环绕通知:完全控制目标方法的执行(最强大)

方法上

@Order

org.springframework.core.annotation.Order

控制多个切面的执行顺序(值越小,优先级越高)

切面类或通知方法上

1.2.5. 代码演示仓库:

大家可以直接访问我的gitee仓库拉取我的项目便于大家进行调试

https://gitee.com/ghr-Hayden/java-project/tree/master/AOPDemo/SpringAOPDemo

2. AOP底层原理:动态代理

SpringAOP是基于动态代码实现的!

代理方式

原理

限制

适用场景

JDK动态代理

基于接口+InvocationHandler

只能代理实现了接口的类

Spring默认代理逻辑(目标类有接口)

CGLIB动态代理

基于继承(生成子类)

不能代理final类/方法

目标类无接口时自动使用

2.1. JDK代理实例:

package com.project.springaopdemo.test;import com.project.springaopdemo.model.OrderReq;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;/*** @className: ProxyTest* @author: 顾漂亮* @date: 2025/10/17 17:02*/
interface OrderService {void createOrder(OrderReq req);
}class OrderServiceImpl implements OrderService {@Overridepublic void createOrder(OrderReq req) {System.out.println("核心业务:生成订单");}
}class LogInvocationHandler implements InvocationHandler {private final Object target;public LogInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("【AOP】方法调用前");Object result = method.invoke(target, args);System.out.println("【AOP】方法调用后");return result;}
}// 测试
public class ProxyTest {public static void main(String[] args) {OrderService target = new OrderServiceImpl();OrderService proxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new LogInvocationHandler(target));proxy.createOrder(new OrderReq());}
}

2.2. CGLIB动态代理实例:

package com.project.springaopdemo.test;import com.project.springaopdemo.model.OrderReq;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** @className: CglibProxyTest* @author: 顾漂亮* @date: 2025/10/17 17:07*/// 没有接口,只是一个普通类
class OrderService {public void createOrder(OrderReq req) {System.out.println("核心业务:生成订单");}}
/*** CGLIB代理类,实现MethodInterceptor接口,用于拦截目标对象的方法调用*/
class LogMethodInterceptor implements MethodInterceptor {private final Object target;public LogMethodInterceptor(Object target) {this.target = target;}@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("【CGLIB AOP】方法调用前");Object result = method.invoke(target, args); // 调用原始对象方法System.out.println("【CGLIB AOP】方法调用后");return result;}
}
/*** CGLIB代理测试类*/
public class CglibProxyTest {public static void main(String[] args) {// 创建目标对象(无接口)OrderService target = new OrderService();// 创建 EnhancerEnhancer enhancer = new Enhancer();enhancer.setSuperclass(OrderService.class); // 设置父类(目标类)enhancer.setCallback(new LogMethodInterceptor(target));// 创建代理对象OrderService proxy = (OrderService) enhancer.create();// 调用代理方法proxy.createOrder(new OrderReq());}
}

3. AOP实战坑点:

3.1. this调用不触发Spring AOP

3.1.1. 无法被拦截代码演示:
package com.project.thisdemo.service;import com.project.thisdemo.annotation.LogExecution;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;/*** @className: OrderSevice* @author: 顾漂亮* @date: 2025/10/17 19:45*/
@Service
public class OrderService {@LogExecutionpublic void methodA() {System.out.println("methodA 被调用,this = " + this.getClass().getName());this.methodB();}@LogExecution // 自定义注解,用于 AOP 拦截public void methodB() {System.out.println("methodB 被调用,this = " + this.getClass().getName());}
}
package com.project.thisdemo.aspect;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;/*** @className: LogAspect* @author: 顾漂亮* @date: 2025/10/17 19:46*/
@Aspect
@Component
public class LogAspect {@Around("@annotation(com.project.thisdemo.annotation.LogExecution)")public Object log(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("【Spring AOP】拦截到方法: " + joinPoint.getSignature().getName());return joinPoint.proceed();}
}
package com.project.thisdemo.annotation;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 LogExecution {
}
package com.project.thisdemo;import com.project.thisdemo.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class ThisDemoApplicationTests {@Autowiredprivate OrderService orderService; // 实际是 Spring 生成的代理对象@Testpublic void testThisCall() {orderService.methodA(); // 通过代理调用 methodA}}
【Spring AOP】拦截到方法: methodA
methodA 被调用,this = com.project.thisdemo.service.OrderService
methodB 被调用,this = com.project.thisdemo.service.OrderService

根据上述的日志其实我们可以分析出来,methodB实际上其实并没有被SpringAOP拦截

3.1.2. 可以被拦截演示
package com.project.thisdemo.service;import com.project.thisdemo.annotation.LogExecution;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;/*** @className: OrderSevice* @author: 顾漂亮* @date: 2025/10/17 19:45*/
@Service
public class OrderService {@LogExecutionpublic void methodA() {System.out.println("methodA 被调用,this = " + this.getClass().getName());((OrderService) AopContext.currentProxy()).methodB(); // 通过代理调用}@LogExecution // 自定义注解,用于 AOP 拦截public void methodB() {System.out.println("methodB 被调用,this = " + this.getClass().getName());}
}
@SpringBootApplication
/** @EnableAspectJAutoProxy - 这个是启动Spring AOP的核心开关,告诉Spring:"我要用AOP功能了,你帮我准备好"* proxyTargetClass = true - 这个参数是说"强制使用CGLIB代理方式"* Spring AOP有两种代理方式:JDK动态代理和CGLIB代理* JDK代理只能代理接口,CGLIB可以直接代理类* 设置为true就是强制用CGLIB,不管有没有接口都用这种方式* exposeProxy = true - 这个参数意思是"把代理对象暴露出来"* 默认情况下,在一个被代理的对象内部,你拿不到代理对象本身* 设置为true后,就可以通过AopContext.currentProxy()来获取当前的代理对象* 这样就能解决之前碰到的"this调用不走AOP"的问题* 简单来说,这个注解就是告诉Spring:"我要开启AOP功能,强制用CGLIB代理方式,并且让我能在对象内部拿到代理对象"。*/
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) // 强制 CGLIB并暴露代理
public class ThisDemoApplication {public static void main(String[] args) {SpringApplication.run(ThisDemoApplication.class, args);}}
3.1.3. 总结:

为什么SpringAOP无法代理this?

其实本质上是因为SpringAOP基于代理模式实现,外部调用目标方法的时候,实际上使用的是代理对象调用的方法,this指向的是原始对象,调用直接发生在原始对象上,没有经过代理对象,所以相当于绕过了SpringAOP这个机制!

路径对比:

  • AOP生效:
外部调用 -> 代理对象.methodA() -> 原始对象.methodA()
  • AOP不生效:
原始对象.methodA() -> this.methodB() (直接调用原始对象,绕过代理)

3.2. 切面执行顺序混乱

场景:日志切面和权限切面都切同一个接口,想让权限切面先执行(先校验登录,再记录日志),结果顺序反了。

解决:用@Order注解指定顺序,数字越小,优先级越高(如@Order(1)的权限切面先执行,@Order(2)的日志切面后执行)。

3.3. 切入点表达式写错,切不到方法

场景:想切所有 Controller 的方法,表达式写成execution(* com.xxx.controller.*(..)),结果子包下的 Controller 没被切到。

原因:com.xxx.controller.*只切 “controller 包下的类”,不切 “子包下的类”。解决:用com.xxx.controller..*(两个点)表示 “controller 包及所有子包下的类”。

http://www.dtcms.com/a/502980.html

相关文章:

  • 第三章 栈和队列——课后习题解练【数据结构(c语言版 第2版)】
  • Kubernetes Ingress与安全机制
  • 【企业架构】TOGAF架构标准规范-机会与解决方案
  • apache建设本地网站wordpress修改成中文字体
  • windows平台,用pgloader转换mysql到postgresql
  • Linux驱动第一期1-10-驱动基础总结
  • 我的WordPress网站梅林固件做网站
  • 分库分表:基础介绍
  • 使用css `focus-visible` 改善用户体验
  • AI人工智能-深度学习的基本原理-第二周(小白)
  • 【2070】数字对调
  • 【AI智能体】Coze 提取对标账号短视频生成视频文案实战详解
  • IOT项目——ESP系列
  • 【成长纪实】Dart 与 ArkTS 函数与类的对比学习:从 Flutter 到 HarmonyOS
  • 基于 JETSON+FPGA+GMSL+AI 车载视频采集与存储系统设计(二)系统测试
  • Flutter Event Loop
  • LeetCode 1287.有序数组中出现次数超过25%的元素
  • 递归-50.Pow(x,n)-力扣(LeetCode)
  • Flutter 并发编程全解:从零掌握 Isolate
  • 跨网络互联技术(①Singbox Core-Flutter-Android)
  • 移动端网站的重要性做一款推荐类的网站
  • 用div做网站中间部分做网站是干啥的
  • 【思维导图SimpleMind Pro】SimpleMind Pro——轻量级思维导图软件完全指南:轻量专业,让思维高效可视化
  • jlink烧入软件的使用
  • 03-流程控制语句-教程
  • 【mqtt参数上云和小程序开发】【第5期】hcsr04超声波测距模块调试完成
  • 【STM32笔记】:P03 ISP 一键下载电路详解
  • 江苏省网站建设哪家好php做的购物网站
  • 常见的位运算的总结
  • CSRF(跨站请求伪造)攻击详解:原理、途径与防范