尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AOP详解:从动态代理到Spring Boot实战,一文搞定切面编程

AOP详解:从动态代理到Spring Boot实战,一文搞定切面编程 1. AOP到底解决什么问题不知道你有没有过这种经历一个老项目里几十个Service方法暂时不说业务逻辑光是“记录日志”“校验权限”“统计耗时”这些重复代码就散落得到处都是。想给所有查询接口加个耗时统计改完一个方法发现还有三十个在等你。想给部分接口加个权限判断复制粘贴了五六遍代码还没想清楚漏了哪个方法。这就是面向对象编程OOP解决不了的核心痛点横切关注点。AOPAspect-Oriented Programming面向切面编程就是专门处理这类问题的。它的核心思想不复杂把那些横跨多个业务模块的公共逻辑从业务代码里抽出来单独做成一个“切面”然后在合适的时机“织入”到业务方法的执行链路里。业务代码保持干净公共逻辑统一维护互不污染。这就是AOP的本质——解耦、复用、无侵入。在Spring全家桶里AOP和IOC控制反转被并称为两大核心基石。IOC解决对象创建和依赖管理的问题让对象不再自己new自己AOP解决横切逻辑复用的问题让业务的每个方法都不必重复写公共代码。理解了AOP你就理解了Spring事务注解为什么能“一个注解搞定所有方法”理解了权限框架如Spring Security底层为什么能在你毫无感知的情况下拦截请求也理解了MyBatis的分页插件为什么能自动地在查询前后做手脚。这篇文章我会从零带你拆解AOP先讲清楚它是怎么想出来的、核心概念都有哪些再深入到Spring AOP的两种底层实现JDK动态代理和CGLIB动态代理的完整代码然后给出实际项目里可以立刻抄走的Spring Boot切面写法最后整理一批你面试时大概率会被问到的AOP高频题。对刚学Spring的人、写业务的老手、准备面试的同学都有参考价值。2. 核心设计思路从OOP到AOP的思维转变2.1 OOP的局限和横切关注点面向对象编程OOP把系统拆成一个个对象类每个对象承担具体的业务职责。这种做法在处理业务逻辑本身时非常顺手但有一类逻辑是“水平穿越”所有对象的比如日志记录、性能统计、事务控制、权限校验。这类逻辑有一个专门的名字横切关注点Cross-Cutting Concerns。画个图理解你有个电商系统里面十几个Service类各自处理订单、商品、用户、购物车。如果要在每个业务方法里记录操作日志代码形状是这样的——订单Service里有日志代码、商品Service里有日志代码、用户Service里也有。日志逻辑横穿了所有这些Service。用OOP的思路很难干净地解决这种问题你可以抽一个LogUtil工具类但调用还是得每个方法手动写一行你可以做一个基类但Java单继承的限制让这招并不好用而且会把业务的继承体系搞乱。AOP的思维方式是翻转的既然日志逻辑是横切的那我不在业务代码里调用日志了。我定义好规则比如“凡是包名下以find开头的所有方法执行前先记一条日志执行后再记一条执行结果”。然后把这段规则想办法“织入”到这些方法里去。业务代码保持原本的样子日志逻辑单独维护。这个思路就是AOP的雏形。它不是让业务代码“知道”自己会被附加什么逻辑而是让横切代码“决定”去哪些方法上附加自己。2.2 AOP的三大优势解耦、复用、无侵入这三个词是对AOP价值最精炼的总结我拆开说解耦业务代码和横切逻辑不再混在一起。你的订单方法里不用再写“try-finally记录耗时”那段记录耗时的代码只在切面里存在一份。业务方法的职责更纯粹了读代码的人看到的都是业务逻辑本身。复用一套权限校验逻辑不加任何业务代码的情况下可以应用到所有需要的方法上。新增一个业务方法时只要它满足切点匹配规则公共逻辑自动生效。这比“每加一个业务方法就要记得去调用一次公共逻辑”要可靠得多——要知道人总会忘的规则不会。无侵入这是最被低估的特性。在做老项目改造时无侵入意味着你不需要改动现有业务代码。比如老系统的某些接口没有做权限校验你只需要加一个AOP切面用切点表达式精准命中这些接口权限逻辑就生效了。几十个方法不用动一行代码项目风险被控制到极小。换个角度说OOP是纵向的模块化——把系统切成一个个对象AOP是横向的模块化——把跨越所有对象的共同逻辑统一管理。两者互补不是替代关系。2.3 AOP、AspectJ和Spring AOP的关系聊AOP必然会提到AspectJ。这俩容易搞混我一句话说清AspectJ是完整的AOP方案它有自己的编译器能做到编译期织入甚至修改字节码Spring AOP是Spring框架自己的一套AOP实现基于动态代理运行时织入。Spring AOP在底层大量借鉴了AspectJ的注解和概念比如Aspect、Before这些注解是AspectJ定义的但机制上是两套东西。Spring AOP的优势是够用、方便、和Spring无缝集成写个Aspect类就可以用了短板是只能拦截Spring容器管理的Bean的方法调用对静态方法、final方法、非Spring管理的对象无能为力。AspectJ能力更强、性能更好编译期直接把代码织进目标类了但配置和使用门槛要高得多。日常业务开发Spring AOP完全够用追求极致性能或需要织入很多非Spring管理类的场景才需要考虑AspectJ。我后面讲Spring AOP做实操底层原理会展开这两种动态代理方式。2.4 AOP在Spring全家桶里的生态位Spring框架里AOP不是孤零零的一个功能模块它是很多高层特性的地基。最典型的是Transactional——它之所以能让你一个注解就能管理事务底层就是AOP在拦截方法调用执行前开启事务、方法正常返回后提交事务、方法抛出异常时回滚事务。其次Spring Security的权限校验也建立在Filter和AOP机制之上方法级别的权限注解如PreAuthorize就是AOP在起作用。另外Spring Cache缓存注解、异步注解Async、定时任务相关以及各种第三方框架MyBatis分页插件、多数据源切换、接口幂等校验十有八九都用了AOP。所以弄懂AOP不光是为了懂一个概念更是为了看懂Spring整个生态的设计脉络。面试问“IOC和AOP的原理”本质上是在考察你对Spring最底层两块地基的理解程度。3. AOP核心概念逐层拆解切面、切点、通知先给个整体图AOP体系里你只需要搞清楚六个词——切面、连接点、切点、通知、目标对象、织入。每个词都有准确的技术定义但在项目中到底对应什么代码形态下面拆开讲。3.1 五个基础术语一句话理解为了方便记忆我用自己的话把这几个概念过一遍顺便建立一张和代码的对应关系连接点Join Point程序执行的某个位置。Spring AOP里连接点就是“方法调用的时机点”比如“这个方法刚被调用的时候”“这个方法的返回值已经算出来了的时候”。理论上字段赋值、构造器调用也可以作为连接点但Spring AOP支持的连接点是方法调用更准确地说是方法执行到特定阶段。简单理解候选的插入点。切点PointCut从所有候选的插入点里按表达式筛选出一批具体的方法。比如execution(* com.example.service.*.*(..))就是匹配service包下所有类的所有方法。切点本质是一个过滤规则决定了你的横切逻辑要附加到谁身上。通知Advice横切逻辑本身也就是那个要插入的具体代码块记日志、算耗时、检查权限。通知定义了“干什么”和“插在什么时机”。切面Aspect把“切点 通知”打包在一起的模块。一个切面可以包含多个通知和多个切点。目标对象Target Object被织入横切逻辑的业务对象也就是你原本写的那个Service类。AOP动态代理包裹的就是它。织入Weaving把切面代码应用到目标对象并创建出代理对象的过程。Spring AOP的织入发生在运行时别名就叫动态代理。3.2 五种通知类型详解通知类型决定了代码在什么时机执行Spring AOP一共五种。我直接用表格对比每个给一个最简单的比喻通知类型执行时机生活化类比实际典型用途Before前置通知目标方法执行前进门前先检查身份证权限校验、参数校验、操作日志记录After后置通知目标方法执行后无论是否抛异常出门后随手关门资源清理、释放锁AfterReturning返回后通知目标方法正常返回后不抛异常收到快递后确认签收记录成功日志、统计成功次数、处理返回值AfterThrowing异常通知目标方法抛出异常后出事后报警记录异常日志、统一异常包装、告警通知Around环绕通知目标方法执行前后都能干预最强大全流程陪同其前后都能操作耗时统计、分布式锁、事务控制、限流熔断用代码看最直观。环绕通知是能控制整个调用链的前四种本质上是它的语法糖变体某些简单场景用起来更简洁Around(execution(* com.example.service.OrderService.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { // 这里相当于Before long start System.currentTimeMillis(); try { // 调用目标方法拿到返回值 Object result pjp.proceed(); // 这里相当于AfterReturning return result; } catch (Exception e) { // 这里相当于AfterThrowing throw e; } finally { // 这里相当于After System.out.println(方法耗时: (System.currentTimeMillis() - start) ms); } }注意pjp.proceed()是必须调的——你不调目标方法根本不会执行。这是环绕通知和其他四种最大的区别。3.3 切点表达式一口吃下execution、within和注解匹配切点表达式是AOP实战里最容易写错也最容易踩坑的地方。我推荐掌握三种匹配方式足以覆盖绝大多数场景。execution最常用、最精确按方法签名匹配。格式看起来长拆开记就不难execution(修饰符? 返回类型 类路径?方法名(参数类型) 异常模式?)实际我写代码时常见写法是// 匹配某个包下所有类的所有方法常用 execution(* com.example.service..*(..)) // 匹配某个包下所有以create开头的方法 execution(* com.example.service..create*(..)) // 匹配指定类的指定方法 execution(public String com.example.service.UserService.getName(..))注意..在类路径部分表示包及其子包在参数部分表示任意数量的参数。within按类匹配比execution粒度粗适合匹配整个类的所有方法。比如within(com.example.service..*)匹配service包下所有类的所有方法。annotation按注解匹配这是项目里最优雅的方式——你自定义一个注解写在需要横切的方法上切点表达式去匹配“带这个注解的方法”。这种方式精准、直观、可控比包名匹配清晰得多后面实操部分我会演示。另外还有bean()匹配Spring Bean名称、within匹配类上标有注解的所有方法。初学阶段execution annotation 够用。4. 深入原理JDK动态代理和CGLIB动态代理前面说的织入、动态代理这些词终端里看起来有点虚。这一节我用代码把Spring AOP的底层逻辑完全摊开。理解了这个你面试环节“AOP实现原理”这种题随便聊。4.1 为什么需要代理假设你写了UserService类里面有一个addUser()方法。Spring AOP想要做到“不修改UserService源码的情况下给addUser()加日志”唯一可行的运行期办法就是不要直接给你用UserService对象而是给你一个经过加工包装后的代理对象。你调用addUser()时真正执行的是代理对象里的增强代码代理对象再去调用你原本的addUser()。这就是动态代理的核心思路。Spring容器里所有的Bean如果被AOP切面命中容器中实际放着的就是代理对象不是原始对象。这点很关键很多“切面不生效”的问题都是在这里没搞明白后面会展开。4.2 JDK动态代理基于接口JDK动态代理是Java内置的代理机制核心是java.lang.reflect.Proxy和InvocationHandler。原理一句话运行时为接口生成一个实现类的字节码这个实现类通过你传入的InvocationHandler来转发所有方法调用。限制也很明显——目标类必须实现了至少一个接口因为JDK代理生成的代理对象是接口的实现类和你的目标类没关系。看个完整可运行的例子public interface UserService { String getUserName(Long id); } public class UserServiceImpl implements UserService { Override public String getUserName(Long id) { return 用户- id; } } public class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println( JDK Proxy进入方法: method.getName()); Object result method.invoke(target, args); // 反射调用目标方法 System.out.println( JDK Proxy方法执行完了返回值: result); return result; } } // 使用方式 UserService targetService new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( targetService.getClass().getClassLoader(), targetService.getClass().getInterfaces(), new LogInvocationHandler(targetService) ); System.out.println(proxy.getUserName(1L));输出效果 JDK Proxy进入方法: getUserName JDK Proxy方法执行完了返回值: 用户-1 用户-1注意调用方的变量类型是UserService接口持有的是代理对象代理对象内部通过反射method.invoke(target, args)把调用转发给真实的UserServiceImpl。Spring AOP默认走的就是这条路——只要目标Bean实现了接口Spring就优先使用JDK动态代理。4.3 CGLIB动态代理基于继承如果目标类没有实现任何接口Spring会退而求其次使用CGLIB。CGLIBCode Generation Library的原理是运行时生成一个目标类的子类通过重写父类方法来实现增强。调用代理对象的方法时实际执行的是子类里重写过的方法子类方法里对目标逻辑父类方法和增强逻辑做一个编排。典型CGLIB示例// 注意目标类不需要实现任何接口 public class OrderService { public void createOrder() { System.out.println(创建订单核心业务逻辑); } } // 引入CGLIB依赖Spring已经内置了它 public class CglibProxyExample { public static void main(String[] args) { OrderService target new OrderService(); Enhancer enhancer new Enhancer(); enhancer.setSuperclass(target.getClass()); // 设置父类 enhancer.setCallback(new MethodInterceptor() { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println( CGLIB前置增强: 准备创建订单); Object result proxy.invokeSuper(obj, args); // 调用父类目标方法 System.out.println( CGLIB后置增强: 订单创建完成); return result; } }); OrderService proxy (OrderService) enhancer.create(); proxy.createOrder(); } }这里有个关键点proxy.invokeSuper(obj, args)是CGLIB特有的调用方式直接调父类的方法避免绕过增强逻辑或性能损失。如果用method.invoke(target, args)这种反射调用会丢失代理增强链路。关于Spring Boot 2.x从Spring Boot 2.0开始Spring对代理方式的选择逻辑发生了变化——即使类有接口默认也会优先使用CGLIB即spring.aop.proxy-target-classtrue成为默认。这个设计是为了避免有些类实现了接口但想被代理时还得手动配置的麻烦。如果你的项目是传统Spring配置非Spring Boot默认仍然是接口存在优先JDK代理。4.4 核心对比JDK代理 vs CGLIB对比维度JDK动态代理CGLIB动态代理目标要求目标类必须实现接口目标类不能是final类final方法无法重写代理实现机制为接口生成实现类生成目标类的子类是否需要额外依赖不需要JDK内置需要引入cglib或spring-core自带的CGLIB性能特点创建代理对象时速度较快运行时反射调用有开销创建代理对象较慢需要生成字节码运行时通过MethodProxy调父类方法略快方法级限制只能代理接口中声明的方法final方法无法代理、static方法无法代理调用方式method.invoke(target, args)proxy.invokeSuper(obj, args)实际使用中对绝大多数业务系统而言两者的性能差距微乎其微不需要过度纠结。真正需要关心的是如果目标类没实现接口记得不能代理final方法因为final方法不能被重写以及如果目标方法在接口里没有声明JDK代理会漏掉它。5. 手把手实战Spring Boot里写一个可落地的AOP切面理解原理之后我们来点真东西。下面这个案例我会完整地写出依赖、代码、执行效果你照着敲一遍就能跑通。5.1 项目准备和依赖新建一个Spring Boot项目2.x或3.x都可以核心场景是给一堆查询接口加“接口耗时统计 打印入参出参 记录操作日志”的能力。关键依赖只有两个web和aopSpring Boot 2.x里spring-boot-starter-aop就包含AspectJ运行时dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependencySpring Boot的自动配置会在类路径发现Aspect注解的类后自动启用AOP代理不需要额外加EnableAspectJAutoProxy——Spring Boot已经默认开了。只有在原生Spring项目里才需要手写这个注解。5.2 第一种写法基于包路径的切面简单粗暴先来最直接的方案拦截com.example.demo.service包下的所有方法。Aspect Component public class ServiceLogAspect { private static final Logger logger LoggerFactory.getLogger(ServiceLogAspect.class); // 切点表达式匹配service包下所有类的所有方法 Pointcut(execution(* com.example.demo.service..*.*(..))) public void servicePointcut() {} // 环绕通知 Around(servicePointcut()) public Object around(ProceedingJoinPoint pjp) throws Throwable { // 拿到目标方法信息 MethodSignature signature (MethodSignature) pjp.getSignature(); String className signature.getDeclaringType().getSimpleName(); String methodName signature.getName(); Object[] args pjp.getArgs(); long start System.currentTimeMillis(); logger.info([AOP] {}.{} 入参: {}, className, methodName, Arrays.toString(args)); Object result null; try { result pjp.proceed(); // 正常返回记录耗时和返回值 long cost System.currentTimeMillis() - start; logger.info([AOP] {}.{} 返回: {}耗时: {}ms, className, methodName, result, cost); return result; } catch (Throwable throwable) { long cost System.currentTimeMillis() - start; logger.error([AOP] {}.{} 异常: {}耗时: {}ms, className, methodName, throwable.getMessage(), cost); throw throwable; } } }一些经验细节入参打印时小心敏感信息密码、token等别直接Arrays.toString(args)全打出来。工作中的做法是自定义序列化策略敏感字段脱敏。记录耗时一定要放在finally里或者用异常分支兜底否则方法一抛异常耗时就被吞了。就算日志框架对{}占位符做了惰性拼接也要注意如果入参对象重写了toString发生过重的序列化性能会有损耗。压测环境下慎用大对象入参打印。5.3 第二种写法自定义注解 切面推荐用于正式项目基于包路径的写法在大型项目里不够精准——你只想要少数关键接口记录日志而不是service包下所有方法都记录。我的习惯做法是自定义一个注解然后切面只切有这个注解的方法。这样做有几个好处精确控制范围、可以通过注解参数传递业务信息比如操作类型、代码自解释。效果如下Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OperationLog { String module(); // 模块名比如用户管理 String operation(); // 操作名比如新增用户 }切面写法Aspect Component public class OperationLogAspect { private static final Logger logger LoggerFactory.getLogger(OperationLogAspect.class); // 切点直接匹配带OperationLog注解的方法 Pointcut(annotation(com.example.demo.annotation.OperationLog)) public void logPointcut() {} // 通过proceed方法内的注解对象拿到注解里的业务信息 Around(logPointcut()) public Object around(ProceedingJoinPoint pjp) throws Throwable { MethodSignature signature (MethodSignature) pjp.getSignature(); Method method signature.getMethod(); OperationLog annotation method.getAnnotation(OperationLog.class); String module annotation.module(); String operation annotation.operation(); // 这里你可以注入一个日志Service把操作记录写到数据库或MQ // opLogService.saveLog(module, operation, params, result, cost, userId, ip); return saveLogAround(pjp, module, operation); } }用法Service public class UserService { // 只需要一行注解日志逻辑自动生效 OperationLog(module 用户管理, operation 新增用户) public void addUser(UserAddDTO dto) { // 业务逻辑 } }这种“注解驱动切面”的模式在真实项目中是最常见的。后面如果你要加幂等校验、限流、分布式锁思路一模一样——定义注解 → 定义切面 → 注解打在需要的方法上。5.4 控制多个切面的执行顺序Order有时候多个切面都命中同一个方法比如一个切面做权限校验一个切面做日志记录它们之间的执行顺序就很重要。总不能先记录日志再校验权限——那可能把未授权操作也记进去了。Spring AOP中用Order控制顺序值越小优先级越高越先进入Aspect Component Order(1) public class PermissionAspect { // 先执行权限校验 Around(annotation(com.example.demo.annotation.RequirePermission)) public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable { // 校验权限失败直接抛异常 return pjp.proceed(); } } Aspect Component Order(2) public class LogAspect { // 后执行日志记录 // ... }嵌套关系和钓鱼“洋葱皮”类似Order(1)在最外层最后进入目标方法。5.5 获取参数及返回值的细节技巧切面代码获取参数有几种途径效果各有不同我按推荐程度排个序用ProceedingJoinPoint.getArgs()获取参数数组。适用性最广但参数顺序和位置不一定容易对应。用MethodSignature的getParameterNames()获取参数名和getArgs()的索引对应。在Spring Boot 2.x项目里如果开启了-parameters编译参数Spring Boot Maven插件默认会带参数名可以拿到。用joinPoint的getTarget()拿目标对象再反射拿一些上下文信息但一般用得少。要拿到方法的返回值也有讲究在Around里直接用proceed()的返回值即可。在AfterReturning里可以用returning属性指定返回值注入参数名AfterReturning(pointcut servicePointcut(), returning result) public void afterReturning(JoinPoint joinPoint, Object result) { // result就是目标方法的返回值 }最后提一个容易踩的坑切面方法本身不要抛异常尽量不要影响原主流程。环绕通知里如果对返回值做了包装或修改一定要注意返回值类型不能被改变——比如目标方法返回User你的环绕通知返回了一个String强转直接报ClassCastException。6. 使用场景地图AOP适合做什么不适合做什么6.1 日志记录与操作审计这是AOP最常见的落地场景之一。尤其是“一键记录用户所有操作行为”通过AOP拦截所有标注了操作日志注解的方法记录操作人、操作时间、入参关键信息、操作结果。这种横切逻辑和业务代码天然分离换任何监控系统或审计系统都不会影响业务逻辑本身。我的经验操作审计的切面里重点记录“谁、何时、做了什么、结果是成功还是失败”而不是把全部入参和返回体都存下来。数据量太大了一种尽收眼底的日志表三天就能把数据库撑膨胀而且隐私安全问题严重。6.2 接口权限校验Spring Security的PreAuthorize注解底层就是基于AOP的。你也可以自己实现轻量的接口权限校验。比如有一个RequirePermission(order:create)注解切面里查询当前用户的权限集合如果不包含则抛出无权限异常。好处是权限逻辑集中在一个切面类里多个接口共享同一套校验逻辑以后升级权限模型只改一个地方。6.3 性能监控与耗时统计这是我自己项目上线前必做的一步用AOP切面统计核心方法的耗时发现慢接口。可以把耗时分成几个级别处理超过500ms警告、超过1s报错并上报到监控系统。这个思路配合Nacos、Prometheus等监控体系性能问题基本能在用户感知之前被发现。6.4 事务管理和分布式锁Transactional本身就是一个生动的例子。但更典型的自定义场景是分布式锁在秒杀或提现金额更新场景中你希望某个方法在执行前获取Redis分布式锁执行后释放锁。用AOP完美契合——DistributedLock(key #userId)这种注解打上去切面负责加锁、解锁、处理获取锁失败的情况。业务代码里不出现任何锁逻辑。6.5 统一异常包装和服务降级在开发对外暴露的REST API时经常需要把底层异常统一包装成合适的错误码返回给前端。可以在切面里捕获特定类型异常做一次统一转换。同理服务降级逻辑比如调用第三方接口超时时返回默认值也可以做成切面不过要小心切面的粒度粒度和是否真的是横切逻辑。如果只是一两个方法需要降级直接在业务代码里写反而更清晰没必要硬上AOP。6.6 不适合用AOP的场景AOP不是万能药。我见过不少把AOP用歪的案例提醒你避开业务状态强依赖场景比如一个方法是否执行取决于前一个方法是否成功更新了数据库状态这种强耦合业务逻辑不适合放进切面否则排查问题时你要在业务代码和切面代码之间来回跳维护成本很高。高频热点方法的超轻量增强如果方法被千万级QPS调用切面本身就增加了方法调用链路和反射桩开销虽然JDK代理和CGLIB代理性能都还行但终归是有开销的。这种场景下直接在方法里写死更可控。需要精细化调试的场景AOP在日志里表现为一层代理调用定位问题时多一层“隐身跳板”排障的人如果不懂AOP概念会非常困惑。你在大规模团队成员的新人培训里需要额外讲解清楚AOP的存在否则会出现“这个方法明明有人调用为什么日志里没有”之类的排查事故。7. 典型面试题速查含深度解析这一节面向正在准备面试的读者。整理几个高频问题我尽量用“面试官为什么这么问”的角度来回答。7.1 什么是AOP它和OOP的区别是什么AOP是面向切面编程把横切关注点从业务逻辑中抽取出来并统一管理通过代理机制在运行时织入。OOP是纵向模块化把系统拆分为对象AOP是横向模块化解决跨对象的公共逻辑。两者互补不是替代。7.2 Spring AOP的实现原理是什么核心是动态代理。如果目标类实现了接口Spring使用JDK动态代理生成接口实现类如果目标类没有实现接口Spring使用CGLIB动态代理生成目标类的子类。在Spring Boot 2.x后代理策略默认优先CGLIB。7.3 JDK动态代理和CGLIB动态代理有什么区别这是必考题按这张表答基本能覆盖全部得分点一个基于接口一个基于继承一个JDK自带一个额外引入CGLIB库一个用InvocationHandler转发一个用MethodInterceptor和MethodProxy.invokeSuperJDK代理只能代理接口方法CGLIB不能代理final方法。7.4 同一个类内部方法调用AOP为什么不生效典型坑。比如UserService里methodA()调用了本类的methodB()只给methodB()加了切面调methodA()时发现methodB()的切面没生效。原因是AOP代理的原理是外部拿到的是代理对象但methodA()内部用this调用的methodB()this指向的是原始对象不是代理对象。所以methodB()的调用没有经过代理。解决办法一般是让methodB()通过注入的代理对象来调用依赖注入自己、把methodB()挪到另一个Bean里去、或者直接在methodA()上加上同样的切面规则。7.5 Spring AOP和AspectJ有什么区别Spring AOP是运行时织入基于动态代理AspectJ是编译期静态织入功能更强性能更好但使用复杂。Spring AOP只支持方法级别的连接点AspectJ支持字段、构造器、甚至初始化块的连接点。日常业务里Spring AOP够用。7.6 环绕通知和前置通知、后置通知的关系环绕通知最强大可以自己控制目标方法的执行时机前置和后置是特殊的单向通知。Around里调用proceed()前是前置逻辑正常返回后是返回后逻辑抛异常后是异常逻辑。实际项目中复杂逻辑用环绕简单场景用定向通知更清晰。7.7 Spring事务的AOP实现Transactional为什么能在异常时自动回滚Transactional被AOP拦截后等同环绕通知。目标方法执行前事务切面获取数据库连接并开启事务取消自动提交方法正常返回后事务切面提交事务方法抛出RuntimeException等异常时事务切面执行回滚操作并把异常继续向上抛出让上层感知。你可以把它理解为一条“竖起耳朵听着目标方法结果的环绕监听器”。7.8 如何让多个切面按指定顺序执行用Order注解值越小优先级越高。需要注意这个顺序在嵌套模型里理解为外层到内层执行“进入目标”的顺序和“退出目标”的顺序是相反的。举个例子Order(1)的权限切面先进入目标方法但在退出时反而比Order(2)的日志切面更晚执行——想象一下剥洋葱和套娃一层层包裹最后剥的时候先启后剥。8. 实战问题排查切面为什么不生效如何优雅规避写AOP的难点往往不在“会写”而在“出问题时怎么定位”。这里把最常遇到的坑集中列一下附上排查思路。现象可能原因排查方式和解决手段切面类明明写了但方法就是不进切面切面类没被Spring扫描到、切点表达式写法有误、目标方法自己调自己检查Component或Aspect是否生效用Pointcut里的表达式先在测试类里试验检查是否有内部调用接口方法里有增强非接口方法没增强用了JDK动态代理代理对象是接口实现类接口没声明的方法根本不在代理范围改成CGLIB代理或确保目标方法声明在接口中目标方法是final的增强不生效CGLIB无法重写final方法去掉final修饰符或改走接口JDK代理Around里调用了proceed()但业务方法抛了异常切面的异常分支没执行可能是proceed()本体方法抛的异常类型不对检查是否正确捕获了Throwable并在finally里做清理我习惯环绕通知里一定要有finally块同一个方法有多个切面时日志记录时机不对切面顺序没控制给不同切面统一分配Order值形成约定Spring Boot 3.x项目里AOP不生效Spring Boot 3.x是Spring Framework 6.xAOP模块名称和包路径有调整确认导入了spring-boot-starter-aop而非老项目的spring-boot-starter里隐式包含是否不一样了还有一个高发坑写了切面但目标方法是被别的Bean调用那个Bean也是代理对象但代理是懒加载模式下在某个时创建失败的。排查时最直接的手段是加一个启动后的测试接口把UserService的类型打出来——打印出来看着像com.sun.proxy.$Proxy20就是JDK代理看着像UserService$$EnhancerBySpringCGLIB$$xxx就是CGLIB。如果打印出来是普通的UserServiceImpl恭喜你你的切面根本没生效先检查Spring配置。写AOP切面的一点个人风格建议尽量使用annotation或within加注解的组合做精准匹配少用大范围包扫描的execution切面类内部逻辑要越轻越好——你是在给所有方法织入额外开销如果切面里又做了重的DB查询或外部调用等于给所有业务方法累加了双倍负担。8.2 升级版切面里的耗时统计要不要做成可动态开关的在做监控类切面时通常会遇到“线上要不要关掉切面日志”的争论。我的建议是一开始就设计成可配置开关。最简单的手段是配合ConditionalOnProperty或者直接在切面里读一个配置项Value(${app.aop.enabled:true}) private boolean aopEnabled; Around(logPointcut()) public Object around(ProceedingJoinPoint pjp) throws Throwable { if (!aopEnabled) { return pjp.proceed(); // 关闭切面时直接放行 } // 正常切面逻辑 }这样在压测、异常排查时可以动态关闭切面避免正常故障排查时被自己的日志洪流淹没。最后分享一个方法论上的体会AOP确实强大但它解决的问题本质上属于代码组织方式的问题。我见过团队把AOP写得极其飘逸——一个方法上挂着七八个注解每个注解背后又是一个复杂的切面代码确实“优雅”了但排查问题时长路漫漫。我的原则是AOP只服务于跨多个类的横切逻辑单一方法局部逻辑永远优先用普通代码写清楚切面数量控制在5个以内每个切面职责单一。AOP是工具服务于代码可维护性我反复强调这一点——不要为了炫技而过度使用AOP那是把维护成本从今天转移到明天。
返回列表