上次提到 Cordis时,我觉得它现在的设计很像 Spring。所不同的是 Spring 管理的是 Java 对象,Cordis 控制的是 plugin ,都有一个 Runtime 容器的概念,而且都可以按需集成相关模块。
写完那篇文章后,我突然好奇起来。
"为什么其他的语言并没有 Spring 类似的容器来管理对象的声明周期?"
作为一个常年使用 Java 的开发,我真心觉得这是一个伟大的设计。Ioc、DI、AOP,这三项基础功能太强大了,几乎是 Java 开发的必备技能。
**可为什么其他语言都是由开发者自己控制对象的生命周期?似乎并没有类似对象容器的这种概念。**用这种统一的容器框架之后,后面的很多其他功能只需要围绕容器的生命周期来集成并提供集成包。那使用者在集成时就都能做到开箱即用,即使对这个集成的功能知之甚少。
最明显的例子就是 spring 的各种整合包。只要依赖一下相关的包,再加上相关的启动配置,基本就能用了(比如: spring-jdbc、spring-logging)。而其他语言,似乎都需要开发者自己写代码来控制应用与要依赖的模块之间的集成。
灵活性
这是我第一时间能想到的解释。容器技术虽然好,但是它太庞大,不够灵活。有时候你可能只想写一段验证代码,就像 Test 一样。如果你集成容器之后,你会发现,整个启动会变得特别慢。
Python 和 Node.js 很多时候会有这样的场景。比如:写一段算法逻辑来验证、写一个ETL脚本等等。可是 Go 似乎很少这样的场景,但它怎么没有用容器呢?
历史包袱
既然答案不在现在,那我觉得肯定是因为历史原因才让这个容器技术只存在于 Java 中。
我们来深入"案发现场",看看Spring诞生前,基于EJB的J2EE开发到底有多"地狱"。我把当时的痛点拆解成五个具体的"灾难现场":
1. 必须继承"重量级"父类,代码极度"肮脏"
你的业务类必须实现`SessionBean`或`EntityBean`接口,并重写`ejbCreate()`、`ejbPassivate()`、`ejbRemove()`等一大堆生命周期方法。哪怕你只想写个"Hello World",也得带上几十行空实现。**业务逻辑被强行绑死在EJB容器API上**,想单独跑个单元测试?门都没有。
2. 部署描述符是"XML噩梦"
那时没有注解,一个简单的无状态Bean需要在`ejb-jar.xml`里写大段配置,包括JNDI名称、事务属性、安全角色等。更崩溃的是,还要为每个字段写`<cmp-field>`映射。改一个字段名,要改Java代码、改XML、改SQL,漏一个就启动失败。
3. "开发-部署-测试"循环慢到怀疑人生
修改一行代码后,不能直接重启Web容器,而是需要**完整打包、重新生成桩代码、部署到WebLogic/WebSphere、等待服务器启动**(通常要几分钟甚至十几分钟)。程序员大量时间花在喝咖啡等服务器重启上,一天能有效迭代的次数屈指可数。
4. 单元测试几乎不可能(必须依赖容器)
EJB必须运行在容器里,你想测试业务逻辑,**必须把整个应用部署到真实服务器上**,通过客户端远程调用。调试时无法设断点单步跟踪,只能靠打印日志、部署、看日志、再改、再部署......效率极低。
5. 性能与设计上的"过度设计"
远程调用本地化:即使Bean和应用在同一台机器、同一个JVM,方法调用也必须走**RMI-IIOP**(网络协议),要经过序列化、网络传输、反序列化,存在巨大的性能开销。
CMP(容器管理持久化)极其智障:EJB的实体Bean映射机制非常原始,经常生成低效SQL(比如全表锁、N+1次查询),且无法精细控制缓存。
总结一句:那时的开发不是"写代码",而是"伺候容器"。Rod Johnson在书里直接用大量代码证明,**80%的业务代码根本不需要这些重型基础设施**。正是为了终结这场"寒冬",Spring才带着IoC(摆脱依赖查找)、AOP(解耦横切逻辑)和轻量级模板(直接写JDBC)横空出世,让程序员重新找回了"写普通Java类"的快乐。
简单来讲,就是当时 EJB 太臃肿,Spring 是在收拾EJB的烂摊子。
我心里又产生了一个疑问,"为什么就一定要使用这类的容器技术呢?其他语言没使用,不也一样可以进行开发吗?"
分两层回答你:**为什么要设计它(初衷)**,以及**当时到底能不能不用它(现实)**。
1. 为什么要设计EJB?(初衷很美好)
EJB的初衷是"标准化中间件"。它想把复杂的底层技术(事务管理、对象池、远程调用)打包成"容器",由服务器自动接管。开发者只需把业务代码塞进规定好的接口里,容器就会在运行时"魔法般"地注入这些能力。**说白了,EJB想当Java界的"水电煤"基础设施**,让程序员只关心业务。
2. 那当时不用不行吗?(现实极其残酷)
技术上:完全可以用纯JDBC + Servlet写,但极其"自虐"。
你要手动管理数据库连接、手写`try-catch-finally`处理事务(提交/回滚)、手动处理多线程安全问题。
如果要分布式,你得自己搭RMI(远程方法调用),处理各种网络异常。
这些代码重复、脆弱,且极度依赖程序员的个人水平,大型项目几乎难以维护。
行业生态上:几乎"不得不"用。
当时**EJB就是"J2EE企业级开发"的代名词**。Sun公司力推,所有大厂(IBM、Oracle)的应用服务器(WebSphere、WebLogic)只认这个标准。
甲方招标书会明确要求"基于J2EE标准(即EJB)",拿不到这个标签,项目连入围资格都没有。
市场上没有像样的替代方案,Spring还没出生,Hibernate也才刚萌芽,开源社区力量远不如现在。
转折点来了:Rod Johnson(Spring之父)在2002年的书里,直接用**3万行纯JDBC + 简单封装**的代码,成功搭建了一个完整的交易系统,性能远超当时的EJB。他硬生生证明了:**"EJB带来的复杂度,远远大于它解决的问题,而且90%的项目根本不需要分布式!"**
换句话说,当时分布式刚起步,行业整体的技术水平还不够,需要 EJB 来封装复杂的分布式调用、数据库的事务操作。后来 EJB 太臃肿,Spring 解决了EJB 的臃肿问题。
这里就很有意思了,这些答案似乎在告诉我,在 Java 中只有容器技术才能做好统一的封装(AOP)。仔细一想,好像还真是!
可是为什么其他语言不需要呢?
这里的"静态语言",在计算机科学里通常指 "静态类型语言",即**变量的类型必须在编译阶段就确定下来**,不能运行时改变。
为了让你彻底理解,我把它和"动态语言"对比,并结合Spring的痛点来解释:
1. 类型检查的时机(核心区别)
静态语言(Java、Rust、Go):写代码时必须明确类型(如 `String name = "123"`)。编译器会在运行前严格检查,如果试图把 `name` 赋值为数字 `123`,**代码根本编译不过去**。好处是极其严谨,坏处是**缺乏灵活性**。
动态语言(Python、JS):变量只是个标签,不绑定类型。你可以写 `name = "123"`,下一秒改 `name = 456`,完全合法。这种灵活性让代码写起来很快,但也容易在运行时因类型错误而崩溃。
2. 这对Spring容器意味着什么?
正是因为Java的"静态"特性,才逼出了Spring的大容器:
Python/JS搞注入很简单:因为它们是动态的,我可以在运行时随意给对象加属性、改方法。想要事务?直接用装饰器把原函数"换掉"就行,不需要提前声明复杂的接口。
Java搞注入很吃力:Java太死板了,你不能随便改一个已编译好的类的行为。所以Spring不得不动用复杂的**反射(Reflection)**和**动态代理(Proxy)**,在内存里"捏造"一个新的代理类来替你执行事务。这个过程消耗性能,且报错极其晦涩。
搞了半天,原来 Spring 是替 EJB 收拾了烂摊子,然后成为了行业事实标准。**它们都是为了能在抽象层面统一地去解决复杂的分布式通讯、数据库操作,而 Java 的静态语言特性不支持,就只能搞容器。**通过运行时的反射功能,来给业务代码附加这些业务之外的功能。所以,在使用 Java 开发的时候,大家下意识的就会用。
其他的静态语言,在设计之初都避开了容器的设计~~