Spring AOP vs Solon AOP:动态代理与字节码增强深度对比

发布时间:2026/10/3 3:51:52
Spring AOP vs Solon AOP:动态代理与字节码增强深度对比 Java 圈里一聊到 AOP默认语境基本都是 Spring AOP这本身没什么毛病Spring 在 Java 服务端的话语权摆在那里。但最近问 Solon 的人明显变多了尤其是那些要选型、想从 Spring Boot 往国产轻量框架迁移的团队上来第一句基本都是“Solon 的 AOP 和 Spring AOP 是不是一回事我的切面代码能直接搬过去吗”这个问题我在好几个技术群里都见人问过但网上很少有把两套方案掰开揉碎讲清楚的文章。今天就从原理、写法、边界、性能和选型几个维度把这两套 AOP 的差别彻底聊透想弄明白的可以直接拿这篇当参考。1. 机制本质拆解动态代理与字节码增强1.1 Spring AOP 是怎么实现“切”的Spring AOP 的核心是运行时动态代理具体分两条路JDK 动态代理和 CGLIB。JDK 代理要求目标对象至少实现一个接口它在运行时通过Proxy.newProxyInstance()为接口生成一个代理类所有调用都先进InvocationHandler.invoke()再由你去决定是直接放行还是塞入切面逻辑CGLIB 则不走接口它直接以目标类为父类在运行时生成一个子类然后重写目标方法调用时通过MethodInterceptor转发。Spring 的切面能生效依赖一个关键组件AbstractAutoProxyCreator这类BeanPostProcessor。Bean 正常经历实例化、属性填充、初始化之后Spring 会拿容器里所有 Advisor切面通知的载体挨个做匹配匹配上了就用ProxyFactory创建代理对象。所以你最终从容器里拿到的 Bean很多时候根本不是你写的那个类的原始实例而是一个代理包装过的对象。面试喜欢问的“Spring AOP 为什么默认对接口生效”本质上就是这么来的。Spring Boot 2.x 以后做了一个比较重要的默认值调整spring.aop.proxy-target-classtrue也就是默认走 CGLIB 方案哪怕目标类实现了接口也尽量用子类代理避免接口代理遇到的那些强转和接口演化问题。但底层依然是“运行时生成代理对象”那一套区别只是实现手法不同。1.2 Solon AOP 的 ASM 增强是怎么一回事Solon 是国产的轻量级 Java 框架设计目标就是“更小、更快、更少依赖”。在 AOP 这块它没有沿用 Spring 的动态代理思路而是直接采用ASM 字节码增强。Solon 在构建容器里的 Bean 时如果发现这个类需要被切面处理会直接用 ASM 读取并改写类字节码生成增强后的类再交给 JVM 加载。说直白一点Spring 是“先有原对象再包一层代理壳”Solon 是“在创建对象之前就把类本身‘改造’成增强版”。这种方案有几个天然优势。第一不需要接口类上直接做文章第二少了动态代理那一长串的组装逻辑第三因为增强类的生成发生在 Bean 构建阶段整个调用链在运行期会短不少。我当时第一次看 Solon 源码里 AOP 相关的实现时第一反应是“这玩意儿怎么敢这么直接”但跑起来之后确实稳。1.3 织入时机差异带来的连锁反应“织入时机”是这两套 AOP 最根本的分水岭。Spring 把 AOP 织入放在了 Bean 生命周期靠后的阶段Bean 先实例化、再初始化最后才被后处理器包成代理。这样做的代价就是它必须处理一堆“对象还没包代理就被别人引用”的尴尬情况。最典型的就是循环依赖。Spring 为了解决循环依赖加上 AOP 代理之间的矛盾搞出了著名的三级缓存一级缓存放成品 Bean二级缓存放早期暴露的裸对象三级缓存放ObjectFactory。当一个 Bean 在属性填充阶段被提前暴露出去时其他 Bean 拿到的是还没有做 AOP 包装的原始对象等这个 Bean 最终完成初始化、准备生成代理时Spring 还得回头检查早期引用想办法把代理对象“替换”回去。这一套机制非常精巧但也非常绕很多人看 Spring 源码就卡在getSingleton()和三级缓存这一段。Solon 没有这个包袱。因为它在 Bean 构建阶段就直接生成增强类Bean 第一次出现在容器里时就已经是“完成 AOP 形态”的了。不存在先给一个裸对象、后面再偷偷换代理的问题自然也不需要搞二级、三级缓存去兜底。这不是说 Solon 就完全不会有循环依赖而是它的容器在循环依赖场景下不需要再叠加一层 AOP 代理的复杂度设计上确实清爽一大截。2. 写代码时能感知到的区别API 与使用方式2.1 Spring 的 AspectJ 风格Pointcut 表达式是灵魂Spring AOP 的对外 API 完全沿用了 AspectJ 的注解风格最核心的三个注解是Aspect、Pointcut、Around/Before/After。定义切面时你要写 Pointcut 表达式比如Aspect Component public class LogAspect { Around(execution(* com.example.service..*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); try { return pjp.proceed(); } finally { System.out.println([spring aop] 耗时: (System.currentTimeMillis() - start) ms); } } }这条execution(* com.example.service..*.*(..))看着吓人拆开也就四部分*表示返回值任意com.example.service..*匹配 service 包及其子包下的所有类最后一个*匹配任意方法名(..)表示参数列表任意。Spring AOP 的表达式体系非常完整还有within、target、args、annotation等一堆写法能实现很精细的切入点控制。代价是这套东西学习成本高新手经常被表达式搞晕甚至因为表达式写错导致切面静默失效。Spring Boot 下连EnableAspectJAutoProxy都不用手动加自动装配会帮你开好。但如果自己搭 Spring 原生环境忘了加这个注解Aspect类就完全不生效这是新手最容易踩的坑之一。2.2 Solon 的极简切面AopAspect 与 AroundSolon 的 AOP 风格要直接得多。它不搞 Pointcut 表达式那一套本质上更接近“容器级拦截”。定义一个切面最常见的做法是实现AopAspect接口然后在around方法里写逻辑Aspect Component public class TimeAspect implements AopAspect { Override public Object around(AopInvocation inv) throws Throwable { long start System.currentTimeMillis(); try { return inv.invoke(); } finally { System.out.println([solon aop] 耗时: (System.currentTimeMillis() - start) ms); } } }inv.invoke()就等价于 Spring 里的pjp.proceed()继续往下走目标方法。Solon 里也可以用注解式切面在一个容器管理的组件类中直接声明带Around的方法逻辑类似。由于没有表达式语言Solon 的切面匹配逻辑主要靠类型和注解去圈定切面本身用Aspect标注普通的业务类用Component或Bean交给容器管理Solon 在构建时会自动判断哪些 Bean 需要应用切面。写起来少了一层抽象但也意味着你想实现“只切某个包下的某些方法”这种精细控制时需要自己做判断或者用自定义注解来标记目标。2.3 一个代码层面的最小对比示例为了直观我直接列一个最小的对比表。同样一个“方法耗时统计”需求在两边分别怎么写维度Spring AOPSolon AOP切面类注解AspectComponentAspectComponent核心拦截注解Around/Before/AfterAround/AopAspect接口切入点表达AspectJ Pointcut 表达式无表达式靠类型和注解匹配调用下一个方法pjp.proceed()inv.invoke()获取目标信息pjp.getSignature()/getTarget()AopInvocation提供对应访问方式启动开关Boot 自动原生环境需EnableAspectJAutoProxy无需额外开关容器构建时默认生效能看出Solon 的 AOP API 明显更“短平快”没有把 AspectJ 那套理论搬过来而是围绕“拦截方法调用”这件事做了最简实现。对从 Spring 迁过来的同学来说适应成本主要在切点表达式的部分Spring 里你用表达式精确控制范围Solon 更考验你用注解设计来达到同样的隔离效果。3. 边界条件大比武什么能拦什么拦不到3.1 Spring AOP 的失效清单Spring AOP 用得太深的人多半都在“拦截失效”上栽过跟头。我把常见的拦不到的场景列出来你对照着排查同类内部自调用this.doSomeThing()这种调用不会经过代理对象切面直接失效。这是 Spring AOP 最经典的坑之一。静态方法无论 JDK 代理还是 CGLIB静态方法都不在实例方法拦截范围内。final 方法CGLIB 靠继承重写方法final 方法无法重写自然拦不到。private 方法子类无法覆写父类的 private 方法。构造方法代理生成发生在 Bean 实例化之后构造函数本身不会被 AOP 拦截。非 Spring 管理的对象直接new出来的对象跟容器没有任何关系切面不存在。JDK 代理的接口限制如果走 JDK 动态代理代理对象只能转成接口类型代码里强行 cast 到实现类会抛ClassCastException。这也是为什么 Spring Boot 2.x 以后默认proxy-target-classtrue的原因之一为了少踩这种雷。3.2 Solon AOP 的边界与限制Solon 的 AOP 边界和 Spring 有很多重叠的地方但也有一个明显差异。它同样只对容器管理的 Bean 生效直接new出来的对象不会经过增强。final 类、final 方法、static 方法、构造方法Solon 的字节码增强方案同样拦不到——因为它生成的也是子类子类没法覆写 final 成员。真正的差异在“同类内部自调用”上。Spring 因为代理是包裹在原对象外面的this指向的还是原始目标对象所以内部调用会绕过代理。Solon 的增强发生在类层面Bean 拿到手本质上是增强子类的实例内部方法之间互相调用时this指向的就是增强后的对象因此我实测下来Solon 里同类内部调用通常也能被切面拦到。但这里要加个限定这个结论基于我使用的 Solon 2.x 版本AOP 实现属于框架内部细节不同版本可能有调整你评估时最好在自己项目里做一次小实验。3.3 从三级缓存看两套 AOP 的实现哲学这一节其实是面试中特别爱被连带追问的点。Spring 的三级缓存表面上解决的是循环依赖实际上它把“循环依赖”和“AOP 代理”两件事强行耦合在了一起。三级缓存的三层设计不是拍脑袋想出来的一级缓存给最终成品二级缓存存放早期暴露的原始对象三级缓存提供ObjectFactory让提前引用的对象有机会在未来被替换成代理。如果 Spring 没有 AOP二级缓存基本就够了正因为有 AOP它才需要再包一层ObjectFactory来做延迟决策。Solon 的做法从源头上规避了这类复杂度。因为 Bean 构建的时候就已经是增强类不需要“先裸对象、后包装”的过渡容器内部根本不需要设计一条“早期引用升级为代理”的路径。所以说到底这两套 AOP 的本质差异不只是“代理 vs 字节码”这种技术选型而是背后两套完全不同的生命周期设计哲学Spring 在兼容性上做了太多妥协Solon 则因为年轻可以用更干净的方式重新设计。4. 性能感知、资源占用与面试常考点4.1 启动构建与运行时调用的开销对比性能是选型时绕不开的话题。先说启动阶段。Spring 在 Bean 初始化完成后会让所有Advisor去逐个匹配业务 Bean匹配上了再做动态代理组装。当项目有几百个 Bean、十几个切面时这部分工作虽然是必要的但确实会占掉一部分启动时间。CGLIB 生成子类也需要额外开销Spring 虽然在后续版本做了不少缓存优化但整体性能模型还是偏重。Solon 的 AOP 是在 AopContext 构建 Bean 时直接走 ASM 生成增强类类加载完成即用省掉了运行时动态代理的组装和匹配环节启动阶段的开销结构上更轻。运行阶段的差异主要体现在调用链长度。Spring AOP 一次方法调用要经过代理对象、拦截器链、反射或MethodProxy分发链路长环节多。Solon 的增强类是编译期生成的直接调用结构inv.invoke()的调度开销更小。不过说实话在真实业务系统里方法调用的耗时大头在 SQL、RPC 和业务逻辑AOP 本身的 CPU 开销占比非常低。除非你是做基础中间件、追求极致性能否则这两者在运行性能上的差距远没有启动速度和内存占用来得直观。4.2 AOP 使用场景全景日志、事务、权限等抛开框架差异AOP 本身解决的是一大类横切关注点问题也就是把各个业务模块里都存在的“公共逻辑”抽出来集中处理。最常见的场景有操作日志记录接口调用、方法入参出参、操作人、耗时这是 AOP 最经典的应用之一。事务控制Spring 的Transactional底层就是通过 AOP 代理实现的大家写代码时用的是注解实际上方法进入前开启事务、方法退出后提交或回滚全是切面逻辑在干活。权限校验Spring Security 里的PreAuthorize本质也是对带注解的方法做拦截校验当前用户是否具备权限。参数校验与幂等控制对入参做统一校验或者通过方法级 AOP 加分布式锁、做重复提交拦截。性能监控与链路追踪对关键方法做埋点上报耗时、异常率这些交给 AOP 做可以做到对业务代码零侵入。缓存处理Spring 的Cacheable也是 AOP 的功劳方法执行前先查缓存命中直接返回不命中才执行方法。这些场景在 Spring 和 Solon 里都能做。Spring 的优势是生态里已经沉淀了大量现成的 AOP 扩展比如Transactional、Cacheable属于开箱即用Solon 也提供类似编程能力但封装度不如 Spring 全家桶那么细很多场景需要你手动写切面逻辑来组装。4.3 技术选型建议什么时候用 Spring什么时候用 Solon聊选型不能踩一捧一。如果你所在团队已经深度绑定了 Spring 生态团队里人人熟悉 Spring 的注解和排查思路那继续用 Spring Boot 是最稳妥的选择换框架带来的迁移成本大概率超过性能收益。如果项目要接入大量第三方开源库比如 Spring AI、各种 Spring Cloud 组件那基本没有选择空间生态就是硬门槛。什么情况适合认真考虑 Solon我的个人判断是从零开始的中小型项目、内部工具、边缘服务、资源受限环境比如小内存的容器、嵌入式设备Solon 的“轻”是真的香。它的学习曲线对 Spring 开发者来说也不陡IoC 和 AOP 的核心概念一致只是 API 简化了不少。另外“国产化”“自主可控”这类需求在特定领域也是硬性加分项。但要说缺点也很明显社区体量、资料丰富度、第三方集成能力至少在现阶段和 Spring 不在一个量级。选型本质上是在“生态成熟度”和“轻量高效”之间做权衡。5. 实操对比与踩坑记录5.1 动手演示一个计时切面在两边怎么落我直接用一个最简单的方法耗时切面做演示你没跑过 Solon 的话可以把这段代码贴到你的项目里试试。Spring 侧完整步骤是三步定义切面类、写Around、保证类被 Spring 扫描到。Aspect Component public class SpringTimeAspect { Around(execution(* com.example.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { long start System.nanoTime(); try { return pjp.proceed(); } finally { System.out.println(Spring AOP cost: (System.nanoTime() - start) ns); } } }Solon 侧同样定义切面类业务代码完全不用动Aspect Component public class SolonTimeAspect implements AopAspect { Override public Object around(AopInvocation inv) throws Throwable { long start System.nanoTime(); try { return inv.invoke(); } finally { System.out.println(Solon AOP cost: (System.nanoTime() - start) ns); } } }两边跑起来效果是一样的service 包里所有方法执行时都会打印耗时业务代码零侵入。区别只是在定义方式上——Spring 通过表达式划定范围Solon 用类型和注解机制完成同类效果。如果你只是做日志、计时这类简单切面迁移成本非常低基本上就是改pjp.proceed()为inv.invoke()。5.2 我踩过的几个典型的坑第一个坑是 Spring 自调用失效。以前我在一个 service 里写了两个方法A 方法调 B 方法B 上加了Transactional结果事务死活不生效。排查半天才发现问题不在事务配置而是this调用绕过了代理。解决办法无非三种把 B 拆到另一个 Bean、用AopContext.currentProxy()、或者干脆用TransactionTemplate手动管理事务。这个场景在面试里也常考背后就是“代理对象和目标对象不是同一个对象”这个本质。第二个坑是 Solon 的 AOP 只对容器 Bean 生效。有一次我在 Solon 项目里直接用new创建了一个对象然后在里面调用了带切面的方法发现日志完全不打印当时还以为是框架 bug。后来翻了一下文档才反应过来AOP 本身就是容器给你的能力脱离容器创建的对象框架管不着。这点和 Spring 的逻辑完全一致属于概念层面的问题不是框架缺陷。第三个坑是切面顺序。Spring 里多个切面同时命中一个方法时执行顺序由Order或者Ordered接口控制顺序不对会导致日志、事务、校验之间的执行顺序和你设想的不一样。Solon 里同样存在顺序问题虽然 API 表达上没有 Spring 那么多约定但多切面场景下你需要主动关注切面之间的优先级。我建议任何项目里把日志这类横切逻辑和事务这类强约束逻辑拆成独立的切面然后用序值固定顺序避免“看起来都生效了但行为不符合预期”的尴尬。5.3 常见问题速查表最后把最常被人问的几个问题整理成一张表方便收藏备查。问题Spring AOPSolon AOP同类内部调用能被拦截吗不能this调用绕过代理通常可以按版本实际表现为准被代理类必须实现接口吗JDK 代理需要CGLIB 不需要Boot 默认 CGLIB不需要直接类增强static / private / final 方法能拦吗不能不能构造方法能被拦截吗不能不能非容器创建的对象能拦截吗不能不能切入点支持复杂表达式吗支持 AspectJ 全套不支持表达式靠类型和注解匹配需要额外开关吗Spring 原生需要EnableAspectJAutoProxyBoot 自动不需要额外开关这张表可以当做一个排查手册来用。如果项目里切面没生效先看对象是不是容器管理的再看方法修饰符是不是 final/static/private最后再考虑是不是自调用问题。这套排查顺序在 Spring 和 Solon 里都通用。根据我个人的迁移经验给想换框架的团队一个建议动手之前先把项目里所有用 AOP 的地方列出来逐一评估切面类型和依赖的 Spring 特性。如果只是日志、鉴权、简单监控这类切面Solon 迁移成本很低如果大量依赖Transactional、Cacheable、Spring Security 方法级安全这类 Spring 封装好的能力一定先把替代方案想清楚再动手否则切到一半会发现很多事情不是不能做而是得自己重新造轮子。最后再补充一个实用的验证技巧不管用哪个框架写 AOP 的时侯先在切面里打一条日志然后走一次业务调用确认日志出来了再继续写后面的逻辑。这一步能帮你尽早发现“切面到底有没有生效”的问题省掉后面排查的力气。