☰
EJB第二版源码实战:从Ant编译到JNDI部署的避坑指南
2026/10/14 18:12:48 网站建设 项目流程

简介:本资源为《精通EJB源代码 第二版》配套源码包,面向具备Java与面向对象基础、希望深入掌握企业级JavaBeans开发的中高级开发者。内容围绕EJB 3.x展开,涵盖实体Bean、会话Bean与消息驱动Bean的组件模型,容器提供的生命周期、事务与安全管理,JPA持久化映射与JPA QL查询,RMI远程调用、并发事务控制以及Web服务集成等核心主题,并配有真实案例的源代码分析与测试调试思路。压缩包为rar格式,共279个文件,约594KB,其中79个java源文件构成主体实现,33个xml与1个properties承载配置,26个bat脚本用于编译与启动,另有jsp、jar、class、policy、sql、ear等文件支撑部署与运行。已有132人学习下载,适合对照书中章节逐模块研读源码,理解EJB设计原则与实现技巧,将理论转化为实际项目能力。

1. 翻出这份 EJB 第二版源码包时,我为什么先跑了一遍构建脚本

前阵子帮一个做金融系统维护的朋友排查老项目的部署问题,对方丢过来一个压缩包,文件名写着“精通EJB源代码 第二版”。打开一看,目录结构是典型的 Eclipse 老工程布局,src下按包名分层,ejbModule里塞着会话 Bean 和实体 Bean,build.xml用的是 Ant 而不是 Maven。这套东西放在今天看确实有点年代感,但如果你手上正好有基于 EJB 2.x 或 3.x 的老系统要维护、要迁移、要补测试,这份源码的价值就出来了——它不是那种只给你看个 Hello World 的示例,而是把会话 Bean、实体 Bean、消息驱动 Bean、事务注解、JNDI 查找这些核心机制都铺开写了。

适合谁看?一是接手了遗留 Java EE 项目、被@Stateless和@EJB绕晕的维护岗;二是想搞明白 EJB 容器到底在背后替你做了什么、不想只停留在“会用注解”层面的后端开发;三是做技术考古、需要一份能跑起来的参考实现来对照规范文档的人。这份源码包最大的好处是“能落地”——你能把它导进 IDE、能编译、能部署到兼容的容器里看日志,而不是对着一堆理论干瞪眼。

2. 把源码包跑起来:环境选型与工程导入的取舍

2.1 为什么我不建议直接上最新 JDK

EJB 第二版源码的编译目标大概率是 Java 5 或 Java 6 级别,里面可能用到javax.ejb包下的老 API,也可能有sun.misc之类的内部调用。如果你直接拿 JDK 17 去编译,第一个撞上的就是javax命名空间被jakarta取代的问题——EJB 3.1 之后规范迁移到了jakarta.ejb,老源码里的 import 全部会报红。

常见做法是准备一个 JDK 8 的运行环境,配合一个支持 Java EE 7 或更早规范的容器。我一般会选 GlassFish 4.x 或者 JBoss AS 7,这两个对 EJB 2.x/3.x 的兼容性比较稳,部署时不会因为规范版本对不上直接拒绝加载。如果你只是想看代码逻辑、不打算真部署,那用 JDK 8 加一个纯编译环境就够了,至少能把.class文件生成出来验证语法。

提示:先确认源码根目录下有没有pom.xml。如果没有,只有build.xml,说明这是 Ant 工程,别硬套 Maven 的目录约定。

2.2 导入工程时先做一次“依赖体检”

老工程的lib目录里通常躺着一堆 jar,文件名可能是jboss-ejb-api.jar、jta.jar、jndi.jar这种。导入 IDE 后第一件事不是急着编译,而是把这些 jar 逐个加到 Build Path 里,然后看Problems视图还剩多少红叉。

# 先看工程根目录结构,确认构建脚本和依赖目录 ls -la # 典型输出里应该能看到: # build.xml src/ ejbModule/ lib/ META-INF/

上面这条命令的作用是快速判断工程类型。如果build.xml存在且lib目录里有 jar,那基本可以确定是 Ant 驱动的老式 Java EE 工程。参数上没什么好调的,重点看lib里有没有缺失的 API 包——有时候源码包为了减小体积会把容器提供的 jar 剔掉,你需要从目标容器的安装目录里把对应的client包补进来。

2.3 用 Ant 编译并观察第一个报错

<!-- build.xml 里典型的编译目标,重点看 srcdir 和 classpath --> <target name="compile" depends="init"> <javac srcdir="${src.dir}" destdir="${build.dir}" includeantruntime="false" debug="true"> <classpath> <fileset dir="${lib.dir}" includes="**/*.jar"/> </classpath> </javac> </target>

这段 Ant 脚本的逻辑很直白:把src.dir下的 Java 文件编译到build.dir,classpath 来自lib.dir下的所有 jar。关键参数是includeantruntime="false",不加这个的话 Ant 会把自身的运行时塞进 classpath,容易和容器 API 冲突。执行ant compile后,如果报package javax.ejb does not exist,说明lib里缺 EJB API 包;如果报cannot find symbol: class EntityManager,那是 JPA 的包没引全。按报错逐个补 jar 就行,别一次性把容器所有 jar 都倒进去,那样容易引发版本冲突。

