☰
Spring AOP核心原理:从代理模式到动态代理实战解析
2026/10/2 9:36:13 网站建设 项目流程

1. 先说说“代码冗余地狱”到底长什么样

我见过太多人第一次翻开 Spring AOP 源码时,第一反应是“这东西到底解决了什么问题”。其实不用急着看源码,先回忆一下没接触 AOP 之前,我们在真实业务代码里最常干的一件事——给每个方法加日志。

打开一个典型的 Service 类,你会发现一个非常熟悉的画面:

public class UserService { public User findUser(Long id) { log.info("开始查询用户, id=" + id); long start = System.currentTimeMillis(); try { User user = userDao.findById(id); log.info("查询用户成功, 耗时=" + (System.currentTimeMillis() - start) + "ms"); return user; } catch (Exception e) { log.error("查询用户失败", e); throw e; } } public User updateUser(User user) { log.info("开始更新用户, user=" + user); long start = System.currentTimeMillis(); try { User result = userDao.update(user); log.info("更新用户成功, 耗时=" + (System.currentTimeMillis() - start) + "ms"); return result; } catch (Exception e) { log.error("更新用户失败", e); throw e; } } // 其他若干方法... }

这段代码有什么问题?表面上看只是有点啰嗦,实际上它的危害远超想象:

其一,大量横切逻辑重复。日志、权限校验、性能统计、事务管理,这类逻辑不属于核心业务,却要在一个个业务方法里反复出现。一个项目几十个 Service,几百个方法,每一处都要贴一段几乎一模一样的“胶水代码”。

其二,业务代码被淹没。如果你接手的是别人写的类,想快速看懂 findByUser 到底做了什么,你可能要在一堆日志打印、时间统计、异常处理里翻半天才找到真正的那一行 SQL 调用。代码的“可读性”和“可维护性”就是这么被慢慢拖垮的。

其三,改动成本极其恶劣。假设今天监控系统要求把所有日志格式从“开始查询用户, id=”改成 JSON 格式,或者需要加上 traceId 关联链路。怎么办?全局替换?替换完还得出 bug。更麻烦的是如果领导要求“开发环境打 DEBUG 日志,生产环境只打 WARN”,你难道再全局改一遍?

这三个问题,在《AOP 实战》里有个统一的专业称呼——横切关注点(Cross-Cutting Concerns)。所谓横切,就是指日志、权限、事务这些逻辑像一把刀一样横着切过了所有业务模块,它们的代码天然和业务代码纠缠在一起。

当时普通人能想到的解决方案无非几种:抽公共方法、用继承、用模板方法。但稍微有点工程经验的人都会发现,这些方案治标不治本。真正让代码得到救赎的,就是 Spring AOP。

所以这篇文章的目标也很明确:用一整篇的篇幅,把 Spring AOP 的核心原理讲透,重点放在“代理模式”这条主线上。理解了代理,你就理解了 AOP 的一半;理解了 AOP 的概念体系,你就能写出不重样、不冗余、还能被同事夸“不愧是老手”的代码。

2. AOP 的“修仙世界观”:先搞懂切面、连接点、切入点、通知这四大概念

在开始写代码之前,我建议你先把 AOP 的整套概念体系在脑子里过一遍。因为所有框架层面的功能,最终都是对这些概念的具象化实现。你现在理解的越清楚,后面看 Spring 源码就越轻松。

2.1 切面(Aspect)——把横切逻辑模块化

所谓切面,就是把前面提到的日志、事务、权限这类横切逻辑,集中封装成一个模块。简单理解:以前这些逻辑是一盘散沙,东写一块西写一块;现在你把它集中收拢到一个类里,这个类就是切面。

日常生活里类似的操作是“插件化”。比如你买了一个智能插座,这个插座本身不带任何电器功能,但它可以在电流异常时切断电源、在用电高峰时统计功率。它嵌在你家电路和你家电器之间,不干扰电器本身的逻辑,却能在某些时刻介入。切面就是这样一个“智能插座”。

2.2 连接点(JoinPoint)和切入点(Pointcut)——精确到方法的“标靶”

连接点很好理解,它指程序执行过程中可以插入切面的位置。在 Spring AOP 里,连接点就是方法的执行(更严格地说只有方法级,不像 AspectJ 还能切字段、构造器)。

但问题来了:项目里有成百上千个方法,难道切面要每个都插一手?当然不是。所以就有了“切入点”这个概念。切入点更像是一个筛选条件,它告诉你:只有符合这个条件的方法才会被插入切面逻辑。

打个比方,连接点就像全校所有教室,切入点是“三年级二班这样的教室”。只有三年级二班的教室才装监控,其他教室不装。

切入点的编写在 Spring 里依赖 AspectJ 的表达式,最常见的写法是这样:

@Pointcut("execution(* com.example.demo.service.*.*(..))") public void serviceLayer() {}

这段表达式的意思是“匹配 com.example.demo.service 包下所有类的所有方法,不论返回类型和参数”。类似这种表达式还有很多变体,比如匹配注解、匹配参数、匹配返回值等,下一篇讲注解实战时再展开细聊。

2.3 通知(Advice)——五种类型的触发时机

通知就是“切面里要在目标方法前后干的事”。Spring 定义了五种类型,我把它们列成一张表,方便对照记忆:

通知类型触发时机典型场景
@Before目标方法执行前权限校验、参数校验
@AfterReturning目标方法正常返回后审计日志、数据整理
@AfterThrowing目标方法抛出异常后异常记录、告警推送
@After目标方法结束之后(含异常)资源释放、清理
@Around目标方法执行前后都能介入,最灵活日志、事务、性能统计、分布式锁、重试

@Around 为什么最特殊?因为它可以完全接管目标方法的执行。你可以决定“要不要执行”“什么时候执行”“执行前后干什么”“返回值要不要改”,甚至“吞掉异常”。其他四种通知本质上是 @Around 的某种简化形式。

2.4 织入(Weaving)和目标对象(Target)——切面如何“缝”进去

织入是 AOP 术语里最形象的一个词:把切面逻辑织入到目标对象,生成一个新的代理对象。织入的时机有三种:编译期、类加载期、运行期。Spring AOP 采用的是运行期织入,也就是在程序跑起来以后、Bean 创建过程中动态完成的——这就是它依赖代理模式的原因。

目标对象就是你写的那个业务 Bean,比如 UserService。它本身不知道自己会被切面“盯上”。切面做的事,是把 UserService 包装成一个代理对象,外部拿到的其实是这个代理对象。当调用方法时,代理对象会按照通知配置,先执行切面逻辑,再委托给真正的 UserService 方法体。

所以当你从 Spring 容器里取出 UserService 时,那个对象往往不再是原始的 UserService,而是一个“带皮肤”的代理对象。理解这一点,是后面避开无数坑的前提。

3. 代理模式的“功法演进”:从静态代理到 JDK 动态代理,再到 CGLIB

AOP 之所以选择代理模式来实现运行期织入,原因很简单:在不修改原有代码的前提下,给目标对象增加行为。这是代理模式的核心价值,也是 AOP 的核心价值。

但代理模式本身也分好几个层次,我把这条演进路线称为“功法三部曲”。只有把这三部曲看明白了,你才能理解为什么 Spring AOP 最终选了 JDK 动态代理和 CGLIB 两种实现。

3.1 静态代理:一板一眼,但毫无扩展性

静态代理就是手动写一个代理类,和被代理类实现同一个接口,然后在代理类里包一个目标对象,并在方法调用前后插入逻辑。

public interface UserService { User findUser(Long id); } public class UserServiceImpl implements UserService { public User findUser(Long id) { // 真正的业务逻辑 return userDao.findById(id); } } public class UserServiceProxy implements UserService { private final UserServiceImpl target; public UserServiceProxy(UserServiceImpl target) { this.target = target; } public User findUser(Long id) { log.info("开始查询用户, id=" + id); User user = target.findUser(id); log.info("查询用户成功"); return user; } }

静态代理确实解决了“不修改原有代码加入日志”的问题,但它极其僵硬:每个需要代理的类,都要手动写一个代理类。即便你的业务逻辑一模一样,代理类也得跟着复制一遍,项目越大维护成本越高,代码量甚至比你直接改业务类还恐怖。

所以静态代理在实际工程里很少用,它的价值更多是帮助我们理解动态代理究竟“好”在哪。

3.2 JDK 动态代理:基于接口的运行时代理

动态代理的核心变化在于:代理类是在运行时动态生成的,而不是手写死的一个类。JDK 从 1.3 开始提供了 java.lang.reflect.Proxy,配合 InvocationHandler,可以在运行时为任意接口创建代理对象。

看代码感受会更直接:

public class LogHandler implements InvocationHandler { private final Object target; public LogHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log.info("开始执行方法: " + method.getName()); Object result = method.invoke(target, args); log.info("方法执行结束: " + method.getName()); return result; } }

调用方式:

UserService userService = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), new LogHandler(userService) ); User user = proxy.findUser(1L);

