从 Spring 到 Quarkus为什么依赖注入正在从“运行时”退回“编译期”在日常的 Java 开发中很多人可能会产生过这样一个疑问既然改变注入对象比如换一个实现类最终都要修改 Java 代码并重新编译那为什么 Spring 宁愿在启动时耗费大量资源去做反射和动态扫描也不愿意在编译阶段就把依赖注入DI的关系处理好直到以 Quarkus 为代表的新一代云原生框架横空出世带着“编译期注入”的理念把启动速度拉到了毫秒级这个话题再次被推到了风口浪尖。表面上看这是两个框架的 API 或性能之争但往深了挖这其实是应用架构为了迎合底层基础设施演进而做出的一次重大底层逻辑重构。Spring 的初心与“运行时黑魔法”Spring 这么多年“死磕”运行时注入绝不是技术上做不到而是为了极致的动态灵活性。在早期的 Spring XML 时代它的核心卖点就是“代码与配置分离”。假设我们要把数据库从 Oracle 换成 MySQL开发者甚至不需要重新编译 Java 代码运维只要在服务器上用 Vim 改一下beans.xml里的class路径重启 Tomcat底层实现就被平滑替换了。后来即便大家都转向了注解Spring 依然保留并放大了这种运行时的“黑魔法”。我们可以看一个极其常见的微服务实战场景一份代码靠配置决定生死。假设我们开发了一个支付网关包含支付宝和微信两种实现// 支付宝实现类 Service ConditionalOnProperty(name payment.channel, havingValue alipay) public class AlipayService implements PaymentService { // ... } // 微信实现类 Service ConditionalOnProperty(name payment.channel, havingValue wechat) public class WechatService implements PaymentService { // ... } // 订单服务不管底层是谁只管注入接口 Service public class OrderService { Autowired private PaymentService paymentService; }这段代码最迷人的地方在于你只需要编译打包一次。打出来的payment-app.jar就像一个盲盒当运维在启动脚本里写入java -jar payment-app.jar --payment.channelalipaySpring 会在启动的瞬间读取环境配置只实例化AlipayService并注入到业务中。如果第二天客户说想换成微信支付开发人员一行代码都不用改甚至不需要重新打包。运维只需要去 K8s 的 ConfigMap 里把参数改成wechat重启一下进程。Spring 重新扫描时就会把注入的对象动态替换成WechatService。除了这种按需装配的灵活性Spring 还利用运行时反射做到了动态代理AOP的无感实现。当你写下Transactional时Spring 会在内存中利用 CGLIB 动态生成一个代理类。如果在编译期做这件事就需要引入侵入性极强的字节码增强插件而运行时处理则让这一切对开发者完全透明。魔法的代价与 Quarkus 的“降维打击”Spring 这种“运行时反射 动态分析”的代价就是启动慢、内存消耗巨大。这在以前单体应用长连接跑几个月的时代不是问题但在要求极速弹性扩缩容的微服务和 Serverless 时代成了致命伤。Quarkus 敏锐地抓住了这个痛点给出的解法是构建时决断Build-Time Resolution。Quarkus 把 Spring 在启动时干的脏活累活全部前置到了编译打包阶段。当你敲下mvn package时Quarkus 就会把依赖关系分析透彻生成硬编码的注入类用 Gizmo 等字节码工具直接把 AOP 的代理.class文件在编译期生成好为了极致瘦身它甚至会把没用到的类比如上面例子中未被选中的那个支付实现直接在打包时剔除。但这种极致的优化必然伴随妥协在 Quarkus 中严格区分了“构建时配置”和“运行时配置”。如果你想彻底更换一个底层的实现比如切换数据库驱动或者像上面那样切换核心 Bean你无法再像 Spring 那样只改个运行时参数你必须修改构建配置并重新编译打包。这听起来似乎是历史的倒退其实不然。K8s、CI/CD 与“不可变基础设施”为什么 Quarkus 现在敢于牺牲掉 Spring 引以为傲的“免编译动态换组件”的灵活性因为时代变了底层的运维基础设施变了。在云原生时代Kubernetes 和 CI/CD 彻底改变了软件的交付方式。现代架构推崇的核心理念叫不可变基础设施Immutable Infrastructure。在过去部署一次太痛苦所以大家祈求“最好别重新打包改改配置重启就好”。但今天如果有任何核心组件的变更最标准的动作是提交 PR - 触发 CI/CD 流水线 - 跑通自动化测试 - 重新打出一个全新的 Docker 镜像 - 通过 ArgoCD 等工具进行无缝滚动发布。如今没有任何一个理智的团队会允许运维直接去生产环境的容器里偷偷替换一个驱动 Jar 包。“配置漂移”是 K8s 时代的运维大忌打出来的镜像是什么样它到任何环境里就必须是什么样。既然有了强大且自动化的 CI/CD 兜底重新编译和打包已经变得极速且低风险。Quarkus 正宣扬看透了这一点既然大家最终都要重新打 Docker 镜像那我干脆把依赖注入、AOP 统统提前到编译打包阶段用牺牲掉的那一点“古老的灵活性”换来 Java 应用在 K8s 中几十毫秒的瞬间拉起和极低的内存占用这笔买卖在云原生时代稳赚不赔。结语技术的发展就像一个螺旋上升的圈。在物理机时代Spring 用运行时的性能损耗换取了工程上的灵活性在容器化时代Quarkus 又用编译期的固化换取了运行时的极致性能。就连 Spring 自己也在 Spring Boot 3.0 中全面拥抱了 Spring AOT试图把当年的魔法重新搬回编译期。脱离了基础设施谈框架设计都是耍流氓。从 Spring 到 Quarkus 的理念变迁本质上是 Java 兵器谱为了适应 Kubernetes 和 CI/CD 这片新战场而完成的一次漂亮的自我进化。
从 Spring 到 Quarkus:为什么依赖注入正在从“运行时”退回“编译期”?
发布时间:2026/6/7 15:10:25
从 Spring 到 Quarkus为什么依赖注入正在从“运行时”退回“编译期”在日常的 Java 开发中很多人可能会产生过这样一个疑问既然改变注入对象比如换一个实现类最终都要修改 Java 代码并重新编译那为什么 Spring 宁愿在启动时耗费大量资源去做反射和动态扫描也不愿意在编译阶段就把依赖注入DI的关系处理好直到以 Quarkus 为代表的新一代云原生框架横空出世带着“编译期注入”的理念把启动速度拉到了毫秒级这个话题再次被推到了风口浪尖。表面上看这是两个框架的 API 或性能之争但往深了挖这其实是应用架构为了迎合底层基础设施演进而做出的一次重大底层逻辑重构。Spring 的初心与“运行时黑魔法”Spring 这么多年“死磕”运行时注入绝不是技术上做不到而是为了极致的动态灵活性。在早期的 Spring XML 时代它的核心卖点就是“代码与配置分离”。假设我们要把数据库从 Oracle 换成 MySQL开发者甚至不需要重新编译 Java 代码运维只要在服务器上用 Vim 改一下beans.xml里的class路径重启 Tomcat底层实现就被平滑替换了。后来即便大家都转向了注解Spring 依然保留并放大了这种运行时的“黑魔法”。我们可以看一个极其常见的微服务实战场景一份代码靠配置决定生死。假设我们开发了一个支付网关包含支付宝和微信两种实现// 支付宝实现类 Service ConditionalOnProperty(name payment.channel, havingValue alipay) public class AlipayService implements PaymentService { // ... } // 微信实现类 Service ConditionalOnProperty(name payment.channel, havingValue wechat) public class WechatService implements PaymentService { // ... } // 订单服务不管底层是谁只管注入接口 Service public class OrderService { Autowired private PaymentService paymentService; }这段代码最迷人的地方在于你只需要编译打包一次。打出来的payment-app.jar就像一个盲盒当运维在启动脚本里写入java -jar payment-app.jar --payment.channelalipaySpring 会在启动的瞬间读取环境配置只实例化AlipayService并注入到业务中。如果第二天客户说想换成微信支付开发人员一行代码都不用改甚至不需要重新打包。运维只需要去 K8s 的 ConfigMap 里把参数改成wechat重启一下进程。Spring 重新扫描时就会把注入的对象动态替换成WechatService。除了这种按需装配的灵活性Spring 还利用运行时反射做到了动态代理AOP的无感实现。当你写下Transactional时Spring 会在内存中利用 CGLIB 动态生成一个代理类。如果在编译期做这件事就需要引入侵入性极强的字节码增强插件而运行时处理则让这一切对开发者完全透明。魔法的代价与 Quarkus 的“降维打击”Spring 这种“运行时反射 动态分析”的代价就是启动慢、内存消耗巨大。这在以前单体应用长连接跑几个月的时代不是问题但在要求极速弹性扩缩容的微服务和 Serverless 时代成了致命伤。Quarkus 敏锐地抓住了这个痛点给出的解法是构建时决断Build-Time Resolution。Quarkus 把 Spring 在启动时干的脏活累活全部前置到了编译打包阶段。当你敲下mvn package时Quarkus 就会把依赖关系分析透彻生成硬编码的注入类用 Gizmo 等字节码工具直接把 AOP 的代理.class文件在编译期生成好为了极致瘦身它甚至会把没用到的类比如上面例子中未被选中的那个支付实现直接在打包时剔除。但这种极致的优化必然伴随妥协在 Quarkus 中严格区分了“构建时配置”和“运行时配置”。如果你想彻底更换一个底层的实现比如切换数据库驱动或者像上面那样切换核心 Bean你无法再像 Spring 那样只改个运行时参数你必须修改构建配置并重新编译打包。这听起来似乎是历史的倒退其实不然。K8s、CI/CD 与“不可变基础设施”为什么 Quarkus 现在敢于牺牲掉 Spring 引以为傲的“免编译动态换组件”的灵活性因为时代变了底层的运维基础设施变了。在云原生时代Kubernetes 和 CI/CD 彻底改变了软件的交付方式。现代架构推崇的核心理念叫不可变基础设施Immutable Infrastructure。在过去部署一次太痛苦所以大家祈求“最好别重新打包改改配置重启就好”。但今天如果有任何核心组件的变更最标准的动作是提交 PR - 触发 CI/CD 流水线 - 跑通自动化测试 - 重新打出一个全新的 Docker 镜像 - 通过 ArgoCD 等工具进行无缝滚动发布。如今没有任何一个理智的团队会允许运维直接去生产环境的容器里偷偷替换一个驱动 Jar 包。“配置漂移”是 K8s 时代的运维大忌打出来的镜像是什么样它到任何环境里就必须是什么样。既然有了强大且自动化的 CI/CD 兜底重新编译和打包已经变得极速且低风险。Quarkus 正宣扬看透了这一点既然大家最终都要重新打 Docker 镜像那我干脆把依赖注入、AOP 统统提前到编译打包阶段用牺牲掉的那一点“古老的灵活性”换来 Java 应用在 K8s 中几十毫秒的瞬间拉起和极低的内存占用这笔买卖在云原生时代稳赚不赔。结语技术的发展就像一个螺旋上升的圈。在物理机时代Spring 用运行时的性能损耗换取了工程上的灵活性在容器化时代Quarkus 又用编译期的固化换取了运行时的极致性能。就连 Spring 自己也在 Spring Boot 3.0 中全面拥抱了 Spring AOT试图把当年的魔法重新搬回编译期。脱离了基础设施谈框架设计都是耍流氓。从 Spring 到 Quarkus 的理念变迁本质上是 Java 兵器谱为了适应 Kubernetes 和 CI/CD 这片新战场而完成的一次漂亮的自我进化。