简介这是一份专为Java初学者设计的EJB入门项目资源基于IDEA集成开发环境与JBoss 7.1.1应用服务器完整演示了会话Bean无状态/有状态、实体Bean与消息驱动Bean三类核心组件的定义、实现与部署。项目中包含Maven工程结构、JNDI绑定、事务控制、JMS消息处理等关键配置可帮助读者跨越理论到实践的鸿沟快速掌握企业级开发基础。资源以RAR压缩包发布共46个文件大小2.84MB。其中含5个Java源文件、12个XML配置、4个JSP页面、4个properties资源配置以及编译后的15个class文件和2个依赖jar完整呈现了从源码到可运行模块的各个环节。目前已有777人学习下载。通过运行本工程读者能直观理解EJB组件的生命周期管理、本地/远程接口调用方式以及如何将业务逻辑打包并部署到JBoss服务器。同时项目中的JSP页面与目录结构示例也为后续整合Web层与业务层提供了参考。1. EJB入门项目到底在学什么一个被误解多年的Java企业级组件模型看到“EJB入门项目”这个标题很多人第一反应是这东西不是早就被 Spring Boot 取代了吗这个判断对了一半。EJB 这个缩写确实背了十多年“笨重”的锅但 Java EE 8 之后你连一行 ejb-jar.xml 都不用写只要在业务类上标一个Stateless容器就帮你接好了事务、并发、安全、远程调用这四件事——它们恰恰是企业级应用里最容易写错的部分。以某公司结算系统为例一次入账要同步改账户余额、写流水、加积分如果事务边界靠手写 commit/rollback漏一个点就要半夜爬起来修数据。EJB入门项目的核心不是“怎么让一个 Bean 跑起来”而是让你理解容器托管的边界在哪、JNDI 名字怎么推导、事务什么时候提交。适合两类人刚学完 Java SE 想搞懂服务端组件的初学者以及被旧框架劝退后回来看“现代 EJB 到底长什么样”的开发者。2. 跑通第一个EJB无状态会话Bean的完整落地链路2.1 为什么入门从无状态会话Bean开始接口选择与EJB Lite边界会话 Bean 分三种无状态Stateless、有状态Stateful、单例Singleton。入门项目我几乎必定选无状态原因很直接无状态 Bean 不绑定调用方容器可以用实例池复用对象不涉及钝化passivation、不涉及实例亲和性出错面最小。事务边界、依赖注入、异步调用这些核心概念全部能在无状态 Bean 上演示等你把这些搞明白了再去看有状态 Bean 的状态补偿和Remove压力会小很多。接口选择是新手最先纠结的地方。EJB 3.x 之前写一个 Bean 要同时写远程接口、Home 接口、实现类光接口就十几个文件。现在容器支持三类视图无接口视图、Local本地接口、Remote远程接口。无接口视图最省事——连接口都不用定义Bean 类本身就是业务接口。但我的建议是入门也要写一个Local接口原因后面讲 Mock 和容器替换时会提到接口只要定义在接口上,后续做单元测试或换实现时不会被全局 JNDI 命名耦合卡死。这里还要提一下 EJB Lite。Java EE 的 Web Profile 容器里只要求实现 EJB Lite 子集它涵盖无状态、有状态、单例 Bean、本地接口、无接口视图、拦截器和容器管理事务但不强制要求远程接口和消息驱动 BeanMDB。换句话说你学入门项目根本不需要碰 RMI-IIOP用 GlassFish 或 WildFly 的默认配置就够了。三种视图的对比如下视图类型需要写的文件调用方式适用场景无接口视图只写 Bean 类本地调用直接ctx.lookup返回 Bean 类入门、模块内部调用Local接口接口 实现类本地调用接口暴露给同进程组件需要定义业务契约、便于测试Remote接口接口 实现类 序列化注意RMI/IIOP 跨 JVM 调用分布式部署、遗留系统集成2.2 用Maven搭建EJB模块从pom到部署的最小命令先看项目结构。一个最简单的 EJB 模块只需要一个 Maven 模块源码目录保留标准src/main/java打包方式用ejb。如果你用的 IDE 对ejbpackaging 支持不好改成jar也能部署应用服务器并不看扩展名是否是.jar它扫描的是字节码和注解。下面是最小的 pomproject modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdejb-demo/artifactId version1.0.0/version packagingejb/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdjavax.ejb/groupId artifactIdjavax.ejb-api/artifactId version3.2/version scopeprovided/scope /dependency /dependencies /project这段配置的关键点是依赖的scope必须是provided。因为javax.ejb-api只是编译期需要容器GlassFish、WildFly已经自带了完整实现如果你打成compile最后会把 API 类也塞进 jar部署时反而可能出现类冲突。packagingejb是给 Maven 一个提示这个模块不是 Web 应用不需要生成 war 结构。写一个最小的无状态 Beanpackage demo.ejb; import javax.ejb.Stateless; Stateless public class HelloEjb { public String sayHello(String name) { return Hello, name; } }这里Stateless注解的作用是告诉容器把这个类当作无状态会话 Bean 管理。无状态意味着实例不保存调用方数据方法执行完容器就把它放回实例池。部署时WildFly 会自动扫描 jar 里的注解并注册 JNDI 名字不需要任何 XML 配置。部署命令按服务器区分。WildFly 最简单把构建产物复制到部署目录即可mvn clean package cp target/ejb-demo.jar $WILDFLY_HOME/standalone/deployments/GlassFish 则用管理命令mvn clean package asadmin deploy target/ejb-demo.jar部署成功时WildFly 控制台会打印deployed关键字GlassFish 会返回Command deploy executed successfully。如果你的环境是第一次部署建议先看控制台输出里有没有报错再往下写客户端代码。2.3 客户端调用与JNDI解析逻辑java:global命名空间怎么拼Bean 部署进容器客户端怎么拿到它答案是 JNDI。EJB 3.x 之后JNDI 名字可以由容器自动分配但新手最容易在拼名字上翻车。全局命名空间的标准格式是java:global/应用名/模块名/Bean类名假如模块 jar 叫ejb-demo.jarBean 类是demo.ejb.HelloEjb那么在同一个容器内查找代码是这样package demo.client; import javax.naming.Context; import javax.naming.InitialContext; import demo.ejb.HelloEjb; public class Client { public static void main(String[] args) throws Exception { Context ctx new InitialContext(); HelloEjb hello (HelloEjb) ctx.lookup(java:global/ejb-demo/HelloEjb); System.out.println(hello.sayHello(engineer)); } }这里java:global是跨模块共享的命名空间ejb-demo是模块名HelloEjb是 Bean 的默认名字。注意一点不同应用服务器对默认 JNDI 名推导规则有差异有些容器会把应用名也拼进去形成java:global/应用名/模块名/Bean名。最稳妥的办法不是背规则而是看服务器启动日志WildFly 部署时会打印每个 EJB 对应的 JNDI 绑定列表GlassFish 在asadmin list-jndi-entries里也能看到。如果客户端和 Bean 不在同一个 JVM还需要配置jndi.properties这是入门阶段最容易卡住的地方java.naming.factory.initialorg.jboss.naming.remote.InitialContextFactory java.naming.provider.urlremote://localhost:4447但我的建议是入门项目尽量把所有组件放在同一个容器进程里跑用java:global本地查找。等本地链路通了再研究远程调用协议否则你大概率会同时面对 JNDI 拼写错误、序列化异常、端口不通三个问题排查起来非常劝退。3. 让EJB真正可用依赖注入、事务与异步调用的三个必调参数3.1 EJB注入与Stateless生命周期别把EJB当普通new对象用很多初学者在 Servlet 里用new HelloEjb()的方式调用 EJB结果事务不生效、Asynchronous不生效、注入的资源全是空的。原因很简单EJB 的能力全部来自容器代理new出来的对象只是普通 Java 对象没有经过容器处理。正确做法是用EJB注入package demo.ejb; import javax.ejb.EJB; import javax.ejb.Stateless; Stateless public class OrderService { EJB private InventoryService inventoryService; public boolean createOrder(String skuId, int count) { // 这里的 inventoryService 是容器代理事务和注入都有效 return inventoryService.deduct(skuId, count); } }EJB注入发生时容器会在目标 Bean 的实例池里取一个可用实例创建代理对象并把依赖关系在方法调用边界上绑定。无状态 Bean 实例本身是有状态的——它可能被多个线程复用所以你在无状态 Bean 里放实例变量保存调用方数据是入门阶段最隐蔽的错误。正确姿势是所有方法参数和数据都通过方法入参传递实例变量只放无状态工具类或注入的依赖。容器实例池的参数也会影响行为。WildFly 里无状态 Bean 的实例池可以通过stateless-bean-pool配置核心参数有max-pool-size和idle-timeout。默认值一般够用但当你的入账接口并发超过预期时池不够大会表现为方法执行变慢、线程阻塞在获取实例上。排查时不要只看数据库慢查询先确认一下池参数是不是被压到了极限。new与EJB的区别值得单独强调调用方式是否走容器代理事务控制Asynchronous注入资源new HelloEjb()否不生效不生效空EJB HelloEjb是生效生效正常3.2 容器管理事务CMT默认提交边界与TransactionAttribute五个取值EJB 入门项目另一个核心点是容器管理事务CMT。默认情况下每个业务方法都带有REQUIRED事务属性方法入口开启事务方法正常返回时提交运行时异常RuntimeException导致回滚。这个默认值对大多数场景够用但有一条边界必须记住——受检异常不会自动回滚。看一个典型支付场景package demo.ejb; import javax.annotation.Resource; import javax.ejb.SessionContext; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; Stateless public class PaymentService { Resource private SessionContext ctx; TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void createOrder(String orderId) { // 这个方法独立开一个事务不受调用方事务影响 } public void transfer(String from, String to, int amount) throws Exception { try { accountDao.debit(from, amount); accountDao.credit(to, amount); } catch (Exception e) { // 需要标记回滚否则容器看到方法正常返回事务就提交了 ctx.setRollbackOnly(); throw e; } } }代码里的setRollbackOnly()是让当前事务强制回滚的关键。如果你在transfer里直接吞掉异常容器认为方法执行成功两个账户就各扣各的数据永远对不上。这是入门项目里最贵的教训。RuntimeException的情况下容器会自动回滚所以不需要手动调setRollbackOnly但如果你的代码catch住了运行期异常并吞掉容器同样无法感知事务也会提交。TransactionAttribute一共有六个取值入门阶段记住前四个就够了取值事务行为典型场景REQUIRED有事务则加入无事务则新建默认值几乎通用REQUIRES_NEW总是新开事务挂起旧事务记录日志、发消息MANDATORY必须在已有事务中运行否则抛异常内部校验、不允许独立启动的操作NOT_SUPPORTED挂起事务以无事务方式运行发送邮件、生成报表SUPPORTS有事务则加入无则无事务只读查询NEVER有事务直接抛异常不允许事务内执行的长任务参数选择的核心原则是事务边界越短越好跨系统调用不要放进长事务。比如你把发短信塞进createOrder同一个事务里短信网关卡 30 秒数据库连接就被占着 30 秒连接池一满整个接口就挂。常见做法是把这类外部调用拆到REQUIRES_NEW方法里或者用异步处理。3.3 Asynchronous异步调用Future返回值的坑与线程池参数业务方法里常有发通知、推数据这种耗时操作EJB 提供Asynchronous注解让方法调用立刻返回。先看最小实现package demo.ejb; import javax.ejb.AsyncResult; import javax.ejb.Asynchronous; import javax.ejb.Stateless; import java.util.concurrent.Future; Stateless public class NotificationService { Asynchronous public FutureString sendEmail(String to) { // 模拟真实邮件发送耗时可能几秒 try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return new AsyncResult(sent: to); } }调用端拿到的Future不是最后结果而是容器异步线程正在执行的任务句柄。如果你在调用端立刻调用future.get()效果等同于同步等待异步白白浪费一层。正确用法是先让异步任务跑着业务逻辑继续往下走最后需要结果时再get(3, TimeUnit.SECONDS)设置超时避免无限等待。Asynchronous有一个无数人踩过的坑在同一个 Bean 内部调用带注解的方法异步不会生效。原因是异步能力来自容器代理你调用this.sendEmail()时没有经过代理容器无法拦截。解决方式有三种把异步方法拆到另一个StatelessBean 里注入自身代理对象通过SessionContext.getBusinessObject(NotificationService.class)获取当前 Bean 的代理。我实践下来拆到独立 Bean 最直观也最方便单独配置线程池。异步线程池参数在 WildFly 等容器的 EJB 子系统里配置核心看两个核心线程数和队列容量。生产环境里如果异步任务频率高默认线程池会被打满表现为Future.get()超时、任务堆积在队列。排查先看日志里有没有javax.ejb.EJBException伴随线程池拒绝异常再决定是调大线程池还是给异步方法加限流。4. 状态从哪来有状态会话Bean的正确打开方式4.1 有状态Bean的创建与钝化和HttpSession的差别在状态补偿无状态 Bean 不保存调用方数据那一类需要“记住”用户会话状态的场景怎么办比如购物车、分步表单、在线审批草稿。这时候要用Stateful有状态会话 Bean。它的核心特征是容器为每个客户端会话维护一个专属实例实例字段天然就是会话状态。有状态 Bean 在容器里有一个“钝化”机制内存压力大时容器把不活跃的实例序列化到磁盘释放内存等客户端再次访问时再激活。如果你在 Bean 里放了数据库连接、Socket 这类不可序列化的资源钝化就会失败。EJB 提供了两个回调注解来补偿资源状态package demo.ejb; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.ejb.PostActivate; import javax.ejb.PrePassivate; import javax.ejb.Stateful; Stateful public class SessionResourceHolder { private transient java.sql.Connection connection; PostConstruct public void init() { // 创建数据库连接对应方法注入或手动初始化 } PrePassivate public void beforePassivate() { // 钝化前释放数据库连接 } PostActivate public void afterActivate() { // 激活后重新创建连接 } PreDestroy public void cleanup() { // 客户端超时或Remove调用后的清理 } }这里transient关键字配合PrePassivate和PostActivate是状态补偿的标准写法。有状态 Bean 与HttpSession的差别在于HttpSession只存储对象不参与事务有状态 Bean 可以同时绑定事务边界在方法调用上提供隔离。下面用表直接对比维度HttpSessionStateful Bean状态归属客户端会话容器管理的专属实例超时处理自动过期容器钝化或销毁事务参与不参与可以绑定 CMT分布式复制依赖应用配置依赖容器配置显式销毁invalidate()Remove方法4.2 购物车场景实现Stateful Remove的成对使用购物车是有状态 Bean 的经典案例。实现起来不长但Remove的使用必须到位package demo.ejb; import javax.ejb.Remove; import javax.ejb.Stateful; import java.util.ArrayList; import java.util.List; Stateful public class CartBean { private ListString items new ArrayList(); public void add(String skuId) { items.add(skuId); } public ListString getItems() { return new ArrayList(items); } Remove public String checkout() { // 假设这里生成订单号 int total items.size(); items.clear(); return ORDER- total; } }Remove是这个场景里的关键收尾客户端调用checkout()后容器会把当前有状态 Bean 实例直接销毁释放它持有的内存和资源。如果你不写Remove实例只能等超时后被动销毁数据仍在实例字段里占着内存。在 Web 层使用时有几个注意点。EJB注入的CartBean不是每次请求同一个实例容器会依据客户端的 HTTP Session 解析该绑定到哪个实例。因此 HTTP Session 不能禁用 Cookie一旦 Session 丢失购物车内容也会跟着丢。远程客户端场景更麻烦每次调用要携带 Session ID入门阶段没必要碰。还有一点新手容易写错有状态 Bean 里保存的数据如果放到Singleton或StatelessBean 里就不合适了。后者是池化复用多个客户端共享同一个实例购物车会串数据。如果你在项目里看到购物车内容莫名出现在另一个用户手里十有八九是把Stateful写成了Stateless或者从单例 Bean 里注入了本应每会话独立的实例。5. EJB入门避坑指南5个能让你半夜爬起来改代码的典型问题5.1 坑一接口编译通过但部署报错找不到Stateless实现类现象代码在 IDE 里编译通过Local接口和实现类都存在但部署到服务器后报javax.ejb.CreateException或EJB not found日志提示找不到Stateless实现类。原因最常见的是把Stateless写在了接口上而实现类没有标注。容器扫描的是实现类上的注解接口上的注解不会被当作 EJB 注册依据。另外还有一种情况是打包时.class文件路径不对接口和实现类散落在两个 jar 里而模块的打包配置没有把实现类目录包含进去。解决先检查注解位置——接口上不要写Stateless实现类必须标注再检查构建产物用jar tf target/xxx.jar看实现类的.class是否在demo/ejb/路径下。如果用的是 IDE 的 Run 配置注意清理 target 目录后重新构建很多诡异问题来自增量编译留下的旧类文件。5.2 坑二JNDI名字记不住运行时NoInitialContextException现象客户端代码编译通过运行时报javax.naming.NoInitialContextException提示Name [java:global/xxx] not found。原因JNDI 名字拼接规则在不同容器、不同打包方式下不一样。打成ejb-jar和打成war模块名推导就不同如果ejb-jar.xml里显式配置了ejb-name又和注解默认名不一致查找就失败。解决部署成功后先看服务器日志。WildFly 部署时会把所有 JNDI 绑定打印在启动信息里搜java:global就能看到实际名字GlassFish 用asadmin list-jndi-entries --context java:global查看。我的习惯是写一个专门的JndiLister工具类在本地测试环境把InitialContext能查到的所有 EJB 名字打出来避免靠猜。5.3 坑三异步方法没有生效调用还是同步返回现象Asynchronous方法上标注了注解调用时却仍然阻塞或者Future.get()立刻抛IllegalStateException。原因最常见的是同类调用即异步方法在同一个 Bean 内部被this.method()调用绕过了容器代理。其次是方法被声明为final或private容器无法生成代理子类还有一种是把Asynchronous标在了接口方法上但实现类没标部分容器不认接口上的注解。解决把异步方法拆到独立StatelessBean或者在调用处注入自身代理。最省心的写法是让异步方法所在的类和调用方不在同一个类里这样不走this代理必然生效。如果用了Asynchronous后发现日志里线程名没有变化马上按这个方向排查。5.4 坑四有状态Bean被当成无状态Bean反复注入数据串了现象购物车场景里用户 A 添加的商品出现在用户 B 的结算列表里。原因有状态 Bean 的实例本来应该与会话绑定但如果你把它注入到Singleton或全局工具 Bean 里多个客户端共享同一个实例购物车字段就被多人覆盖。另一个原因是 Web 层依赖 Cookie 识别 Session浏览器禁用了 Cookie容器每次请求都认为是一个新客户端创建新实例旧数据全部丢失。解决约束有状态 Bean 的使用边界——从 Servlet 或业务 Facade 里EJB注入不要从Singleton里面再往下传。同时确认 HTTP Session 依赖的JSESSIONID能正常往返。远程调用有状态 Bean 时要显式传递 Session ID这个在入门项目里先不要碰等理解会话绑定原理后再上。5.5 坑五事务回滚了但数据还是写进去了现象业务代码里抛了RuntimeException日志也捕获到了异常但数据库里数据已经提交怎么回滚都没用。原因EJB 的事务回滚依赖异常穿透到容器边界。如果你的代码try/catch把异常吞掉了容器看到方法正常返回就会执行提交逻辑。另一个常见原因是在事务方法里调用了外部系统的 HTTP 接口外部系统已经执行了扣款你的本地事务回滚了两边数据自然对不上。解决在catch块里主动调用ctx.setRollbackOnly()标记事务回滚再把异常抛出或做降级处理跨系统调用的结果要考虑补偿不要指望本地事务能管住外部接口。业务上预期会抛出的异常用ApplicationException(rollbacktrue)标注这样既能让事务回滚又不会被框架误打成系统错误。6. 把EJB入门项目升级为可验证的工程单元测试、打包结构与监控验证6.1 用嵌入式EJB容器把入门项目变成可回归测试的工程入门项目跑到能部署、能调用只完成了第一步。真正让 EJB 项目值得投入的是它能不能被自动化验证。常见做法是引入嵌入式 EJB 容器在 JUnit 里启动一个容器实例直接跑业务方法。这样不依赖外部服务器CI 环境也能执行。package demo.ejb; import org.junit.jupiter.api.Test; import demo.ejb.HelloEjb; class HelloEjbTest { Test void testSayHello() { // 容器启动后从上下文查找并调用 // 这里省略嵌入式容器启动代码核心思路是先通过JNDI拿到代理 HelloEjb hello lookupHelloEjb(); String result hello.sayHello(engineer); // 再断言结果事务和注入都在容器代理里走 } }嵌入式容器方案的价值在于测试走的是真实代理链路事务边界、依赖注入、拦截器全部生效和部署到独立服务器的行为基本一致。相比之下直接new HelloEjb()的单元测试只能测纯逻辑测不到容器行为。打包结构也需要心里有数单个 EJB 模块打包成ejb-jar直接放进服务器的deployments目录即可如果还有 Web 层通常打成war多个模块要共享 JNDI 和事务边界时再考虑ear。入门项目用一个ejb-jar加一个 Servlet 客户端就够等引入多个模块再拆ear。部署后的监控验证我的习惯是看三个地方部署日志里的 JNDI 绑定列表、业务日志里的事务提交记录、以及数据库里实际落库的行数。曾经有一次我改事务属性漏看日志以为回滚生效了结果流水表多了一行脏数据这类问题只有靠日志和数据库双重验证才能兜住。后来我给自己定了条规矩每个 EJB 方法先写一个空方法部署一遍确认 JNDI 名字和事务日志都正常再填业务逻辑这样翻车时能少一个变量要查。希望帮到你。本文还有配套的精品资源点击获取