看到重点没有?我们写一个通用的 LogHandler,就可以代理任何接口。无论是 UserService、OrderService 还是 ProductService,只要它们有接口,都能复用同一套代理逻辑。这就是动态代理相对静态代理的本质区别:代理逻辑被抽象成了可复用的 Handler,代理类的生成交给运行时反射机制完成。

JDK 动态代理的局限也明显:要求目标对象必须实现至少一个接口。如果你的类没有接口,它就不能被直接代理。这个限制在后面的 CGLIB 那里得到了解决。

3.3 CGLIB:基于继承的字节码增强

CGLIB(Code Generation Library)的思路完全不同:它通过生成目标类的子类来实现代理,子类重写父类的方法,在重写的逻辑里插入切面代码。因为用的是继承,所以不需要接口也能代理。

技术层面上,CGLIB 使用的是字节码操作技术,直接生成新的 Class 文件字节码,性能上也不差。从 Spring 4.0 开始,CGLIB 被重新打包放进了 spring-core 里,你不需要单独引入依赖就能使用。

但 CGLIB 有一个必须牢记的限制:final 类无法被代理,final 方法也无法被重写增强。这一点很关键,很多自认为对 AOP 很熟的人,遇到“为什么某个方法切面没生效”这种问题,最后查出来的原因就是方法不小心加了 final。

