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常用注解:
注解 | 所在包 | 作用 | 使用位置 |
|
| 标识一个类是“切面类” | 类上 |
|
| 定义切入点表达式(即“切哪些方法”) | 方法上(通常为空方法) |
|
| 前置通知:目标方法执行前执行 | 方法上 |
|
| 后置通知:目标方法执行后执行(无论是否异常) | 方法上 |
|
| 返回通知:目标方法成功返回后执行 | 方法上 |
|
| 异常通知:目标方法抛出异常后执行 | 方法上 |
|
| 环绕通知:完全控制目标方法的执行(最强大) | 方法上 |
|
| 控制多个切面的执行顺序(值越小,优先级越高) | 切面类或通知方法上 |
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 包及所有子包下的类”。