1. 为什么你写的业务代码总在重复同一件事
如果你打开一个真实的后端项目,随便翻几个Service方法,大概率会看到这样的画面:每个方法开头先判断权限,然后打一行日志"开始执行",中间写真正的业务逻辑,最后在结束位置再打一行"执行完毕",顺手算个耗时。要是方法里抛了异常,还得有个try-catch把异常记录下来再往外抛。
一周下来,你发现自己写了四十遍"记录日志",三十遍"校验权限",二十遍"统计耗时"。这些逻辑和"用户下单"“查询订单详情”这些核心业务没有任何关系,但它们就是赖在每个方法里不走。更麻烦的是,今天产品经理说所有写操作都要加审计记录,你新建一个方法时还记得要加,老方法就漏了。
我们管这类逻辑叫横切关注点——它们像一把刀横着切过所有业务方法,与业务本身垂直。AOP(Aspect Oriented Programming,面向切面编程)就是专门解决这个问题的:把那些横切逻辑从业务代码里抽出来,统一写在一个地方,再通过某种机制“织入”到目标方法的前后左右。
这篇内容是写给刚接触AOP的小白的,我会把AOP原理里最绕的JDK动态代理和CGLIB动态代理讲明白,也会聊透Spring AOP到底怎么落地、适合用在哪些场景。我默认你写过几行Java,看过SpringBoot的基本用法,但不要求你懂代理模式,跟着代码一步一步来就行。
先给个直观结论:AOP不是什么黑魔法,它的本质就是"我帮你包了一层方法"。你看别人的博客天天讲"面向切面",其实拆开看就三部分——切在哪里、做什么、什么时候做。后面全部内容都在展开这三件事。
2. AOP的三板斧:切点、通知、切面是怎么配合的
2.1 从外卖订单理解AOP的核心概念
我第一次学AOP,被一堆术语砸晕了:Aspect、JoinPoint、Pointcut、Advice、Weaving、Target。这些词翻译过来分别是切面、连接点、切点、通知、织入、目标对象。看着高大上,换成外卖场景就好懂了。
假设你是一家餐厅,核心工作是做菜(业务逻辑)。但你发现每个订单都得分三步:接单时记录顾客要求,出餐时检查有没有漏放餐具,送走时记录出餐时间。后厨里每个做菜师傅(目标对象)都得干这些活儿,师傅们烦得要死。
AOP的做法是:你在后厨门口装一个"出餐流水线处理器",只认订单(连接点,即程序执行过程中的某个位置,比如方法调用)。然后你规定:"所有做菜方法的调用点"就是切点(Pointcut),也就是流水线的感应范围。处理器里配置的那些动作——记录要求、检查餐具、记录时间——就是通知(Advice)。处理器本体就是切面(Aspect)。把处理器装到后厨门口让每个订单自动过一遍,这个安装动作叫织入(Weaving)。
术语表给小白放这里:
| 术语 | 一句人话解释 |
|---|---|
| 目标对象 Target | 被AOP改造的那个Bean,你的业务Service |
| 连接点 JoinPoint | 程序执行中能插入逻辑的位置,在Spring里通常指方法调用 |
| 切点 Pointcut | 通过表达式描述"哪些连接点要管",比如"所有以Service结尾的类里的所有方法" |
| 通知 Advice | 干活逻辑本身,比如打日志、算耗时 |
| 切面 Aspect | 切点+通知的组合体,通常是一个带@Aspect注解的类 |
| 织入 Weaving | 把通知应用到切点所匹配的方法上的过程 |
2.2 五种通知:你打算在什么时候插手
搞清楚了"切哪里",下一件事就是"什么时候做"。AOP通知一共五种:
- @Before:方法执行前跑。适合做权限校验、初始化参数、记录开始时间。
- @AfterReturning:方法正常返回后跑。适合记录结果、生成审计日志。
- @AfterThrowing:方法抛异常后跑。适合统一异常记录、发送告警。
- @After:不管正常还是异常,方法结束后都要跑。类似finally块,适合释放资源。
- @Around:最强的一种,把方法的整个调用包在中间,你可以决定"要不要执行原来方法"“执行前干什么”"执行后干什么"。适合统计耗时、加缓存、做重试。
这五种通知在Spring里都是注解形式,写在切面类的方法上。
我用得最多的就是@Around。因为它能完全控制目标方法的生命周期,像这样:
@Around("execution(* com.example.service.*.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); // 调用真正的方法 return result; } finally { System.out.println("耗时: " + (System.currentTimeMillis() - start)); } }这里joinPoint.proceed()是灵魂——它就是"继续执行原方法"的意思。你在它前后写什么,就相当于在原方法前后插入了什么逻辑。
2.3 切点表达式到底怎么写
很多小白卡在切点表达式上。Spring AOP最常用的切点表达式是execution,格式是:
execution(修饰符 返回类型 类路径.方法名(参数) 异常)实际操作时修饰符和异常经常省略,常见三种写法:
// 匹配com.example.service包下所有类的所有方法 execution(* com.example.service.*.*(..)) // 匹配某个具体方法,callOrder是方法名,第一个参数是Long execution(* com.example.service.OrderService.callOrder(Long)) // 匹配所有public方法 execution(public * *(..))..代表任意数量参数,*代表任意返回类型或任意类名。初次记不住没关系,IDE里有AspectJ插件能帮你校验表达式。
另一个常用切点类型是@annotation,匹配"方法上带了某个注解"的方法。比如你自定义一个@AuditLog注解,然后在需要审计的接口方法上打这个注解,切点表达式就写@annotation(com.example.annotation.AuditLog)。这种方式比execution精确得多,也是我在真实项目里推荐的方式——切点语义直接跟着注解走,方法加不加审计一目了然。
3. 织入的秘密:JDK动态代理和CGLIB动态代理到底在干嘛
3.1 为什么Spring非要用"代理"
搞懂前面概念,你可能会问:Spring是怎么把通知逻辑塞进目标方法里的?它不可能真的改掉你的class文件,也不想去重写你的Service类。
答案就四个字:动态代理。
Spring会为你的目标对象生成一个代理对象(Proxy),然后让你注入的其实是这个代理。当你调用orderService.callOrder()时,外界的引用指向的是代理对象,代理对象负责先执行你的切面逻辑,再通过反射或继承机制调用真实目标方法。原代码一行没改,行为却变了。
动态代理有两种实现路线:JDK动态代理和CGLIB动态代理。这两个词在AOP面试里几乎必考,理解它们就是理解AOP原理的钥匙。
3.2 JDK动态代理:只能代理有接口的类
JDK动态代理从Java 1.3就有,核心就两个东西:java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。
它要求目标类必须实现接口,代理对象也只能以接口形式暴露给外部。原理是:运行时用Proxy.newProxyInstance()生成一个实现该接口的类实例,这个实例内部持有InvocationHandler,每次调用接口方法时都会走到invoke()方法里。
我写一个最小示例。先定义接口和实现类:
public interface UserService { String getUserName(Long userId); } public class UserServiceImpl implements UserService { @Override public String getUserName(Long userId) { System.out.println("真实方法执行"); return "用户" + userId; } }然后建一个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 { System.out.println("调用前: " + method.getName()); Object result = method.invoke(target, args); System.out.println("调用后: " + method.getName()); return result; } }最后生成代理对象并调用:
UserServiceImpl impl = new UserServiceImpl(); UserService proxyInstance = (UserService) Proxy.newProxyInstance( impl.getClass().getClassLoader(), impl.getClass().getInterfaces(), new LogHandler(impl) ); proxyInstance.getUserName(1001);控制台会依次输出"调用前: getUserName"、"真实方法执行"、"调用后: getUserName"。请注意,我们从头到尾没改过UserServiceImpl的代码,只是给它套了一层处理器。
这里有个关键点:代理对象只能强制转换成UserService接口,不能直接转成UserServiceImpl。因为JDK代理生成的类只"实现接口",并不"继承实现类"。你在Spring里注入时要是按实现类类型注入,就会拿到一个ClassCastException。
3.3 CGLIB动态代理:不需要接口,靠生成子类
CGLIB(Code Generation Library)走的是另一条路——生成目标类的子类,通过方法重写来织入逻辑。它代理的核心类是Enhancer,配合MethodInterceptor接口使用。
同样是上面的场景,不定义接口,直接一个普通类:
public class OrderService { public String createOrder(String productId) { System.out.println("创建订单业务逻辑"); return "订单" + productId; } }CGLIB代理写法:
public class CglibProxyFactory { public static Object createProxy(Object target) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> { System.out.println("CGLIB调用前: " + method.getName()); Object result = methodProxy.invokeSuper(obj, args); System.out.println("CGLIB调用后: " + method.getName()); return result; }); return enhancer.create(); } }调用:
OrderService proxy = (OrderService) CglibProxyFactory.createProxy(new OrderService()); proxy.createOrder("A100");注意这里methodProxy.invokeSuper(obj, args),这句话的意思是调用父类也就是目标类的原方法。CGLIB生成的代理是OrderService的子类,所以可以向上转型成OrderService,使用方式上比JDK代理更自由。
但"继承"这条路是有代价的:final类无法被代理,final方法也无法被重写。你一旦在Service方法上加了final关键字,CGLIB就绕不过去,只能失效。这个坑我后面还会提到。
3.4 一张表说清两者怎么选
网上对比JDK动态代理和CGLIB动态代理的文章很多,我结合实测和经验给你一张实用表格:
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 必要条件 | 目标类必须实现至少一个接口 | 无需接口,但类/方法不能是final |
| 代理生成方式 | 实现接口,运行时动态生成代理类 | 继承目标类,生成子类 |
| 生成代理对象速度 | 相对快 | 相对慢(要生成子类字节码) |
| 方法调用速度 | 通过反射调用,相对慢 | 通过FastClass直接调用,相对快 |
| SpringBoot 2.x+默认 | 不再优先使用 | 默认使用 |
| 典型局限 | 只能用接口接收代理对象 | final类/方法失效 |
关于速度对比,过去很多文章说CGLIB调用速度优于JDK,这是基于老版本JDK的结论。JDK版本升级后,反射调用也在持续优化,实际差距在小方法热调用场景并不明显。真正影响选择的是"有没有接口"和"是否兼容final",而不是性能数字。
Spring的旧版本策略是"有接口就JDK,没接口就CGLIB"。SpringBoot 2.x之后,默认强制使用CGLIB,就算你的类实现了接口,也会优先用CGLIB。想切回JDK代理,可以在配置里设置spring.aop.proxy-target-class=false。我在生产项目里基本开默认值,只有在和某些字节码增强框架冲突时才会改动。
4. Spring AOP/SpringBoot里的真实配置:从注解到落地
4.1 最小依赖和切面类的完整骨架
原理讲完,必须落到SpringBoot工程里跑通一个真实切面。先把依赖加上,SpringBoot的web和aop两个starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>注意spring-boot-starter-web本身会带AOP相关的传递依赖,但为了保险我还是建议显式加spring-boot-starter-aop,确保切面能力完整可用。
然后写一个切面类,我拿"统计每个Service方法耗时+记录参数"做例子:
@Aspect @Component public class PerformanceLogAspect { private static final Logger log = LoggerFactory.getLogger(PerformanceLogAspect.class); @Around("execution(* com.example.demo.service.*.*(..))") public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); long start = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); log.info("方法[{}] 参数[{}] 返回[{}] 耗时[{}ms]", methodName, Arrays.toString(args), result, System.currentTimeMillis() - start); return result; } catch (Throwable throwable) { log.error("方法[{}] 参数[{}] 异常[{}] 耗时[{}ms]", methodName, Arrays.toString(args), throwable.getMessage(), System.currentTimeMillis() - start); throw throwable; } } }@Aspect表示Spring要把它识别成切面,@Component保证它能被扫描注册成Bean。@Around里用joinPoint.getSignature()拿到方法名,用getArgs()拿到入参,proceed()执行真实方法。异常时记录了错误信息后必须throw throwable重新抛出去,否则业务方拿不到异常,事务回滚也会失灵。
4.2 切点复用:用@Pointcut把表达式抽出来
如果同一个切点表达式在多处出现,比如前置通知、返回通知、异常通知都要匹配同一个范围,每个注解里写一遍表达式又长又容易抄错。可以用@Pointcut定义一个空方法,其他通知引用它:
@Aspect @Component public class AuditAspect { @Pointcut("execution(* com.example.demo.service.*.*(..))") public void servicePointcut() {} @Before("servicePointcut()") public void beforeCheck() { System.out.println("前置校验"); } @AfterReturning("servicePointcut()") public void afterReturn() { System.out.println("正常返回"); } }定义@Pointcut的方法本身是个空方法,它只是切点表达式的载体,不会被业务调用。这种方式在切面逻辑复杂时能让代码整洁不少。
4.3 通知方法的参数怎么拿
你可能注意到,不同通知拿"方法信息、参数、返回值、异常"的方式不一样。我整理了一组对应关系:
| 通知 | 想要的信息 | 方法参数写法 |
|---|---|---|
| @Before | 方法签名、参数 | JoinPoint joinPoint |
| @AfterReturning | 返回值 | @AfterReturning(pointcut = "servicePointcut()", returning = "result")+ 形参Object result |
| @AfterThrowing | 异常 | @AfterThrowing(pointcut = "servicePointcut()", throwing = "ex")+ 形参Exception ex |
| @Around | 全部信息+继续执行 | ProceedingJoinPoint joinPoint |
@Before拿参数很简单:joinPoint.getArgs()拿参数数组,joinPoint.getSignature()拿方法签名。@AfterReturning里要拿返回值,必须在注解里用returning属性指定一个名字,然后让方法形参也叫这个名字,Spring才会通过反射把返回值塞进来。名字对不上是拿不到的,这是个非常容易踩的细节。
@Around因为自己控制整个执行流程,所以能同时拿到参数、返回值和异常,不用分别写三个通知,这也是我偏爱它的原因。
4.4 Spring AOP切点表达式中容易踩的细节
切点表达式写在注解里,看起来是字符串,实际上它对"包名""类名""参数匹配"有自己一套严格约定。我总结三个常见坑:
第一个:*匹配的是单个包,不是多级包。
execution(* com.example.service.*.*(..))这个表达式只匹配com.example.service包下的类,不匹配com.example.service.sub包下的类。想匹配子包,得写com.example.service..*,中间两个点代表匹配当前包及任意子包。
第二个:参数匹配写..会匹配任意参数,写(String)会精确匹配只有一个String参数的签名。
execution(* com.example.service.OrderService.createOrder(Long))这是精确匹配"一个Long参数"。如果你写createOrder(String, ..),意思是第一个参数是String,后续参数任意。弄混了会导致切面不生效,排查时又看不出来,因为日志和异常都没有。
第三个:切点表达式写错了不一定启动报错。
有些写错的表达式在应用启动时没有任何异常,只有调用不匹配的方法时切面静默不执行。排查这类问题最有效的办法是开AOP的调试日志,在application.yml里加:
logging: level: org.springframework.aop: DEBUG这样启动时Spring会把匹配到的切点和方法关系打出来,一眼就能看到你的切点有没有被正确解析。
5. AOP适合干什么、不适合干什么:使用场景与边界
5.1 从我实际项目看,AOP用得最爽的几个地方
统一日志与审计。财务系统的所有订单金额变化都要留下"谁在什么时间改了什么"的记录。用@annotation切点自定义一个@AuditLog注解,在有敏感操作的方法上打注解,切面统一记录操作人、操作时间、修改前后值。这个场景AOP是绝对王者,既不污染业务代码,又能保证不会漏记。
接口耗时与监控。我上家公司做过一个"慢接口大屏",所有Controller方法被AOP拦截后,如果执行时间超过500ms,就异步上报给监控系统。实现起来就是一个@Around切面加一条ThreadPoolExecutor派发,几十行代码搞定全站性能数据采集。
权限校验。有些老项目没用Spring Security,只要求"管理员才能调某些接口"。在切面里解析请求头里的token,校验角色,不通过就直接抛异常返回403。相比在每个Controller里写if判断,切面统一管理明显更优雅。
缓存与重试。如果某个方法的计算结果可以缓存,可以写一个"查缓存->没有就执行方法->存缓存"的@Around切面。RPC调用失败时的重试逻辑也可以放切面里,代码一挂,所有方法自动获得重试能力。
事务管理。严格来说Spring事务就是AOP的经典应用,@Transactional背后就是通过代理对象提交/回滚连接。理解AOP之后回看@Transactional莫名其妙的失效问题,会豁然开朗。
5.2 AOP不是万能的,这几种情况千万别硬上
AOP的横切能力很漂亮,但滥用起来同样让人头疼。我在代码评审里见过不少"用切面把业务逻辑包装成玄学"的案例,这部分是小白最容易跑偏的方向。
不适合做复杂的跨模块业务编排。比如"下单后要扣库存、发短信、推送消息、更新报表",这些步骤有明确依赖关系和先后顺序,业务语义非常强。你强行把它们拆成多个切面各种切入点,代码看起来是"优雅了",但执行顺序、异常补偿、幂等控制全都变得难排查。这种场景应该老老实实写Service方法调用,让逻辑可见。
不适合在切面里做重活。切面里同步调用外部HTTP接口、跑复杂批量SQL、打印超大对象的toString,都会拖慢目标方法,而且因为代理关系,调用链上多了一层隐形的慢操作。如果必须在切面里做"非必须但耗时"的事,请异步化——丢线程池,或者配合消息队列。
切面执行有顺序风险。多个切面作用于同一个方法时,它们之间的执行顺序由@Order决定。默认情况下SpringBoot并没有保证切面执行的绝对顺序,一旦两个切面相互依赖顺序,结果就会飘忽不定。所以多切面场景一定要手动指定@Order(1)、@Order(2),数字小的先执行,让顺序从"随缘"变成"可控"。
异常处理的边界要格外小心。切面里catch Throwable后如果不重新抛出,目标方法的异常就被"吞掉"了,上游拿不到任何失败信号,事务也可能不会回滚。很多人调完AOP发现"方法没报错,但数据不对",多半就是切面里吞了异常。
5.3 和拦截器(Interceptor)、过滤器(Filter)的对比
这三者都具备"在方法运行前后插入逻辑"的能力,很容易混。我给小白一个粗糙但有效的区分方式:
| 组件 | 作用范围 | 典型用途 |
|---|---|---|
| Filter | Servlet容器层面,每个请求进出都会经过 | 字符编码、跨域处理、接口签名验签 |
| Interceptor | Spring MVC层面,只拦截Controller请求 | 登录态校验、请求头补充 |
| AOP | Spring Bean方法层面,可以拦截任意Service/Component方法 | 日志、耗时、权限、缓存、事务 |
选型建议:凡是能通过Filter/Interceptor解决的,别先想着上AOP。AOP虽然灵活,但它介入的是Bean的方法调用,比前面两者更底层,排查问题也更难。比如跨域和编码问题用Filter天经地义,事件日志记录用Interceptor或AOP都可以——如果面向的是纯后端Service层调用,那AOP才是唯一选择。
6. 小白最容易踩的坑:代理失效、通知顺序、无限递归
6.1 代理失效:我切面明明写了,方法却像没被拦
这是AOP新手问得最多的问题,症状通常是"日志完全没打,切面静悄悄"。排除切点表达式写错之外,最高频的三大元凶是:
元凶一:同类内部调用。假设OrderService里有一个pay()方法,内部调用了this.sendNotify()。你对sendNotify()配置了日志切面,但调用是通过this直接发起的,根本没有经过代理对象,切面自然不触发。Spring的代理只能拦截"从外部进入代理对象"的调用,代理内部的方法互调属于"自调用",代理对它爱莫能助。
解决办法三个方向:把内部调用改成通过注入自身的代理来调用(不推荐,容易循环依赖);把需要被切的方法挪到另一个Service里;或者像我一样干脆调整设计,把sendNotify()做成切面自己调用的逻辑,而不是让它作为被切目标。
元凶二:private方法。JDK动态代理基于接口,CGLIB基于继承,两者都操作不了private方法。private方法是类私有实现细节,外界根本无法通过代理发起调用,切点怎么可能拦到?把切点表达式瞄准private方法是个典型误区。
元凶三:final类或final方法。CGLIB要生成子类才能代理,final类不能继承,final方法不能重写,所以一旦碰到final,代理直接失效。我在一个老项目里见过有人给Service类加final"防继承保护",结果所有AOP静默失效,排查了一整个下午。
想快速确认代理是否生效,有个笨办法非常管用:在目标方法里打印this.getClass()。如果输出的是class xxx.$$EnhancerBySpringCGLIB$$或者$Proxy之类的类名,说明当前类已经被代理了;如果输出的是你自己写的类名,说明代理没缠上它,问题就在代理链路的某个环节。
6.2 自调用还有救吗?用AopContext拿到原生代理
如果你实在不想把内部调用拆出去,Spring提供一个AopContext工具,可以在目标方法内部获取当前代理对象,然后通过代理调用另一个方法:
public void pay(Long orderId) { // 业务逻辑 OrderService proxy = (OrderService) AopContext.currentProxy(); proxy.sendNotify(orderId); }但用它之前必须把spring.aop.expose-proxy=true打开,否则会抛IllegalStateException: Cannot find current proxy。这个方案可行,但说实话可读性比较差,我在生产里只用来应急,不推荐当常规写法。相比AopContext.currentProxy(),更干净的解法是让两个方法分别处于不同Bean里,天然形成代理调用。
6.3 多个切面的顺序:@Order到底怎么用
假设一个Service方法上同时挂了"权限校验"切面和"耗时统计"切面,你希望先校验权限再统计耗时,还是先统计耗时再校验权限?语义差别很大。前者记录的是"被拒绝的请求不需要统计耗时",后者则是"连被拒绝的请求都该有耗时数据可查"。
默认多个切面执行顺序不确定,所以必须用@Order控制。规则是:
@Order(1)的执行在@Order(2)之前- 如果不在不同切面上标注,执行顺序依赖切面初始化顺序,基本不受控
@Aspect @Component @Order(1) public class AuthAspect { ... } @Aspect @Component @Order(2) public class PerformanceAspect { ... }注意一个细节:@Order控制的是切面之间嵌套的执行顺序。对@Around通知来说,编号小的切面的proceed()调用会"包"住编号大的切面。也就是说AuthAspect先执行,它内部调用proceed时才轮到PerformanceAspect。如果你把顺序写反了,效果完全相反。
6.4 无限递归:切面又拦截了切面自己
给切面类的方法本身配上切点,就会形成"切面方法被代理拦截,拦截后又记录日志,日志方法又被代理拦截……"的死循环。最典型的翻车是:把切点表达式写成execution(* com.example.demo..*.*(..)),范围太大,把切面类自己所在的com.example.demo.aspect包也圈进去了。
排查方法很简单:切点范围不要贪大,只锁定业务Service或Controller包;另外把切面类单独放一个包,用表达式明确排除它:
execution(* com.example.demo.service.*.*(..)) && !within(com.example.demo.aspect..*)&&和!在AspectJ表达式里都是合法操作符,within(com.example.demo.aspect..*)表示切面类所在范围内,排除掉即可。
6.5 事务注解突然失效,先想想代理
@Transactional是AOP落地最典型的功能,它的失效原因几乎都能用代理机制解释。比如同类中methodA()调用methodB(),methodB上标了@Transactional,实际执行时因为走的是this直接调用,事务切面根本没机会介入,于是事务不生效。这个和自调用失效是同一个道理。
所以排查@Transactional失效问题时,别急着调事务参数,先打印一下类名,确认当前对象是不是代理对象。如果是代理,再看是不是自调用;如果不是代理,再看@EnableTransactionManagement是否开启、事务管理器Bean是否配置正确。按这个顺序排查,能省掉大量无用功。
7. 从入门到能用,我给你的三步建议
看完这篇内容,你已经把AOP原理、JDK动态代理、CGLIB动态代理、Spring AOP的配置和使用场景都走了一遍。最后我想结合自己实战带团队的经验,给刚入门的朋友一段非常务实的建议。
第一步,先别追求一步到位理解所有内置原理,而是把代码跑起来。建一个SpringBoot工程,写一个最普通的Service,加一个@Around切面打日志,故意制造一个"自调用失效"的现场,用debug模式观察代理对象长什么样。自己亲眼看到代理对象,比读十篇原理文章都管用。
第二步,把最常见的四个切点表达式背下来:execution精确到类、execution精确到包、@annotation注解方式、配合@Pointcut复用。日常开发90%的场景就靠这四个组合。表达式不用全背,IDE和文档都能查,真正值钱的是你知道自己能控制到哪一层。
第三步,在你手头的存量项目里选一个真正重复的场景,比如接口耗时统计,用AOP重写一遍,然后观察代码删减了多少。等你感受到"几十个方法自动获得同一个能力"的瞬间,AOP算是真正入门了。
最后留个实际体会:AOP最考验人的地方不是技术本身,而是"边界感"。它适合做横切逻辑,不适合篡改业务语义。把切面当成一个能力补全层,而不是业务放逐地,你的代码会越写越干净。过阵子我再聊聊Spring AOP在这些代理机制之外的坑,比如被final挡住的事务、异步切面线程上下文传递等等,都是实战里能救命的细节。