为了更直观,我把两种代理机制的差异整理成一张对比表:

对比维度JDK 动态代理CGLIB
实现方式基于接口的运行时代理基于继承的字节码增强
前提条件目标对象必须实现接口目标类不能是 final,方法不能是 final
生成方式Proxy.newProxyInstanceEnhancer 创建子类
版本支持JDK 1.3+集成在 Spring 4.0+ 核心包
性能特点创建代理时开销小,调用走反射创建代理时开销较大,调用可通过 MethodInterceptor 直接调用
适用范围有接口的 Service无接口的类、接口实现类均可

3.4 Spring 到底如何选择代理方式?

Spring AOP 在创建代理时,有一套自己的决策逻辑,核心判断依据就是“目标对象有没有接口”。

在 Spring Boot 2.x 之前,Spring 的默认行为是:如果目标对象实现了接口,就优先使用 JDK 动态代理;如果没有接口,才退而使用 CGLIB。这个策略很合理,因为 JDK 动态代理是纯 JDK 内置能力,不引入额外字节码操作,兼容性最好。

但请注意:Spring Boot 2.x 之后,官方把默认代理方式改成了 CGLIB(即 proxyTargetClass 默认为 true)。背后的原因主要是:越来越多项目里的 Spring 配置类、Bean 对象大量采用了类级别的 AOP,而不再强调必须要有接口;CGLIB 能够覆盖无接口类,行为更统一,也减少了很多“为什么这个类没被代理”的困惑。

这里有个很实际的建议:除非有特殊需求,否则不要强行把 Spring Boot 的代理方式改回 JDK 动态代理。我曾经在项目里见过一段配置文件,为了兼容某个老 SDK 强制指定 proxyTargetClass=false,结果无接口的 Bean 不能被切面拦截,排查耗费了整整半天。保持默认,基本不会错。

4. Spring AOP 的“织入时刻”:Bean 创建流程中的代理插曲

很多面试者能把代理模式的两种实现背得滚瓜烂熟,但问他“Spring 容器里到底什么时候给你生成代理对象”,他就答不上来了。这一节我们就来解开这个谜。

4.1 从 @EnableAspectJAutoProxy 说起

如果你的 Spring Boot 项目里有类似这样的配置代码,那 AOP 就已经被开启了:

@Configuration @EnableAspectJAutoProxy public class AppConfig { }

在 Spring Boot 里,如果你引入了 spring-boot-starter-aop 依赖,其实这个注解已经由自动配置帮你加好了,你都不需要手动写。但它的存在很关键:它向容器注册了一个特殊的 Bean 后置处理器,叫AnnotationAwareAspectJAutoProxyCreator。

