Spring AOP面向切面编程-从注解到实战
目录
一、学习背景与目标
1.1 学习背景
1.2 学习目标
二、核心概念与原理
2.1 AOP 是什么
2.2 五大核心概念
2.3 AOP 的底层原理
2.4 五种通知类型
三、逻辑图谱与对比
3.1 AOP 实现技术图谱
3.2 注解 AOP vs XML AOP
3.3 决策流程图
四、核心详解
4.1 基础环境搭建
4.2 最标准注解 AOP 模板
4.3 JoinPoint vs ProceedingJoinPoint
4.4 Pointcut 表达式详解
execution —— 最常用,按方法签名匹配
within —— 按类匹配(更快)
@annotation —— 按注解匹配(最灵活,推荐用于自定义注解场景)
this / target —— 按代理对象类型匹配
bean —— 按 Spring Bean 名称匹配
组合表达式
4.5 多切面排序
4.6 XML 配置 AOP(传统方式,了解即可)
五、四大实战案例
5.1 案例:自定义注解实现接口日志切面(最实战)
5.2 案例:接口性能监控 + 慢请求告警
5.3 案例:注解方式实现权限校验
5.4 案例:方法重试切面
六、常见坑与最佳实践
6.1 易错点清单
6.2 内部调用自注入解决方案
6.3 最佳实践清单
七、总结与学习路线图
7.1 知识点提炼
核心口诀
核心要点速览
自检清单
7.2 反思与后续规划
推荐延伸阅读
🧩一句话读懂:AOP 让你在不修改源码的前提下,给方法批量添加「前置检查」「后置日志」「异常兜底」等能力——就像在公司流程里插入一道审批节点,而不用每个部门单独改流程。 🎯适合人群:有 Spring Boot 项目经验、了解 IOC/DI 的 Java 开发者 📊难度等级:⭐⭐⭐☆☆(中等) ⏱阅读时长:约 18 分钟 💡前置知识:Spring Boot 基础、Java 反射基础、代理模式概念
一、学习背景与目标
1.1 学习背景
想象一个真实的项目,你需要在每个接口方法里加上日志记录:
// 改造前——干干净净 public Order getOrder(Long id) { return orderMapper.selectById(id); } // 改造后——每个方法都要加这段 public Order getOrder(Long id) { log.info("调用 getOrder, 参数: id={}", id); // ← 新增 long start = System.currentTimeMillis(); try { Order result = orderMapper.selectById(id); log.info("getOrder 返回: {}, 耗时: {}ms", result, System.currentTimeMillis() - start); return result; } catch (Exception e) { log.error("getOrder 异常: ", e); // ← 新增 throw e; } }一个接口改完还行。十个接口呢?五十个呢?每一处都是复制粘贴,改个日志格式要跑遍全项目。
不止日志,还有:
权限校验:每个接口方法前查用户角色
限流:每个接口前查调用次数
缓存:每个查询前查缓存,查询后写缓存
事务:每个 Service 方法前开启事务,结束后提交/回滚
性能监控:每个方法记录耗时上报
这些需求有一个共同点:和核心业务无关,但散落在各处,重复实现。
AOP 就是来解决这个问题的。它把这类「横切关注点」抽取成独立模块,然后声明式地插入到业务方法的「前置」「后置」「异常」等位置。
1.2 学习目标
读完本篇后,你能做到:
✅ 理解 AOP 的核心概念:切面、连接点、切入点、通知
✅ 掌握注解驱动的 AOP 写法(最主流、最实用)
✅ 掌握声明式 AOP的 XML 配置写法
✅ 用 AOP 实现三个实战场景:日志切面 + 性能监控 + 权限校验
✅ 知道JoinPoint / ProceedingJoinPoint 的用法区别
✅ 知道执行顺序的坑和怎么排
二、核心概念与原理
2.1 AOP 是什么
定义:AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它把多个类中重复的「横切逻辑」抽取成独立的模块(切面),然后通过动态代理技术在运行时/编译期织入到目标方法执行链中。
和 OOP 的区别:
OOP(纵向):UserService → OrderService → ProductService ↓ ↓ ↓ 日志代码 日志代码 日志代码 ← 重复! AOP(横向): 日志切面 ───→ 织入每一个 Service 方法 权限切面 ───→ 织入特定 Controller 方法 事务切面 ───→ 织入所有 Service 方法
一句话区分:OOP 是竖着切,AOP 是横着切。OOP 解决代码复用,AOP 解决横切关注点的复用。
2.2 五大核心概念
| 概念 | 英文 | 类比 | 一句话理解 |
|---|---|---|---|
| 切面 | Aspect | 一个"插件模块" | 把"我要做的事"和"我要往哪插"打包在一起 |
| 连接点 | Join Point | 所有可能下刀的位置 | 方法调用、异常抛出等都是连接点 |
| 切入点 | Pointcut | 具体下刀的位置 | "哪些方法需要增强"的条件表达式 |
| 通知 | Advice | 下刀后做什么 | 前置/后置/环绕/异常/最终五种 |
| 织入 | Weaving | 把刀装上去的过程 | 代理对象生成的过程 |
一张图理清关系:
项目中的所有方法 → 这些都是 JoinPoint │ │ Pointcut 表达式筛选 ▼ 只匹配到的 TargetMethod │ │ Advice 定义"怎么增强" ▼ Aspect = Pointcut + Advice → 织入生成代理对象
2.3 AOP 的底层原理
AOP 的底层实现分两种:
1. JDK 动态代理(默认)
条件:目标类实现了至少一个接口 原理:运行时生成一个实现了相同接口的代理对象
2. CGLIB 代理(当目标类没有实现接口时)
条件:目标类没有实现接口(或者配置 proxyTargetClass=true) 原理:运行时生成目标类的子类作为代理对象
Spring Boot 2.x+ 默认行为:优先 CGLIB(spring.aop.proxy-target-class=true),因为绝大多数业务类都没有实现接口,CGLIB 适配更广。
// JDK 代理 public class JdkProxy implements InvocationHandler { private Object target; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置增强 Object result = method.invoke(target, args); // 后置增强 return result; } } // CGLIB 代理 public class CglibProxy implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 前置增强 Object result = proxy.invokeSuper(obj, args); // 后置增强 return result; } }面试考点:为什么 Spring AOP 默认用 JDK 动态代理(老版本)?因为 JDK 代理是基于接口的,和 Spring 的「面向接口编程」理念一致。后来为了适配更多场景,Spring Boot 2.x 默认改成了 CGLIB。
2.4 五种通知类型
| 通知类型 | 注解 | 执行时机 | 典型场景 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行前 | 权限校验、参数校验 |
| 后置通知 | @After | 目标方法执行后(无论是否异常) | 释放资源、记录结果 |
| 返回通知 | @AfterReturning | 目标方法正常返回后 | 记录返回值、缓存写入 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 | 异常告警、回滚操作 |
| 环绕通知 | @Around | 包围目标方法,可控制执行全过程 | 性能监控、事务、重试 |
执行顺序示意图:
@Around 开始(proceed 前) ↓ @Before ↓ 目标方法执行 ↓ @AfterReturning(正常返回) / @AfterThrowing(异常) ↓ @After(无论如何都执行) ↓ @Around 结束(proceed 后)
注意:多个切面可以用
@Order控制优先级。数值越小,前置通知越先执行,后置通知越后执行。
三、逻辑图谱与对比
3.1 AOP 实现技术图谱
AOP 的两种主要实现方式 │ ├─ Spring AOP(本篇重点) │ ├─ 运行时动态代理(JDK / CGLIB) │ ├─ 只支持方法级别的连接点 │ └─ 两种配置方式 │ ├─ 注解驱动(@AspectJ 风格)👍 推荐 │ └─ XML 配置(传统 Spring 风格) │ └─ AspectJ(纯正的 AOP 框架) ├─ 编译期织入(需要特殊编译器) ├─ 支持字段、构造器、方法、属性等所有连接点 └─ 功能更强大,但配置更复杂
3.2 注解 AOP vs XML AOP
| 对比维度 | 注解驱动(@AspectJ) | XML 配置 |
|---|---|---|
| 声明方式 | @Aspect+@Component | <aop:config>标签 |
| 切入点声明 | @Pointcut注解 | <aop:pointcut>标签 |
| 通知声明 | @Before/@After/@Around | <aop:before/after/around>标签 |
| 可读性 | ⭐⭐⭐⭐⭐(代码即文档) | ⭐⭐⭐(切面和业务分离) |
| 灵活性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 推荐程度 | ✅生产首选 | ❌ 仅适配老项目 |
除非你在维护 Spring XML 配置的老项目,否则直接用注解方式。
3.3 决策流程图
遇到需要"批量增强方法"的需求 │ ├─ 需求:批量给特定的方法加日志/权限/监控 │ └─ → Spring AOP + @AspectJ 注解(本篇方法) │ ├─ 需求:要在构造器调用、字段赋值时做拦截 │ └─ → AspectJ 编译期织入(如 Spring Configurable) │ ├─ 需求:对某个类的所有方法做拦截(通用的) │ └─ → Spring AOP + execution 表达式 │ ├─ 需求:对加了某个注解的方法做拦截 │ └─ → Spring AOP + @annotation 表达式 + 自定义注解 │ └─ 需求:只想对特定包的特定方法名做拦截 └─ → Spring AOP + within + execution 组合
四、核心详解
4.1 基础环境搭建
<!-- Spring Boot 项目,AOP 已内置,只需引入 starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> @SpringBootApplication @EnableAspectJAutoProxy // Spring Boot 自动配置,一般不需要手动加 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }Spring Boot 2.x+ 默认开启了 AOP 自动配置,
@EnableAspectJAutoProxy不需要手动加。只有在老版本 Spring 项目中才需要。
4.2 最标准注解 AOP 模板
@Aspect // 告诉 Spring 这是一个切面类 @Component // 注册到 IOC 容器 @Slf4j public class LogAspect { /** * 定义切入点 —— 匹配哪些方法 */ @Pointcut("execution(* com.example.service.*.*(..))") public void servicePointcut() {} // ============ 五种通知 ============ @Before("servicePointcut()") public void beforeAdvice(JoinPoint joinPoint) { log.info("前置通知: 方法={}, 参数={}", joinPoint.getSignature().getName(), joinPoint.getArgs()); } @After("servicePointcut()") public void afterAdvice(JoinPoint joinPoint) { log.info("后置通知: 方法={}, 执行完毕", joinPoint.getSignature().getName()); } @AfterReturning(value = "servicePointcut()", returning = "result") public void afterReturningAdvice(JoinPoint joinPoint, Object result) { log.info("返回通知: 方法={}, 返回值={}", joinPoint.getSignature().getName(), result); } @AfterThrowing(value = "servicePointcut()", throwing = "ex") public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) { log.error("异常通知: 方法={}, 异常={}", joinPoint.getSignature().getName(), ex.getMessage()); } @Around("servicePointcut()") public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); String methodName = pjp.getSignature().getName(); log.info("环绕前置: {}", methodName); Object result; try { result = pjp.proceed(); // 执行目标方法(关键!) log.info("环绕后置: {}, 耗时={}ms", methodName, System.currentTimeMillis() - start); } catch (Throwable t) { log.error("环绕异常: {}, 耗时={}ms", methodName, System.currentTimeMillis() - start); throw t; } return result; } }4.3 JoinPoint vs ProceedingJoinPoint
| 对比 | JoinPoint | ProceedingJoinPoint |
|---|---|---|
| 适用通知 | @Before,@After,@AfterReturning,@AfterThrowing | 仅@Around |
| 能否控制方法执行 | ❌ 不能,已固定 | ✅ 能,通过proceed() |
| 获取方法签名 | getSignature() | 同(继承自 JoinPoint) |
| 获取参数 | getArgs() | 同 |
| 获取目标对象 | getTarget() | 同 |
| 修改参数 | ❌ | ✅proceed(Object[] args) |
一句话记住:不控制方法执行就用 JoinPoint,要控制执行(如修改参数、加 try-catch、重试)就用 ProceedingJoinPoint。
4.4 Pointcut 表达式详解
切入点是 AOP 的灵魂。Spring AOP 支持以下几种表达式:
execution —— 最常用,按方法签名匹配
// 语法:execution(修饰符 返回值 包.类.方法(参数) throws 异常) // 匹配 Service 包下所有类的所有方法 @Pointcut("execution(* com.example.service.*.*(..))") // 匹配 Service 包及子包下所有类的所有方法 @Pointcut("execution(* com.example.service..*.*(..))") // 匹配 OrderService 的所有 public 方法 @Pointcut("execution(public * com.example.service.OrderService.*(..))") // 匹配 findById 方法,不管参数 @Pointcut("execution(* com.example.service.*.findById(..))") // 匹配无参数的方法 @Pointcut("execution(* com.example.service.*.*())")within —— 按类匹配(更快)
// 匹配 Service 包下的所有 Bean @Pointcut("within(com.example.service.*)") // 匹配 OrderService 类 @Pointcut("within(com.example.service.OrderService)")@annotation —— 按注解匹配(最灵活,推荐用于自定义注解场景)
// 匹配加了 @LogAnnotation 注解的方法 @Pointcut("@annotation(com.example.annotation.LogAnnotation)")this / target —— 按代理对象类型匹配
// target:目标对象的类型 @Pointcut("target(com.example.service.OrderService)") // this:代理对象的类型(通常和 target 一样,除非多层代理) @Pointcut("this(com.example.service.OrderService)")bean —— 按 Spring Bean 名称匹配
// 匹配名称以 Service 结尾的 Bean @Pointcut("bean(*Service)") // 匹配 OrderServiceImpl @Pointcut("bean(orderServiceImpl)")组合表达式
// &&(且)、||(或)、!(非) @Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.set*(..))")4.5 多切面排序
@Aspect @Component @Order(1) // 数值越小优先级越高 public class LogAspect { ... } @Aspect @Component @Order(2) public class PermissionAspect { ... }执行顺序:
PermissionAspect@Around 前(@Order=1,先执行) LogAspect@Around 前(@Order=2) PermissionAspect@Before LogAspect@Before 目标方法 LogAspect@AfterReturning PermissionAspect@AfterReturning LogAspect@Around 后 PermissionAspect@Around 后
直观理解:@Order 值小的切面「包在外面」,前置先执行,后置后执行。
4.6 XML 配置 AOP(传统方式,了解即可)
<!-- 切面类(纯 Java 类,不需要 @Aspect) --> public class XmlLogAspect { public void beforeLog(JoinPoint joinPoint) { log.info("前置日志: {}", joinPoint.getSignature().getName()); } public void afterLog(Object result) { log.info("后置日志: {}", result); } } <!-- applicationContext.xml --> <aop:config> <aop:aspect ref="xmlLogAspect"> <!-- 定义切入点 --> <aop:pointcut id="servicePointcut" expression="execution(* com.example.service.*.*(..))"/> <!-- 定义通知 --> <aop:before method="beforeLog" pointcut-ref="servicePointcut"/> <aop:after-returning method="afterLog" pointcut-ref="servicePointcut" returning="result"/> </aop:aspect> </aop:config>五、四大实战案例
5.1 案例:自定义注解实现接口日志切面(最实战)
📋 需求描述:用一个@Loggable注解标注在需要记录完整入参+出参+耗时的方法上,自动输出日志。
💻 实现代码:
// 1. 自定义注解 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Loggable { String value() default ""; // 业务描述 boolean logParams() default true; // 是否打印参数 boolean logResult() default true; // 是否打印返回值 } // 2. 切面实现 @Aspect @Component @Slf4j public class LoggableAspect { @Pointcut("@annotation(com.example.annotation.Loggable)") public void loggablePointcut() {} @Around("loggablePointcut()") public Object handle(ProceedingJoinPoint pjp) throws Throwable { // 获取注解信息 MethodSignature signature = (MethodSignature) pjp.getSignature(); Loggable loggable = signature.getMethod().getAnnotation(Loggable.class); String methodName = signature.getDeclaringTypeName() + "." + signature.getName(); String businessDesc = loggable.value(); // 构建日志 StringBuilder sb = new StringBuilder(); sb.append("【Loggable】").append(businessDesc) .append(" | 方法: ").append(methodName); if (loggable.logParams()) { sb.append(" | 参数: ").append(Arrays.toString(pjp.getArgs())); } long start = System.currentTimeMillis(); try { Object result = pjp.proceed(); sb.append(" | 耗时: ").append(System.currentTimeMillis() - start).append("ms"); if (loggable.logResult()) { sb.append(" | 返回: ").append(result); } log.info(sb.toString()); return result; } catch (Throwable t) { sb.append(" | 异常: ").append(t.getMessage()); log.error(sb.toString(), t); throw t; } } } // 3. 在业务方法上使用 @Service public class OrderService { @Loggable(value = "查询订单", logResult = false) // 不打印返回值(可能包含敏感信息) public Order getOrder(Long id) { return orderMapper.selectById(id); } @Loggable(value = "创建订单") public Order createOrder(CreateOrderRequest request) { // 业务逻辑 return order; } }📤 输出日志:
【Loggable】查询订单 | 方法: com.example.service.OrderService.getOrder | 参数: [1001] | 耗时: 23ms
✅ 一句话总结:自定义注解 +@annotation切入点 = AOP 最灵活、最面试友好的写法。
5.2 案例:接口性能监控 + 慢请求告警
📋 需求描述:监控所有 Controller 方法的执行时间,超过 500ms 的 Method 输出告警日志。
💻 实现代码:
@Aspect @Component @Slf4j public class PerformanceMonitorAspect { private static final long SLOW_THRESHOLD_MS = 500; @Pointcut("execution(* com.example.controller.*.*(..))") public void controllerPointcut() {} @Around("controllerPointcut()") public Object monitor(ProceedingJoinPoint pjp) throws Throwable { long start = System.nanoTime(); try { return pjp.proceed(); } finally { long durationMs = (System.nanoTime() - start) / 1_000_000; String methodName = pjp.getSignature().toShortString(); if (durationMs > SLOW_THRESHOLD_MS) { // 慢请求告警级别 log.warn("⚠️ 慢请求告警: {} | 耗时={}ms > 阈值={}ms", methodName, durationMs, SLOW_THRESHOLD_MS); } else { log.debug("性能监控: {} | 耗时={}ms", methodName, durationMs); } } } }✅ 一句话总结:@Around配合finally块 + 纳秒计时,低成本实现全接口性能监控。
5.3 案例:注解方式实现权限校验
📋 需求描述:用@RequireRole("ADMIN")注解来标记需要特定角色的接口,无权直接返回 403。
💻 实现代码:
// 1. 注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String[] value(); // 允许的角色列表 } // 2. 切面 @Aspect @Component public class RoleCheckAspect { @Pointcut("@annotation(requireRole)") public void rolePointcut(RequireRole requireRole) {} @Before(value = "rolePointcut(requireRole)", argNames = "joinPoint,requireRole") public void checkRole(JoinPoint joinPoint, RequireRole requireRole) { // 从当前请求上下文中获取用户角色(实际项目中从 JWT/Session 取) String currentUserRole = SecurityContextHolder.getContext() .getAuthentication().getAuthorities() .stream().findFirst().orElse("").getAuthority(); String[] allowedRoles = requireRole.value(); boolean hasPermission = Arrays.asList(allowedRoles).contains(currentUserRole); if (!hasPermission) { throw new AccessDeniedException("权限不足,需要角色: " + Arrays.toString(allowedRoles)); } } } // 3. 使用 @RestController @RequestMapping("/admin") public class AdminController { @GetMapping("/deleteUser") @RequireRole("ADMIN") public Result deleteUser(Long userId) { userService.delete(userId); return Result.success(); } }🧠 要点解析:
@annotation(requireRole)可以同时绑定注解实例到通知参数中,优雅地拿到注解上配置的角色数组异常统一由 Spring 的
@ControllerAdvice处理,全局返回 403
5.4 案例:方法重试切面
📋 需求描述:某些外部接口调用偶尔网络超时失败,想自动重试 3 次。
💻 实现代码:
// 1. 注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Retryable { int maxRetries() default 3; long delayMs() default 100; // 重试间隔 Class<? extends Throwable>[] retryFor() default {Exception.class}; // 哪些异常需重试 } // 2. 切面 @Aspect @Component public class RetryAspect { @Pointcut("@annotation(retryable)") public void retryPointcut(Retryable retryable) {} @Around(value = "retryPointcut(retryable)", argNames = "pjp,retryable") public Object handleRetry(ProceedingJoinPoint pjp, Retryable retryable) throws Throwable { int maxRetries = retryable.maxRetries(); long delayMs = retryable.delayMs(); Class<? extends Throwable>[] retryFor = retryable.retryFor(); Throwable lastException = null; for (int i = 0; i <= maxRetries; i++) { try { return pjp.proceed(); } catch (Throwable t) { lastException = t; // 判断是否需要重试 boolean shouldRetry = Arrays.stream(retryFor) .anyMatch(clazz -> clazz.isAssignableFrom(t.getClass())); if (!shouldRetry || i == maxRetries) { throw t; // 不重试的异常 或 重试已达上限,直接抛出 } log.warn("方法执行失败,第{}次重试, 异常={}", i + 1, t.getMessage()); if (delayMs > 0) { Thread.sleep(delayMs); } } } throw lastException; // 不会走到这里,但满足编译器 } } // 3. 使用 @Service public class PaymentClient { @Retryable(maxRetries = 3, delayMs = 200, retryFor = {SocketTimeoutException.class, ConnectException.class}) public PaymentResponse callExternalApi(PaymentRequest request) { // 调用外部支付网关 return restTemplate.postForObject(url, request, PaymentResponse.class); } }六、常见坑与最佳实践
6.1 易错点清单
| # | 坑点 | 现象 | 原因 | ✅ 避坑方案 |
|---|---|---|---|---|
| 1 | 内部方法调用不生效 | AOP 失效,日志没打出来 | 类内部this.method()调用不走代理 | 用AopContext.currentProxy()或注入自身 |
| 2 | 同一切面内 @Before 引用 @Around 不存在的 Pointcut | 启动报错或顺序混乱 | Pointcut 定义理解不一致 | 每个切面单独定义 Pointcut |
| 3 | @Around 忘记调用 proceed() | 目标方法不执行 | 环绕通知必须手动 proceed | 模板写在 IDE 里,永远不在 @Around 里漏proceed() |
| 4 | 多切面 @Order 未指定 | 执行顺序不可控 | 默认按切面类名排序 | 显式加@Order(n) |
| 5 | @annotation 表达式里方法参数名不匹配 | 启动失败,提示无法解析 | 注解名写法错误 | 确保@annotation(变量名)和切面方法参数名一致 |
| 6 | Spring Boot 2.x 与 3.x 的 AOP 自动配置差异 | 切面不生效 | 3.x 默认不开启 AOP | @EnableAspectJAutoProxy显式开启 |
| 7 | 调用 private 方法 | 切面不生效 | Spring AOP 只能拦截 public 方法 | 改为 public 方法 |
| 8 | 切面类没有加 @Component | 切面无效 | 不在 IOC 容器中 | 检查 @Aspect + @Component 组合 |
| 9 | 静态方法被切面拦截 | 不生效 | Spring AOP 不支持静态方法 | 改为实例方法,或用 AspectJ |
| 10 | 代理对象强转失败 | ClassCastException | JDK 代理不能强转为目标类本身 | 面向接口编程,或用 CGLIB 代理 |
6.2 内部调用自注入解决方案
@Service public class OrderService { @Autowired private OrderService self; // 注入自身代理 public void outerMethod() { // 错误:this.innerMethod() 不触发 AOP // this.innerMethod(); // 正确:通过代理对象调用 self.innerMethod(); } @Loggable("内部方法") public void innerMethod() { // 业务逻辑 } } 或者使用 @EnableAspectJAutoProxy(exposeProxy = true): @Service public class OrderService { public void outerMethod() { // 暴露当前代理对象 ((OrderService) AopContext.currentProxy()).innerMethod(); } }6.3 最佳实践清单
✅优先用自定义注解 +
@annotation切入点——灵活、可配置、面试加分✅所有切面类显式加
@Order——可读性强,顺序明确✅@Around 通知里永远不要忘记
proceed()——IDE 里建模板自动生成✅@Before 中抛出的异常会被 @Around 捕获——时序问题时先确认通知执行链
✅切入点表达式尽量精确——
execution(* com.example.service.*.*(..))比execution(* *..*.*(..))好✅日志切面放在 @Order 最后(值最大)——确保其他切面(权限、事务)先运行
❌不要在 @Around 里吞异常——吞了异常上游感知不到
❌不要在 AOP 切面里写复杂业务逻辑——切面是横切关注点,不是业务层
❌不要用 AOP 替代事务注解——
@Transactional本身就是 AOP,不要重复造轮子
七、总结与学习路线图
7.1 知识点提炼
核心口诀
切面编程三要素,切点通知和切面 execution 最常用,annotation 最灵活 Around 要手动 proceed,Before 只能看不能碰 内部调用不生效,注入自己才管用
核心要点速览
| 维度 | 要点 | 一句话记住 |
|---|---|---|
| 概念 | 切面 = 切入点 + 通知 | 哪个方法 + 做什么事 |
| 切入点 | execution按签名,@annotation按注解 | 精确用 execution,灵活用 @annotation |
| 通知 | 5 种,记住@Around是万能之王 | 能控制执行的全过程 |
| 代理 | JDK 基于接口,CGLIB 基于继承 | 没有接口就 CGLIB |
| 坑 | 内部方法调用不走 AOP | 注入自己,或者 exposeProxy |
自检清单
- 我能说清楚 AOP 的五大核心概念
- 我能写出一个自定义注解 + 切面的完整代码
- 我知道 @Before、@After、@Around 的执行顺序和区别
- 我知道 JoinPoint 和 ProceedingJoinPoint 的区别
- 我会用 execution 和 @annotation 两种切入点表达式
- 我知道为什么「同一类内部方法调用」AOP 不生效
- 我能用 AOP 实现日志、权限校验、重试三个场景
7.2 反思与后续规划
本次总结:
从手写重复日志的痛点引出 AOP,理解了它解决的是横切关注点复用问题
掌握了注解式 AOP 的标准模板,这是最主流的写法
通过 4 个实战案例覆盖了日志、监控、权限、重试四大典型场景
避坑清单覆盖了 AOP 最常见的 10 个坑,尤其是「内部调用不生效」
下一步学习路线图:
阶段一(基础 — 本篇已覆盖):注解 AOP + 5 种通知 + execution/@annotation ↓ 阶段二(深入原理):JDK 动态代理 vs CGLIB 源码分析 + 代理对象创建过程 ↓ 阶段三(源码级):@Transactional 的 AOP 实现 + AbstractAutoProxyCreator 解析 ↓ 阶段四(架构):AOP + 策略模式实现业务扩展点 + 多级缓存 AOP 实现
推荐延伸阅读
Spring AOP 官方文档 — 最权威的参考资料
@EnableAspectJAutoProxy 源码解析 — 理解 AOP 自动配置原理
Spring AOP 代理对象创建过程 — 源码级解读 JDK 和 CGLIB 的创建差异
