
目录
[1. AOP的本质:](#1. AOP的本质:)
[1.1. 示例:电商"下单接口"](#1.1. 示例:电商“下单接口”)
[1.2. 真实的业务场景:3个高频的AOP用法](#1.2. 真实的业务场景:3个高频的AOP用法)
[1.2.1. 场景1:接口统一日志(最常用)](#1.2.1. 场景1:接口统一日志(最常用))
[1.2.2. 场景2:接口统一权限校验](#1.2.2. 场景2:接口统一权限校验)
[1.2.3. 场景3:全局异常统一处理(AOP变种)](#1.2.3. 场景3:全局异常统一处理(AOP变种))
[1.2.3.1. 为什么此处是AOP?](#1.2.3.1. 为什么此处是AOP?)
[1.2.4. Spring AOP常用注解:](#1.2.4. Spring AOP常用注解:)
[1.2.5. 代码演示仓库:](#1.2.5. 代码演示仓库:)
[2. AOP底层原理:动态代理](#2. AOP底层原理:动态代理)
[2.1. JDK代理实例:](#2.1. JDK代理实例:)
[2.2. CGLIB动态代理实例:](#2.2. CGLIB动态代理实例:)
[3. AOP实战坑点:](#3. AOP实战坑点:)
[3.1. this调用不触发Spring AOP](#3.1. this调用不触发Spring AOP)
[3.1.1. 无法被拦截代码演示:](#3.1.1. 无法被拦截代码演示:)
[3.1.2. 可以被拦截演示](#3.1.2. 可以被拦截演示)
[3.1.3. 总结:](#3.1.3. 总结:)
[3.2. 切面执行顺序混乱](#3.2. 切面执行顺序混乱)
[3.3. 切入点表达式写错,切不到方法](#3.3. 切入点表达式写错,切不到方法)
1. AOP的本质:
AOP本质上其实不是"高大上的概念",而是"解耦的工具"。其实AOP的核心目的其实就是把"业务逻辑"和"通用功能(如日志、权限、异常处理)"分开写,避免代码冗余。
1.1. 示例:电商"下单接口"
核心业务:"扣库存、生成订单",此外我们还需要通用功能:
- 记录请求参数和响应结果(日志)
- 校验用户是否登陆(权限)
- 出现异常时统一返回格式(异常处理)
如果我们不使用AOP,那么代码会变成这样:
java
/**
* @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:接口统一日志(最常用)
java
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:接口统一权限校验
java
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变种)
java
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代理实例:
java
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 {
@Override
public void createOrder(OrderReq req) {
System.out.println("核心业务:生成订单");
}
}
class LogInvocationHandler implements InvocationHandler {
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public 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动态代理实例:
java
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;
}
@Override
public 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();
// 创建 Enhancer
Enhancer 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. 无法被拦截代码演示:
java
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 {
@LogExecution
public 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());
}
}
java
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();
}
}
java
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 {
}
java
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 {
@Autowired
private OrderService orderService; // 实际是 Spring 生成的代理对象
@Test
public void testThisCall() {
orderService.methodA(); // 通过代理调用 methodA
}
}
java
【Spring AOP】拦截到方法: methodA
methodA 被调用,this = com.project.thisdemo.service.OrderService
methodB 被调用,this = com.project.thisdemo.service.OrderService
根据上述的日志其实我们可以分析出来,methodB实际上其实并没有被SpringAOP拦截
3.1.2. 可以被拦截演示
java
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 {
@LogExecution
public 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());
}
}
java
@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生效:
java
外部调用 -> 代理对象.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 包及所有子包下的类"。