这个类可以说是 Spring AOP 的总指挥。它本身实现了 BeanPostProcessor 接口,这意味着 Spring 在创建每一个 Bean 时都会经过它的手。

4.2 Bean 创建流程中的关键节点:postProcessAfterInitialization

Spring Bean 的创建过程大致是:实例化 -> 属性填充 -> 初始化方法 -> 注册到容器。其中,在“属性填充”和“初始化”这两个步骤之间、以及初始化之后,Spring 都会调用一群 BeanPostProcessor 的钩子方法。

AnnotationAwareAspectJAutoProxyCreator 重写的是postProcessAfterInitialization,翻译过来就是“Bean 初始化完成之后”。在这个时机,它会做三件事:

  1. 检查当前这个 Bean 是否已经是一个代理对象,如果是就跳过,避免重复代理。
  2. 拿当前 Bean 的类信息去和所有切面的 @Pointcut 匹配,看是否命中切入点。
  3. 如果命中,就调用 ProxyFactory 创建代理对象,把切面里的通知逻辑封装成 Advisor,织入代理链。

整个过程用一句话总结:切面先注册,Bean 再创建;Bean 初始化完毕后,被总指挥判定是否“切一刀”,命中了就把代理对象返回给容器。

所以外部调用方从容器里拿到的 Bean,可能是原始对象,也可能是一个代理对象。容器本身不关心你是否是代理,它只关心“Bean 的创建流程走完了没有”。

4.3 代理对象的调用链:通知到底怎么被执行的

假设你调用了代理对象的 findUser 方法,内部执行顺序是什么样的?

Spring 会把匹配到当前方法的所有通知(Advice)组装成一个拦截器链(Interceptor Chain)。这些拦截器排好顺序,依次执行。以 @Around 通知为例,它处于拦截器链的最外层,调用 moment 进入代理方法后,先执行切面前置逻辑,然后调用 joinPoint.proceed() 去触发下一个拦截器,直到最后才真正调用目标对象的原始方法。原始方法返回后,通知再执行后置逻辑。

这种设计的好处是:多个切面可以叠加,且顺序可控。比如一个方法既有日志切面,又有事务切面,还有权限切面,它们会按照 @Order 注解指定的优先级依次执行,互不干扰。

我在第一次理解这个拦截器链时,总觉得它很像洋葱:每层切面就是一层洋葱皮,你从外往内逐层调用,穿过所有皮才触达核心业务,出来时又一层一层地穿回去。这个比喻虽然简单,但对理解 @Around 的 proceed 机制非常管用。

5. 手写一个最小 AOP 框架:彻底摆脱对框架的“黑盒恐惧”

有些读者看到这里会觉得:概念都懂了,但真要让我解释动态代理怎么和 AOP 结合,我好像还是说不清楚。没关系,这一节我们动手写一个迷你版 AOP,把思路彻底跑通。

5.1 目标与设计

我们要实现的能力:标记了某个注解的方法,在执行前自动打印“开始执行”,执行后打印“执行结束”。不需要引入 Spring,只靠 JDK 动态代理完成。

先定义一个注解:

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 LogAnno { }

再定义一个业务接口和实现类:

public interface Calculator { int add(int a, int b); } public class CalculatorImpl implements Calculator { @Override @LogAnno public int add(int a, int b) { return a + b; } }

最后写核心的代理处理器:

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class AopProxy { public static <T> T createProxy(T target) { Class<?> clazz = target.getClass(); return (T) Proxy.newProxyInstance( clazz.getClassLoader(), clazz.getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 判断当前方法是否带 @LogAnno 注解 Method targetMethod = clazz.getMethod(method.getName(), method.getParameterTypes()); if (targetMethod.isAnnotationPresent(LogAnno.class)) { System.out.println("开始执行:" + method.getName()); Object result = method.invoke(target, args); System.out.println("执行结束:" + method.getName()); return result; } return method.invoke(target, args); } } ); } }

测试类:

public class Main { public static void main(String[] args) { Calculator calculator = new CalculatorImpl(); Calculator proxy = AopProxy.createProxy(calculator); System.out.println("计算结果:" + proxy.add(3, 5)); } }

运行输出:

开始执行:add 执行结束:add 计算结果:8