3. 会话 Bean 与实体 Bean 的代码拆解:从注解到 JNDI 查找

3.1 无状态会话 Bean 的典型写法与调用链

源码里最常见的入口是@Stateless标注的会话 Bean。它实现一个本地接口或远程接口,容器负责池化实例、管理事务边界。下面这段是我从源码包里摘出来并做了简化的结构:

// 本地接口,定义业务方法 public interface OrderServiceLocal { String createOrder(String userId, double amount); } // 无状态会话 Bean 实现 @Stateless public class OrderServiceBean implements OrderServiceLocal { @PersistenceContext(unitName = "orderPU") private EntityManager em; // 容器注入的实体管理器 @Override public String createOrder(String userId, double amount) { Order order = new Order(); order.setUserId(userId); order.setAmount(amount); em.persist(order); // 持久化到数据库 return "ORDER-" + order.getId(); } }

逻辑说明:@Stateless告诉容器这个 Bean 不需要维护会话状态,可以放进实例池复用。@PersistenceContext是 JPA 的注入点,unitName对应persistence.xml里的持久化单元名。em.persist()把实体对象交给 EntityManager 管理,事务提交时自动落库。参数上要注意unitName必须和配置文件里写的一致,否则部署时直接报PersistenceUnit not found。

调用方通常通过 JNDI 查找或者@EJB注入来拿到这个 Bean。源码里两种方式都有示例,JNDI 那种更适合跨模块调用:

// JNDI 查找方式,适合不在容器管理范围内的客户端 Context ctx = new InitialContext(); OrderServiceLocal service = (OrderServiceLocal) ctx.lookup( "java:comp/env/ejb/OrderServiceLocal"); String result = service.createOrder("U1001", 299.0);

这里的java:comp/env/ejb/OrderServiceLocal是部署描述符里配置的引用名,不是随便写的字符串。如果你在ejb-jar.xml里改了<ejb-ref-name>,这里也要同步改,否则NameNotFoundException就是你的下场。

3.2 实体 Bean 的两种流派:BMP 与 CMP 的取舍

第二版源码里实体 Bean 部分同时覆盖了 Bean-Managed Persistence(BMP)和 Container-Managed Persistence(CMP)。BMP 需要你自己在ejbCreate、ejbLoad、ejbStore里写 JDBC 代码,控制权大但代码量大;CMP 则由容器生成 SQL,你只需要在部署描述符里声明字段和关系。

对比项BMPCMP
持久化逻辑开发者手写 JDBC容器自动生成
灵活性高,可优化 SQL低,受容器实现限制
代码量大,每个方法都要填小,只声明抽象方法
适用场景复杂查询、遗留数据库标准 CRUD、快速开发

我一般建议先看 CMP 部分,因为它的抽象层次更高,能帮你理解 EJB 容器到底替你做了哪些事。BMP 部分则适合对照着看,搞明白“如果不靠容器,自己该怎么写”。源码里 CMP 的实体 Bean 通常是抽象类,字段的 getter/setter 由容器在部署时生成子类实现,这个机制在调试时容易让人懵——你打断点发现调用的是一个不存在的类,别慌,那是容器动态生成的。

3.3 消息驱动 Bean 与事务注解的配合

消息驱动 Bean(MDB)是 EJB 里比较有特色的东西,它监听 JMS 队列或主题,收到消息后触发onMessage方法。源码里有一个订单异步处理的例子:

