Spring AOP面向切面编程-从注解到实战
目录一、学习背景与目标1.1 学习背景1.2 学习目标二、核心概念与原理2.1 AOP 是什么2.2 五大核心概念2.3 AOP 的底层原理2.4 五种通知类型三、逻辑图谱与对比3.1 AOP 实现技术图谱3.2 注解 AOP vs XML AOP3.3 决策流程图四、核心详解4.1 基础环境搭建4.2 最标准注解 AOP 模板4.3 JoinPoint vs ProceedingJoinPoint4.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 是什么定义AOPAspect-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 代理当目标类没有实现接口时条件目标类没有实现接口或者配置 proxyTargetClasstrue 原理运行时生成目标类的子类作为代理对象Spring Boot 2.x 默认行为优先 CGLIBspring.aop.proxy-target-classtrue因为绝大多数业务类都没有实现接口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对比维度注解驱动AspectJXML 配置声明方式AspectComponentaop:config标签切入点声明Pointcut注解aop:pointcut标签通知声明Before/After/Aroundaop: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 groupIdorg.springframework.boot/groupId artifactIdspring-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对比JoinPointProceedingJoinPoint适用通知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 { ... }执行顺序PermissionAspectAround 前Order1先执行 LogAspectAround 前Order2 PermissionAspectBefore LogAspectBefore 目标方法 LogAspectAfterReturning PermissionAspectAfterReturning LogAspectAround 后 PermissionAspectAround 后直观理解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 refxmlLogAspect !-- 定义切入点 -- aop:pointcut idservicePointcut expressionexecution(* com.example.service.*.*(..))/ !-- 定义通知 -- aop:before methodbeforeLog pointcut-refservicePointcut/ aop:after-returning methodafterLog pointcut-refservicePointcut returningresult/ /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处理全局返回 4035.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 定义理解不一致每个切面单独定义 Pointcut3Around 忘记调用 proceed()目标方法不执行环绕通知必须手动 proceed模板写在 IDE 里永远不在 Around 里漏proceed()4多切面 Order 未指定执行顺序不可控默认按切面类名排序显式加Order(n)5annotation 表达式里方法参数名不匹配启动失败提示无法解析注解名写法错误确保annotation(变量名)和切面方法参数名一致6Spring Boot 2.x 与 3.x 的 AOP 自动配置差异切面不生效3.x 默认不开启 AOPEnableAspectJAutoProxy显式开启7调用 private 方法切面不生效Spring AOP 只能拦截 public 方法改为 public 方法8切面类没有加 Component切面无效不在 IOC 容器中检查 Aspect Component 组合9静态方法被切面拦截不生效Spring AOP 不支持静态方法改为实例方法或用 AspectJ10代理对象强转失败ClassCastExceptionJDK 代理不能强转为目标类本身面向接口编程或用 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 要手动 proceedBefore 只能看不能碰 内部调用不生效注入自己才管用核心要点速览维度要点一句话记住概念切面 切入点 通知哪个方法 做什么事切入点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 的创建差异