这个迷你框架只有二十多行代码,但它完整展现了一个 AOP 实现的核心要素:根据方法上的元信息(注解)判断是否需要拦截;拦截时通过 InvocationHandler 在目标方法调用前后插入额外逻辑;调用方拿到的是代理对象,而不是原始对象。

5.2 从手写框架回看 Spring AOP

别看这个框架简陋,它背后的套路和 Spring AOP 惊人地一致。

Spring 里的 @Pointcut 表达式,本质上就是决定“哪些方法要拦截”。我们的手写框架里用 @LogAnno 注解来标记可拦截方法,Spring 里也可以用 @annotation(com.example.LogAnno) 这样的 expression 实现同样的效果。

Spring 里的 @Before、@After、@Around 通知,本质上就是 InvocationHandler 里在不同位置插入逻辑。Spring 把这一层抽象得更优雅,提供了五种注解来声明执行时机,我们则必须在 invoke 方法里手动控制位置。

Spring 里的 Advisor、Advice、Pointcut 组装,本质上也和我们的判断逻辑一样:先做方法匹配,再执行增强逻辑。

所以当你以后再看 Spring AOP 源码,不要被一堆类名吓到。你只需要默念这句心法:它不过是一个升级版的 InvocationHandler,加上一个更强大的方法匹配器,再嵌套了一层层拦截器形成调用链。底层原理,和这段二十行代码说的是同一件事。

6. 实战中的“心魔”:为什么你的切面不生效?

原理都懂,但写出来的切面偶尔就是不生效,这是每个经历过 Spring AOP 实战的人都绕不开的坎。这里我把自己踩过的高频坑整理出来,每一个都是真实项目里出现过的。

6.1 同一个类中的方法调用,切面失效

这是最经典的问题。看这段代码:

@Service public class UserService { @LogAnno public void outerMethod() { // 调用同类内部方法 innerMethod(); } @LogAnno public void innerMethod() { // 业务逻辑 } }

你调用 outerMethod 的时候,innerMethod 上的 @LogAnno 不会生效。原因很简单:Spring 的切面是通过代理对象生效的。当你从容器里拿 UserService 时,它可能是代理对象。你在 outerMethod 内部调用 this.innerMethod(),这里的 this 指向的是目标对象本身,不是代理对象,所以内部方法直接执行了原始逻辑,切面根本没机会介入。

解决方式有三种:

