1999 年,Java 正带着"一次编写,到处运行"的梦想席卷企业级开发领域。彼时的分布式应用、事务管理、安全控制等需求如同一座座大山,压得开发者喘不过气。就在这时,EJB(Enterprise JavaBeans)作为 Sun 公司力推的企业级开发规范横空出世,试图用标准化的方式终结混乱。然而,谁也没想到,它为了实现"容器托管"而设计的强制继承机制,会成为后来无数开发者的噩梦,也为 Spring 的崛起埋下了伏笔。
一、无奈的选择:为什么必须继承 EJBObject/EJBHome?
在今天看来,强制业务代码继承框架接口似乎是一种"反模式",但在 1999 年的 Java 技术栈里,这几乎是唯一可行的方案。
当时的 Java 还没有注解(Annotation 直到 JDK 5.0 才出现),动态代理(Java Dynamic Proxy)虽在 JDK 1.3 中初露端倪,但远未普及,早期 EJB 主要依赖 RMI(远程方法调用)的静态代理机制。容器要想管理 Bean 的生命周期、拦截远程调用、注入事务上下文,就必须和业务代码建立一套"强制契约"------而 EJBObject 和 EJBHome 就是这套契约的载体。
-
EJBObject定义了容器需要的生命周期钩子:getEJBHome()(获取工厂)、remove()(销毁对象)、getHandle()(远程句柄)等; -
EJBHome则负责对象的创建与销毁:create()、remove()等方法是容器实例化 Bean 的唯一入口。
在那个没有反射、AOP 还只是学术概念的年代,强制继承就像一把"生硬的钥匙",只有通过它,容器才能"打开"业务对象的大门,完成托管。
二、历史的阵痛:强制继承带来的五大真实大坑
强制继承的设计虽然解决了"容器托管"的燃眉之急,却把业务代码和 EJB 规范牢牢焊死在了一起。每一个影响,都是当年 Java 开发者踩过的血泪坑。
1. 业务代码被"绑架":离开容器就成废代码
这是最致命的问题。假设你写了一个 UserServiceRemote 接口:
java
public interface UserServiceRemote extends EJBObject {
boolean login(String username, String password) throws RemoteException;
}
一旦业务需求变化------比如想把登录逻辑复用到普通 Java SE 桌面工具,或者公司决定从 EJB 迁移到 Spring------你会发现:
-
普通 Java SE 项目里根本没有
javax.ejb.EJBObject这个类,接口直接报错; -
迁移到 Spring 时,所有接口必须重写成纯 POJO(Plain Old Java Object),实现类里的
SessionContext、生命周期方法也得全部删掉,工作量相当于代码重写一遍。
当年很多公司从 EJB 迁移到 Spring,光接口和实现类的重构就花了几个月------业务逻辑没变,却因为规范绑定,不得不推倒重来。
2. 单元测试的噩梦:测一个方法要等 10 分钟
纯 POJO 的测试很简单:直接 new 对象,调用方法,几毫秒就能跑完。但 EJB 2.x 的 Bean 不行------它必须由容器创建,依赖容器注入的 SessionContext,自己 new 出来的对象根本跑不起来。
当年测试一个 login 方法,流程是这样的:
-
启动 JBoss/WebLogic 容器(慢的话要等 5 分钟);
-
把 EJB 打包成
ejb-jar,部署到容器; -
写客户端代码,通过 JNDI 查找
Home接口,创建Remote接口; -
调用
login方法验证结果; -
关闭容器,清理部署。
一次测试 5-10 分钟,容器启动失败、JNDI 查找失败、端口冲突......这些和业务无关的问题都会导致测试失败。后果就是:当年很多 EJB 项目的单元测试覆盖率不到 10%------不是开发者不想写,是写测试太麻烦了。
3. 代码冗余:空方法堆成山,新人接手就晕
强制继承不仅要求接口继承 EJBObject,实现类还必须实现 SessionBean 接口,里面有 5 个生命周期方法:
java
public class UserServiceBean implements SessionBean {
private SessionContext ctx;
// 不管用不用,必须实现
public void setSessionContext(SessionContext ctx) { this.ctx = ctx; }
public void ejbCreate() {} // 空实现也得写
public void ejbRemove() {} // 空实现也得写
public void ejbActivate() {} // 无状态Bean根本不会调用,也得写
public void ejbPassivate() {} // 无状态Bean根本不会调用,也得写
// 真正的业务逻辑只有这一个
public boolean login(String username, String password) {
return "admin".equals(username) && "123456".equals(password);
}
}
代码里一堆空方法,新人接手时根本分不清哪些是业务方法,哪些是规范要求的"摆设"。维护时不敢删,怕容器跑不起来,只能留着,代码越来越臃肿。
4. 学习门槛拉满:新手学 EJB 学到放弃
纯 POJO 开发,懂 Java 基础就行。但 EJB 2.x 开发,你必须先学一堆和业务无关的规范:
-
记住
EJBObject、EJBHome、SessionBean里的所有方法,知道它们什么时候被调用; -
理解
RemoteException为什么必须抛,不抛会怎么样; -
学会写
ejb-jar.xml配置文件,搞懂每个标签的含义; -
掌握 JNDI 查找,会配置
InitialContext。
这些东西和"登录逻辑""转账逻辑"没有任何关系,但你必须全部掌握,否则代码根本跑不起来。当年很多 Java 新手,学 EJB 学到放弃------本来想写个简单的业务功能,结果被一堆规范接口、配置文件搞晕了。
5. 灵活性被扼杀:Java 单继承的枷锁被锁死
Java 是单继承的,业务接口必须继承 EJBObject,就意味着你不能再继承其他自定义父接口了。
比如你有一个通用的 BaseService 接口,里面定义了所有业务服务都需要的 log() 方法:
public interface BaseService { void log(String message); }
你想让 UserServiceRemote 继承 BaseService 复用 log() 方法------但不行,因为它必须继承 EJBObject,Java 不支持多继承。你只能把 log() 方法复制到每个业务接口里,或者用更复杂的组合设计,代码灵活性被大大限制。
三、变革的曙光:Spring 如何用纯 POJO 突围
就在 EJB 2.x 把开发者折磨得苦不堪言时,2003 年,一个叫 Rod Johnson 的人带着《Expert One-on-One J2EE Design and Development》和 Spring 框架横空出世,用"纯 POJO + IoC + AOP"彻底解决了"侵入性"问题。
同样的登录逻辑,在 Spring 里是这样的:
java
public interface BaseService {
void log(String message);
}
-
可复用:接口和实现类可以直接拿到任何 Java 项目里用,不需要 Spring 容器;
-
易测试 :直接
new UserServiceImpl()就能测试,几毫秒跑完; -
低门槛:新手只需要懂 Java 基础,不用学框架接口;
-
高灵活:接口可以自由继承自定义父接口,不受单继承限制。
Spring 用 IoC(控制反转)通过反射和依赖注入管理对象生命周期,用 AOP(面向切面编程)动态织入事务、安全等能力------不再需要强制契约接口,业务代码终于从容器的枷锁中解放了出来。
四、史鉴:技术选择的时代局限与问题驱动创新
回望 EJB 2.x 的历史,我们不能简单地把它批判为"糟糕的设计"------它是 1999 年 Java 技术栈限制下的无奈但最优的选择。它试图用标准化解决企业级开发的混乱,却因为技术局限付出了"侵入性"的代价。
而 Spring 的崛起,恰恰是技术史"问题驱动创新"的最佳例证:它没有否定 EJB 的企业级需求,而是用更先进的技术(反射、动态代理、AOP)解决了 EJB 的痛点,用"非侵入性"重新定义了 Java 企业级开发。
这段历史告诉我们:技术选择永远受时代局限,没有绝对的"对错",只有"是否适合当时的场景"。而那些曾经让开发者痛苦的问题,恰恰是下一次技术变革的起点。
后来,EJB 3.0 也吸取了 Spring 的经验,引入注解、支持纯 POJO,完成了自我救赎------但 Spring 早已凭借"轻量级、非侵入性"的理念,成为了 Java 企业级开发的事实标准。这段 EJB 与 Spring 的历史纠葛,也成了 Java 技术史上最经典的"问题-创新"案例之一。