@MessageDriven(activationConfig = { @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"), @ActivationConfigProperty(propertyName = "destination", propertyValue = "java:/jms/queue/OrderQueue") }) public class OrderMessageBean implements MessageListener { @Resource private MessageDrivenContext context; @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public void onMessage(Message message) { try { // 解析消息并调用会话 Bean 处理业务 String userId = message.getStringProperty("userId"); // ... 业务逻辑 } catch (JMSException e) { context.setRollbackOnly(); // 异常时回滚事务 } } }

activationConfig里的destination必须和 JMS 提供者里配置的队列名完全一致,大小写敏感。@TransactionAttribute(REQUIRED)表示如果调用方已经有事务就加入,没有就新建一个。context.setRollbackOnly()是手动标记回滚的后悔药——消息处理失败时不让事务提交,消息会重新投递。这里有个坑:如果异常没被捕获,容器默认也会回滚,但消息可能被反复重投直到达到最大重试次数,日志会被刷爆。

4. 部署与调试:把源码包扔进容器后要看什么

4.1 打包结构与部署描述符的最小集

EJB 工程的打包产物通常是 jar,结构如下:

order-ejb.jar ├── META-INF/ │ ├── ejb-jar.xml # 部署描述符,声明 Bean 和引用 │ ├── persistence.xml # JPA 持久化单元配置 │ └── MANIFEST.MF ├── com/example/ejb/ │ ├── OrderServiceBean.class │ └── OrderServiceLocal.class └── ...

ejb-jar.xml在 EJB 3.x 里是可选的,注解已经能表达大部分信息。但源码包里保留了完整的描述符,这对理解“注解和 XML 的优先级”很有帮助——当两者冲突时,XML 通常覆盖注解。persistence.xml里要配好<jta-data-source>,指向容器里配置的 JDBC 数据源,名字对不上就是部署时直接失败。

4.2 部署后先查 JNDI 树,别急着调业务

# 以 GlassFish 为例,用 asadmin 查看 JNDI 绑定 asadmin list-jndi-entries --context java:comp/env # 输出里应该能看到你的 EJB 引用名和队列名

这条命令的作用是确认容器到底把你的 Bean 绑到了哪个 JNDI 名字上。不同容器、不同版本的默认绑定策略不一样,有的会加__EJB31_Generated__前缀,有的按接口全限定名绑定。先看清楚再写查找代码,能省掉大量NameNotFoundException的排查时间。参数--context指定要查看的 JNDI 上下文,不指定的话默认看根上下文,东西太多反而不好找。

4.3 日志里要盯住的几个关键词

部署 EJB 时容器日志会刷很多行,重点搜这几个:Portable JNDI names后面跟的是你的 Bean 绑定名;EJB Container初始化信息能看出容器识别到了几个会话 Bean、几个 MDB;如果有WARNING: Cannot resolve reference之类的,说明某个@EJB注入点没找到目标,通常是接口名写错或者目标 Bean 没被打进同一个包。把这些日志行截出来对照源码里的注解,基本能定位到问题。

5. 避坑与排查:这份源码包最容易翻车的五个地方

5.1 现象:编译通过但部署报ClassNotFoundException

原因:源码包的lib里放了容器已经提供的 API jar,打包时又把这些 jar 打进了 EJB jar 的lib目录,容器加载时产生冲突。解决:把javax.ejb、javax.persistence这类容器提供的包从打包路径里排除,只保留第三方依赖。Ant 里可以用<zipfileset>的excludes属性控制。

5.2 现象:实体 Bean 的@Id字段生成策略报错

原因:源码里可能用了@GeneratedValue(strategy = GenerationType.AUTO),但目标数据库不支持自增或者序列配置缺失。解决:换成GenerationType.IDENTITY或SEQUENCE,并在persistence.xml里补上对应的<properties>。别小看这个,AUTO 在不同容器加不同数据库的组合下行为差异很大,属于玄学范畴。

5.3 现象:MDB 收到消息后业务方法抛异常,消息无限重投

原因:onMessage里异常没被正确处理,容器默认认为消息处理失败并重新投递。解决:在 catch 块里判断异常类型,不可恢复的错误用context.setRollbackOnly()之外,还要考虑配死信队列。源码里如果没写死信配置,你需要自己在容器层面加。

5.4 现象:JNDI 查找在单元测试里能用,部署后报NameNotFoundException

原因:单元测试时可能用的是本地上下文或者 mock,和容器运行时的 JNDI 树不是一回事。解决:以容器启动日志里打印的Portable JNDI names为准,把查找字符串改成实际绑定名。血泪经验是别信源码注释里写的 JNDI 名,以运行时为准。

5.5 现象:事务注解不生效,数据没回滚

原因:@TransactionAttribute加在了私有方法或者非业务接口方法上,容器代理拦截不到。解决:确保注解加在 public 的业务方法上,且调用方是通过接口引用调用而不是new出来的实例。EJB 的事务管理依赖容器生成的代理,绕过代理就等于绕过了事务。

6. 进阶技巧:用源码包里的模式改造自己的老项目

这份源码包最大的价值不是让你照抄,而是给你一套可对照的“标准答案”。我后来在维护一个老系统时,就借用了里面 CMP 实体 Bean 的关系映射写法,把一堆手写 JDBC 的 DAO 逐步替换成了 JPA 注解。具体做法是:先挑一个最简单的实体,照着源码里的@OneToMany、@ManyToOne把关系配好,用persistence.xml里的hibernate.show_sql打开 SQL 日志,对比生成的 SQL 和原来手写的 SQL 是否一致。一致就继续,不一致就调注解参数。

验证方法上,我习惯写一个最小的集成测试:用 Arquillian 或者容器自带的测试框架,把会话 Bean 注入进来,跑一遍增删改查,看事务是否按预期提交和回滚。源码包里如果有测试类,直接拿来改改就能用。另一个技巧是关注ejb-jar.xml里的<assembly-descriptor>部分,那里定义了方法级的事务属性和安全角色,很多老系统的权限控制就藏在这里,比注解更隐蔽。

从那以后我每次拿到一份老 EJB 源码,都强制自己先跑一遍ant compile,再部署到容器里看 JNDI 绑定,最后才去读业务代码。这个顺序能帮你把环境问题和代码问题分开,省掉大量来回折腾的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询