  1. 注入自身代理:把 UserService 的代理对象注入到 UserService 里(Spring 支持循环依赖时可通过 ObjectProvider 或 @Lazy 处理)。
  2. 使用 AopContext.currentProxy() 获取当前代理对象,前提是开启 exposeProxy=true。
  3. 把内部方法抽到另一个 Bean 里,通过 Bean 之间的调用来触发代理。

这是第一批 AOP 新手遇到的终极问题,也是面试官最爱问的“为什么同类内调方法切面不生效”的标准答案。理解它,你就真正理解代理对象的边界在哪。

6.2 final 方法或 final 类导致代理失败

前面提过,CGLIB 基于继承实现,所以 final 类不能被代理,final 方法不能被重写增强。这在老项目里特别容易中招:某个 Service 类历史悠久,里面某个方法被你加了 final 修饰,结果切面一直不生效,查半天才发现是这个原因。

我的建议是:凡是你希望被 AOP 切面管理的类,不要加 final;方法也尽量不要用 final。虽然设计模式里常有人建议用 final 来防止继承破坏,但和切面增强一起用时,你就得先权衡。

6.3 切面依赖的 Bean 创建顺序问题

有一种诡异的现象:切面本身已经写了,但切面类里用 @Autowired 注入的 Bean 是 null,或者切面根本没被加载。这多半是配置类组织方式的问题,常见于切面类没有被 Spring 扫描到。

排查思路很简单:确认切面类在 @ComponentScan 的扫描路径内;或在 @Configuration 类里通过 @Bean 方式定义切面时,方法要正确返回切面实例。另外,如果同时存在多个切面并且有顺序要求,记得用 @Order 注解指定优先级,数字越小优先级越高。

6.4 如何判断容器里的对象到底是不是代理?

遇到问题时,先用最笨也最有效的方式验证:打印对象的 class 名称。

UserService userService = applicationContext.getBean(UserService.class); System.out.println(userService.getClass().getName());

如果输出里包含 EnhancerBySpringCGLIB 或 jdk.proxy 或 $Proxy,说明代理已生效,切面配置没问题。如果没有,说明代理没生成,再从切入点和配置上排查。

7. 面试官的“考问”:高频 Spring AOP 面试题实战拆解

作为榜单热搜词里的常客,“ioc 和 aop 的原理面试”“spring 高级面试题”“spring 底层 api 源码解析”被反复搜到。下面我从面试视角,把这些高频问题拆开揉碎,给你一套可复述的答案框架。

7.1 问:Spring AOP 的实现原理是什么?

框架答案很简单:Spring AOP 基于动态代理实现。有接口的类使用 JDK 动态代理,生成实现同样接口的代理对象;无接口的类使用 CGLIB,生成目标类的子类。通过在代理对象的方法调用链上织入切面逻辑,在不修改业务代码的前提下完成增强。

如果想让面试官眼前一亮,建议把回答补充到这个深度:Spring AOP 的切面解析、匹配、织入全部发生在 Bean 创建流程中,核心类是 AnnotationAwareAspectJAutoProxyCreator。它作为 BeanPostProcessor,在 Bean 初始化完成后检查是否命中切点,命中则创建代理对象并返回给容器。调用代理对象方法时,通过拦截器链依次执行通知,最终调用目标方法。

7.2 问:JDK 动态代理和 CGLIB 有什么区别?

按我在第 3 节的表格思路答,但记得补上两个关键点:一是 JDK 动态代理要求目标对象有接口,CGLIB 无此限制但要求类和方法不能是 final;二是 Spring Boot 2.x 后默认代理方式切换成 CGLIB,主要是为了统一无接口类的代理行为。

7.3 问:Spring 三级缓存和 AOP 有什么关系?

这个问题更刁钻,也是最近几年热搜里的“spring 三级缓存原理”被频繁关联到的原因。先简单背景一下:Spring 为了解决循环依赖引入了三级缓存:一级缓存存成品 Bean,二级缓存存早期暴露的半成品 Bean,三级缓存存 ObjectFactory(用于生成早期代理对象)。

三级缓存和 AOP 的关联在于:当 Bean 发生循环依赖时,Bean 可能在“属性填充”阶段就要被注入到另一个 Bean 中,但此时它的初始化还没完成,AOP 代理也还没有机会创建。于是 Spring 利用三级缓存的 ObjectFactory,提前获取一个“半成品”的代理对象引用,注入给依赖方。如果后续真的需要代理,这个早期引用的 Bean 也会被替换为代理对象。

这个机制保证了“提前暴露的引用”最终也能和正常创建的 Bean 一样带切面能力。没有三级缓存里的 ObjectFactory,循环依赖和 AOP 组合的场景就会出问题。

回答这个问题的诀窍是:先答缓存分三级的作用,再说 AOP 在循环依赖场景下的处理顺序。

7.4 问:AOP 的使用场景有哪些?

这是送分题,但答案不要只说“日志、事务、权限”。我建议分成两层答:

第一层是 Spring 内置场景:事务管理(@Transactional)、异步注解(@Async)、缓存(@Cacheable)、定时任务。这些都是 Spring 通过 AOP 实现的。

第二层是自定义场景:方法耗时的性能监控、审计日志、接口幂等校验、分布式锁、接口防重提交。我做过一个基于 @Around 方法实现的分布式锁,锁的获取和释放逻辑放在切面里,业务代码完全无感,效果非常好。

只要两层各举两个例子,面试官基本能看出你不只是背了概念,而是有实战经验。

写在最后的个人体会

写这篇文章时,我脑子里一直在回放自己刚接触 Spring AOP 那个阶段的困惑:概念太多,源码太重,注释太少。直到某天自己动手写了一个二十行的迷你代理框架,那种“哇,原来是这么回事”的感觉才真正出现。所以这篇文章里我特意保留了大量代码和类比,就是希望帮你跳过当年那段时间的迷茫。

这一篇作为“上篇”,重点是把“为什么 AOP 能解决代码冗余”和“底层代理机制到底怎么运转”这两件事讲透。下一篇我会重点展开 @Aspect 切面的完整写法、五种通知的实战用法、以及切入点表达式的高级匹配技巧——也就是直接可以抄进项目的具体配置。到时候你会发现,原理通了,那些注解配置根本不需要死记硬背。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询