在项目里跟日志、事务、权限这些东西打交道打得多了,你会发现一个特别扎心的现实:你真正想写的业务逻辑可能就十行,但为了凑齐“记录操作人、打印入参出参、开启事务、校验权限”这些横切逻辑,硬生生能写出五十行重复代码。我最早接触 AOP 切面编程就是被这种重复逼的——每个接口方法里粘一段日志打印、每个 Service 方法上标一个@Transactional,改动一个日志格式要全局搜索替换,漏改一个地方线上排查半天。后来把 AOP 这套机制吃透,才明白这东西解决的不是“某个功能怎么写”,而是“一堆功能怎么从业务代码里抽离出去”。
这篇文章我不会跟你掰扯教科书定义,而是基于实际项目里的使用经验,把 AOP 切面编程从原理到实战、从使用场景到常见坑位完整捋一遍。无论你是刚接触 Spring 没多久的初学者,还是写了两三年业务代码但一直没用明白 AOP 的老手,看完都能知道这东西到底能干什么、不能干什么、以及面试里被问到 IOC 和 AOP 原理时怎么答才显得有深度。
1. 为什么要用 AOP:项目里的痛点与 AOP 的登场
1.1 我实际遇到的代码灾难现场
先还原一个我 2019 年在一个电商后台系统里遇到的真实场景。当时系统的订单模块有十几个方法,每个方法都要做这几件事:打印入参日志、开启事务、操作数据库、打印返回值、记录异常堆栈。最初写代码的人图省事,直接在方法里手工写日志:
public Order createOrder(OrderDTO dto) { log.info("createOrder 入参: {}", JSON.toJSONString(dto)); try { // 核心业务逻辑... Order order = new Order(); order.setUserId(dto.getUserId()); order.setAmount(dto.getAmount()); // 保存数据库... log.info("createOrder 出参: {}", JSON.toJSONString(order)); return order; } catch (Exception e) { log.error("createOrder 异常", e); throw new BizException("创建订单失败"); } }看起来没什么问题对吧?可当项目里这样的方法从 10 个增长到 200 个,问题就来了:产品经理改了一个需求,要求所有操作日志必须记录操作者的 IP——你需要打开 200 个文件,在每个log.info里追加一个参数;新来的同事漏了try-catch,线上报错堆栈半个字没留下;测试环境想临时排查某几个接口的性能,你没法只改部分方法,只能一个文件一个文件地动。这些不属于核心业务的功能,像口香糖一样粘在了业务方法上,而且遍布系统各个角落,这就是典型的“横切关注点”问题——日志、事务、权限校验这些逻辑,在对象模型里跟业务逻辑垂直交叉,用传统的面向对象编程(OOP)很难优雅地统一处理。
1.2 AOP 能解决的问题边界
AOP 切面编程的登场,就是为了收拾“横切关注点”这个烂摊子。我们回过头看 OOP 的思路,它擅长把“纵向”的代码组织成一个个对象:用户对象、订单对象、商品对象,各有各的状态和行为。但跨越所有业务对象的那一层——比如“所有方法执行前打印日志”“所有方法执行后提交事务”——OOP 就无能为力了,它没有一个现成的机制说“给所有订单方法都安一个日志监听器”。
AOP 的思路恰恰相反,它把系统拆成两半:
- 核心关注点:也就是你的业务逻辑,比如创建订单、计算价格、扣减库存。
- 横切关注点:不关心业务是什么,但每个业务执行时都绕不开的那些事,比如日志记录、事务控制、权限校验、性能统计。
AOP 做的事情就是把横切关注点提炼成独立的“切面”,然后在系统运行或编译的关键节点上,把这些切面和业务方法自动“编织”在一起。业务代码不必感知这些逻辑的存在,插件化地达到了“想加功能就加,想卸掉就卸掉”的效果。
我举个生活化的类比帮助理解:医院的每个科室都有医生在给病人看病,这是业务逻辑。但每个医生都需要一台血压计来测生命体征,叫“横切逻辑”。传统做法是每个科室自己采购血压计、自己维护记录,成本高且标准不一。AOP 做的事就是医院统一采购一批血压计,由护士按标准化流程在问诊前统一测量,医生只需要专心看病就行。业务的归业务,横切的归横切,各自专注,靠制度连接——在代码里,这个“制度”就是切点表达式和通知类型。
1.3 五个核心概念一次性串明白
真要理解 AOP,绕不开那五个词:切面、连接点、切点、通知、织入。我第一次看这些术语也头大,后来发现用一条时间线就能想清楚。
- 连接点(Join Point):程序执行过程中的一个点,比如方法调用、方法执行、异常抛出。在 Spring AOP 里,连接点通常就是某个具体的方法执行。它描述的是“哪里可以切”。
- 切点(Pointcut):表达式,用它筛选连接点,告诉切面“哪些连接点才算是我的目标”。比如
execution(* com.example.service.*.*(..))就是匹配 service 包下所有类的所有方法。 - 通知(Advice):切面在特定连接点上执行的逻辑,相当于“切什么呢”。有 @Before、@After、@Around、@AfterReturning、@AfterThrowing 五种。
- 切面(Aspect):通知 + 切点的组合,决定“什么时候、在哪里、做什么”。
- 织入(Weaving):把切面代码应用(或者说编织)到目标对象的过程,最终让目标方法执行前后/异常时自动触发通知。
初次接触的人最容易把“切点”和“连接点”搞混:连接点是个客观存在,比如OrderServiceImpl.createOrder()这个方法调用一次就是一个连接点实例;切点是主观筛选,决定“我要盯哪些连接点”。没有切点,切面会在所有方法上执行,那系统早就炸了。
2. AOP 的核心原理:动态代理,绕不开的两个“替身”
2.1 JDK 动态代理与 CGLIB 的区别与选择
AOP 原理听上去玄乎,落地到代码层面无非一句话:Spring 会为你的目标对象生成一个代理对象,由代理对象包一层逻辑,在合适的时机调用真实对象的方法。你调用orderService.createOrder()时,实际拿到的是 Spring 容器里那个代理对象,不是原始 bean。
Spring AOP 默认的代理方式有两种,这个几乎所有面试都会问。
JDK 动态代理:基于接口做代理。代理类实现了目标对象的接口,方法调用会被InvocationHandler.invoke()拦截。它要求目标类必须实现至少一个接口。Spring 默认你的 bean 实现了接口时,用的就是这种方式。这个代理在 Java 1.3 时代就有了,属于标准 JDK 能力,无需额外依赖。
CGLIB 代理:基于继承做代理。CGLIB 直接生成目标类的一个子类,并覆写那些非 final、非 private 的方法,在覆写逻辑里插入切面代码。它不需要目标类实现任何接口,所以更灵活。但副作用是:final 类没法被代理,final 方法也不会生效。
Spring 的选择逻辑很简单:如果目标 bean 实现了接口,优先用 JDK 动态代理;如果没实现接口,就用 CGLIB。从 Spring Boot 2.x 开始,官方把spring.aop.proxy-target-class=true设为默认值,意思是哪怕实现了接口也优先用 CGLIB。原因很现实:JDK 代理生成的类型是com.sun.proxy.$Proxy123这种,类型强转成具体实现类会强转失败;CGLIB 代理是目标类的子类,强转成目标类不会有问题。日常开发里如果用@Autowired注入的是接口类型,两种方式都没毛病,但有些老项目喜欢注入实现类,遇到 JDK 代理就报类型转换异常,这个坑我踩过,后面细说。
2.2 织入的四种时机,以及 Spring 选了哪种
织入是“把切面逻辑放进业务代码流程”这个动作。按时机分,一共有四种:
- 编译期织入:Java 编译器编译阶段就把切面代码拼进目标类字节码,需要特殊编译器支持。AspectJ 支持这种。
- 类加载期织入:类加载器加载 .class 文件时,动态修改字节码再加载,也属于 AspectJ 的范畴。
- 运行期织入:运行过程中生成代理对象并替换原始对象,Spring AOP 就是这种。
- 手动织入:代码里手工调用 AspectJ API 完成织入,一般不讨论。
Spring AOP 之所以选择运行期织入,核心原因是简单、无侵入、易集成。不像编译期和类加载期需要额外的预处理器、修改构建流程,Spring 只要在容器里把 bean 替换成代理就能搞定。代价是性能上比字节码修改方式稍逊色,但对绝大多数业务系统来说完全够用。
2.3 Spring AOP 与 AspectJ:不是二选一的关系
这里要澄清一个广泛存在的误解:Spring AOP 和 AspectJ 是两回事,不是竞争关系。Spring AOP 是 Spring 框架自己实现的轻量级 AOP 框架,它借鉴了 AspectJ 的注解风格(比如 @Aspect、@Around),但没有依赖 AspectJ 编译器;AspectJ 是完整的、独立的 AOP 框架,支持编译期和类加载期织入,能力更强但学习和构建成本更高。
在实际项目中,95% 的场景用 Spring AOP 就够了。我自己的经验是,只有需要极致的性能、或者要切到字段赋值、构造函数、静态方法调用这种 Spring AOP 覆盖不到的场景,才考虑使用 AspectJ 的 load-time weaving(LTW)。这两种能力边界就像定制服装和成衣的区别,AspectJ 是裁缝量体定制,怎么织都由你定;Spring AOP 是工厂成衣,固定几个档位,但拿来就能穿。先用好 Spring AOP,项目里 80% 的切面需求都解决了。
3. 实战:一个注解搞定全链路日志埋点(完整实操)
3.1 需求背景与设计思路
理论说多了容易飘,直接上一个我最近在“订单网关项目”里做的实操案例。需求是这样的:所有对外提供 HTTP 接口的 Controller 层方法,要打印入参、出参、耗时,还要在异常时输出完整堆栈。如果手工写,又是几百行重复代码。我决定用 AOP 做一个自定义注解解决,让代码能变成这样:
@OperationLog @PostMapping("/create") public ApiResult<OrderVO> create(@RequestBody OrderDTO dto) { // 直接写业务,不用管日志 return orderService.create(dto); }实现思路分三层:
- 定义注解
@OperationLog,用来标记需要埋点的方法。 - 定义切面类,用
@Around环绕目标方法,在目标方法执行前记录开始时间、打印入参,执行后打印出参和耗时,异常时打印堆栈。 - 通过切点表达式选中“所有标注了 @OperationLog 的方法”。
这样做的优势很明显:将来想去掉日志,删掉注解或改动切面即可,Controller 本身零感知;想统一调整日志格式,只改切面一个文件。
3.2 代码实现:注解、切面、切点表达式
先定义注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OperationLog { String bizType() default ""; }注解本身简单,注意两个元注解别写错:@Target(ElementType.METHOD)限定它只能打在方法上;@Retention(RetentionPolicy.RUNTIME)保证运行时可以通过反射拿到,这是 AOP 切面通过注解筛选方法的前提。
然后定义切面类:
@Aspect @Component public class OperationLogAspect { private static final Logger log = LoggerFactory.getLogger(OperationLogAspect.class); // 切点:凡是标注 @OperationLog 的方法 @Pointcut("@annotation(com.example.aop.annotation.OperationLog)") public void operationLogPointcut() { } @Around("operationLogPointcut()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); String methodName = joinPoint.getSignature().toShortString(); // 1. 入参打印:方法参数 Object[] args = joinPoint.getArgs(); log.info("[OperationLog] 方法[{}] 入参: {}", methodName, JSON.toJSONString(args)); try { // 2. 执行目标方法 Object result = joinPoint.proceed(); long costTime = System.currentTimeMillis() - startTime; // 3. 出参打印:返回值 log.info("[OperationLog] 方法[{}] 出参: {}, 耗时: {}ms", methodName, JSON.toJSONString(result), costTime); return result; } catch (Throwable throwable) { long costTime = System.currentTimeMillis() - startTime; log.error("[OperationLog] 方法[{}] 异常: {}, 耗时: {}ms", methodName, throwable.getMessage(), costTime, throwable); throw throwable; } } }这段代码最核心的是joinPoint.proceed(),这一行是放行目标方法的关键。在@Around通知中,你必须在逻辑里调用它,否则目标方法根本不会执行;并且try-finally或throw throwable中要把异常重新抛出去,否则异常被吞掉、业务方拿不到错误信息。
提示:
@Around里joinPoint.proceed()抛出的异常,必须继续向上抛,不能只在切面里打印。手动 try-catch 住了异常又没抛,接口会返回成功但实际上业务失败了,这种 bug 很难排查。
3.3 切点表达式细节:execution、@annotation、within 怎么选
上面示例用了@annotation(com.example.aop.annotation.OperationLog)这种切点表达式,它的含义是“匹配所有标注了指定注解的方法”。日常开发里常见的切点表达式还有几种,选型逻辑我总结在下表:
| 切点表达式 | 含义 | 典型场景 | 注意点 |
|---|---|---|---|
execution(public * com.example.service.*.*(..)) | 匹配某个包下所有类的 public 方法,返回类型任意,参数任意 | Service 层统一事务、统一日志 | 方法多时影响面大,切点范围要控制精准 |
within(com.example.service..*) | 匹配某个包及其子包下所有类的所有方法 | 按包批量处理 | 不能细粒度到某个方法 |
@annotation(com.example.xxx.OperationLog) | 匹配标注了指定注解的方法 | 自定义注解统一埋点、限流 | 注解必须配RUNTIME保留策略 |
bean(orderService) | 匹配容器中指定名称的 bean 的所有方法 | 单 bean 针对性增强 | 慎用,影响该 bean 所有方法 |
我的选型原则很简单:尽量用@annotation或execution精准定位,别用within横扫整个包。横扫包看起来很省事,但后期新增了个不需要日志的内部工具方法也会被切面击中,到时你也不知道日志里为什么多了一堆没见过的调用,排查起来很痛苦。
3.4 事务切面的正确姿势:别把事务写在 Controller 层
很多初学者容易犯的错是把@Transactional直接标在 Controller 方法上,然后跑来问我:“为什么事务不生效?”我先给结论:事务切面通常加在 Service 层或更底层,Controller 层压根不该碰数据库事务。
原因在于事务的本质是资源(数据库连接)的绑定和释放,它应该围绕业务操作单元展开,Controller 属于入口层,职责是参数接收和响应返回。万一一个接口要调两个 Service 方法,事务标在 Controller 上会导致这些方法共用一个事务,其中之一失败则全部回滚,看似很好,但有些场景你恰恰希望两个 Service 独立提交(比如写日志的表不能因为主业务失败而回滚),这时代码会非常拧巴。
Spring 对事务的处理实际上是靠TransactionInterceptor这个切面完成的,它属于 AOP 机制的一个典型应用:匹配所有加了@Transactional的 bean,在方法进入前开启事务、异常时回滚、正常返回后提交。所以你在使用 AOP 时,脑子里要时刻有根弦——这里是不是就是 Spring 内部用的同类机制?只是它封得更深。
4. AOP 使用场景大盘点:该用的地方和不该用的
4.1 高性价比场景:日志、性能监控、异常通知
尽量挑投入产出比最高的场景,我个人最推荐这三个方向。
统一日志。和 3.2 节示例一样,Controller 或 Service 方法挂注解自动打印入参出参耗时。尤其是在多人维护的旧项目里,日志风格混乱是常态,接一个切面能瞬间把几百个接口的日志风格统一,排查线上问题时效率提升非常明显。
性能监控。切面里统计每个方法的耗时,超过阈值就以 warn 级别打出来,或者上报到监控系统。我见过的一个真实例子:某系统上线后莫名偶发慢请求,业务内排查毫无头绪,后来通过 AOP 对 Service 层所有方法做了耗时埋点,发现是某个第三方 SDK 的一次握手调用偶尔飙到 3 秒。如果不用 AOP,你只能在每个可疑方法里手动打时间戳,工程量大且容易漏。
异常通知。方法抛异常时,通过切面统一发送告警到钉钉或邮件。相比在 catch 块里手工写告警逻辑,AOP 能做到异常的拦截不侵入业务代码,而且能拿到完整的异常堆栈和当前请求参数,定位更方便。
4.2 经典场景:事务、权限、缓存
这几个几乎是教科书写给 AOP 的“本命场景”。
- 声明式事务:
@Transactional本身就是 AOP 的封装,我们不需要自己写,但要理解它底层是 TransactionInterceptor 切面在工作。 - 权限校验:比如基于自定义注解
@RequirePermission("order:create"),切面在方法执行前校验当前用户是否有权限,没有就直接抛异常。权限校验放进切面比在每个方法入口手写判断干净得多。 - 缓存:自定义
@CacheResult注解,切面检查方法入参对应的缓存是否存在,存在则直接返回缓存值,否则proceed()执行方法并写入缓存。这个我在读 Redis 缓存重构旧项目时手动实现过,比硬编码缓存逻辑优雅很多。
4.3 不该用 AOP 的地方:别为了“高级”而“高级”
AOP 不是万金油,有几类场景我明确不建议上切面。
一是核心业务逻辑的流转控制。如果某个业务规则本身就是“A 方法一定要在 B 方法之前执行,失败则中断”,这是业务流程图的一部分,写在切面里会让人摸不着头脑。业务逻辑讲求从代码里直接读懂,切面的“透明增强”在此时反而是障碍。
二是高频且超短方法的过度埋点。比如某个方法每秒被调用上万次,只是做一次字段赋值,你给它加切面做日志、耗时统计。虽然 AOP 本身开销不大,但日志序列化和反射调用叠加起来,对这个热点路径可能是无法忽视的损耗。
三是跨模块的强制依赖。如果切面里要调用另一个微服务的接口来做校验,而这个接口偶尔超时,那等于给目标方法引入了新的故障点。AOP 增强是隐式的,调用方不会意识到一次简单的方法调用背后可能藏着一次远程调用,这种“隐性风险”比显式代码难排查得多。
5. 常见问题与排查技巧:我踩过的坑,你尽量别踩
5.1 自调用失效:内部方法调内部方法,切面不生效
这是 AOP 新手遇到最频繁的问题。看这段代码:
@Service public class OrderServiceImpl implements OrderService { @Override public Order createOrder(OrderDTO dto) { // ... 业务代码 this.updateStock(dto.getSkuId(), dto.getCount()); return order; } @OperationLog public void updateStock(Long skuId, Integer count) { // ... 扣库存 } }createOrder()方法里调用this.updateStock(),你满心期待@OperationLog会在updateStock上打日志,结果完全没有。原因很直接:Spring AOP 代理的是容器中的 bean,而this指向的是原始对象,不是代理对象。也就是说,从容器里拿出来的orderService是代理,this是代理背后的原生实例,内部自调用绕过了代理,所以切面不生效。
解决方案有三种:
- 把被调方法拆到另一个 bean 里,比如新建
StockService,注入进来再调用。这是最清晰的做法。 - 在
createOrder里从容器获取代理对象再调用,通常用AopContext.currentProxy(),配合配置@EnableAspectJAutoProxy(exposeProxy = true)。 - 被调方法不是内部方法,而是通过接口调用另一个 bean 的同名方法。
我自己更倾向第一种,因为代理逻辑抽出来比操作AopContext直观,也避免团队里其他人不理解exposeProxy导致新的困惑。
5.2 循环依赖与 AOP 的乱局:组件 A 和 B 互相引用,切面又掺进来
这个坑在大型项目里很经典。假设A依赖B,B又依赖A,同时 A 被某个切面代理。Spring 解决循环依赖用的是三级缓存,其中一级缓存存最终成品(完整代理对象),二级缓存存早期暴露的半成品毛 bean。当 A 有切面时,早期暴露的“毛 A”其实还不是代理对象,B 拿到的是原始 A,往后的调用都绕过了切面——这是很多“循环依赖 + AOP 组合拳”下切面莫名失效的根因之一。
排查思路:先去查项目里有没有循环依赖,spring.main.allow-circular-references有没有被特殊配置。循环依赖本身能跑通不代表正确,Spring 官方不推荐业务代码里出现循环依赖,能改设计就改设计,不能改就通过@Lazy打破循环,让其中一个依赖延迟注入,这样代理行为更可控。
5.3 切面不生效的 5 步排查清单
我在团队里带新人时,把“切面不生效”这类问题总结成一张速查表,查一遍基本能定位 80% 的问题:
| 序号 | 检查项 | 具体操作 |
|---|---|---|
| 1 | 切面类有没有被 Spring 管理 | 检查是否加了@Component或@Aspect配合配置类扫描 |
| 2 | @EnableAspectJAutoProxy有没有打开 | Spring Boot 默认开启,但原生 Spring 项目需要手动加 |
| 3 | 切点表达式是否命中目标方法 | 打印joinPoint.getSignature()对比表达式匹配情况 |
| 4 | 是否被内部自调用绕过代理 | 检查目标方法是否被同类内部方法用this调用 |
| 5 | 目标方法是不是 final/static/private | CGLIB 无法代理 final 方法,static、private也不归 Spring AOP 管 |
5.4 性能开销:AOP 有没有让系统变慢
有些人担心反射、代理会拖慢性能。我用实际压测数据回答:在普通业务系统里,一个经过切面代理的方法调用,额外开销大约在微秒级到几十微秒级,对一个动辄几十毫秒的数据库事务接口来说几乎可以忽略。但如果目标方法极端高频(每秒几十万次)、切面逻辑又包含 JSON 序列化和远程调用,那就需要慎重了——JSON 序列化大对象可能消耗毫秒级时间。
我的实践建议是:切面里绝对不要做重 IO 操作,比如每次日志都同步写数据库、每次校验都调外部接口。要压的话用高性能的 Logback 异步写入,或把这种操作抽出来做成异步或采样统计。AOP 的“透明”很容易让人忽略切面里代码的成本,你为“方便”付出的隐藏代价,往往都在切面内部。
6. 面试高频考点:IOC 与 AOP 的原理,以及我对这类问题的答法
6.1 IOC 和 AOP 到底什么关系
面试官问“IOC 和 AOP 的原理”时,其实他想考的不是两个独立概念的背诵,而是你有没有真正理解 Spring 的设计哲学。我通常这样回答:
IOC(控制反转)解决的是对象的创建和装配问题,Spring 容器接管了原本由代码new出来的对象,通过依赖注入让各组件解耦。没有 IOC,你会在每个类里手动new Service、new DAO,耦合度高到你怀疑人生。AOP 解决的是横切逻辑的抽离问题,把日志、事务、安全这类横跨多个业务模块的共性逻辑提取出来,在目标方法前后动态织入。
两者在 Spring 中的关系是:AOP 的代理对象本身也是由 IOC 容器创建和管理的。容器发现某个 bean 需要切面增强时,会生成代理对象并注册进容器,之后其他组件依赖注入的就是这个代理对象。所以没有 IOC,AOP 的织入根本没有施展的舞台;没有 AOP,IOC 培养出来的对象只能带着重复的横切代码,洁净度大打折扣。二者合起来,才让 Spring 成为一个“解耦容器 + 横切工具”的双引擎框架。
6.2 面试官爱问的几个深入问题
除了定义,面试官通常还会往下追问这几个点,我给出参考答法。
“Spring AOP 的原理是什么?”——核心是基于动态代理。默认目标类实现接口就用 JDK 动态代理,否则用 CGLIB 生成子类代理。方法调用由代理对象拦截,在拦截逻辑中执行切面通知,再放行原始方法。织入发生在运行期,对象是容器中的 bean,不是自己 new 的对象。
“JDK 动态代理和 CGLIB 的区别?”——JDK 动态代理要求目标类实现接口,生成的代理类实际上是接口的实现类,调用经由InvocationHandler;CGLIB 通过生成目标类的子类来增强方法,不要求接口,但无法代理 final 类和 final 方法。Spring Boot 2.x 默认/proxy-target-class=true,优先 CGLIB。
“切面和拦截器有什么区别?”——拦截器通常针对 Handler 级别或 URL 级别,比如 Spring MVC 的HandlerInterceptor,粒度较粗;切面可以精细到方法级别,能匹配参数、注解、包名,能实现 Around 更强的包裹逻辑。
6.3 自己动手写一个“极简 AOP”:理解比背概念更重要
最后我建议每个学 AOP 的人都亲手实现一个极简版 AOP,哪怕不用 Spring 框架,也能体会那个“代理 + 拦截”的本质。比如你定义一个接口GreetingService,实现类HelloServiceImpl,然后写一个JdkProxyFactory:
public class JdkProxyFactory implements InvocationHandler { private final Object target; private JdkProxyFactory(Object target) { this.target = target; } @SuppressWarnings("unchecked") public static <T> T createProxy(Object target) { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new JdkProxyFactory(target)); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("[极简AOP] 方法执行前: " + method.getName()); Object result = method.invoke(target, args); System.out.println("[极简AOP] 方法执行后: " + method.getName()); return result; } }测试时这样用:
GreetingService service = JdkProxyFactory.createProxy(new HelloServiceImpl()); service.sayHello("张三");控制台会先输出“方法执行前”,再执行sayHello本体,最后输出“方法执行后”。整个过程没有改任何 HelloServiceImpl 的业务代码,却拿到了统一的前后增强逻辑。这一步走通了,Spring AOP 对你来说就不再是黑盒,你只是把自写的JdkProxyFactory换成了 Spring 的容器托管版本,把“打印”换成了自定义通知。
我个人在带团队时经常让新人做这个练习,效果比背十遍定义好得多。理解 AOP 最深的体会,就是记住那句:你调用的不是目标对象,而是目标对象的代理;代理知道要在合适的时机执行那些与业务无关、却又必须存在的“杂活”。把这个底层心智模型建立起来,你再看 Spring 的声明式事务、缓存、安全框架,很多设计都豁然开朗。