
简介这是一份面向Java初学者的EJB入门演示项目基于IDEA与JBoss 7.1.1环境帮助理解并实践会话Bean、实体Bean和消息驱动Bean三大核心组件以及事务、并发、持久化等企业级开发基础概念。压缩包共含46个文件以class字节码、XML配置和Java源码为主同时包含JSP页面、properties配置、项目模块文件、依赖jar包及运行前必看说明整体约2.84MB适合本地搭建学习。目前已有774人学习浏览。项目拆分为EJBServer与EJBClient两个模块带有Maven配置和元数据信息通过实际部署与调用可以直观了解EJB接口定义、注解使用、生命周期管理、JNDI绑定以及JMS消息处理流程为后续更复杂的Java EE项目打下扎实基础。1. EJB到底是个啥先把这个老家伙说清楚EJB全称Enterprise JavaBeans翻译过来就是“企业级JavaBean”。我第一次接触这个名词的时候也一头雾水心想JavaBean不是写个私有属性加getter/setter就完事了吗怎么到了企业级就变得神乎其神的。实际上EJB和普通JavaBean完全是两码事它更像是Java官方为企业级应用准备的一套“标准答案”。那EJB解决了什么问题我举个实在的例子。你要做一个电商系统用户下单的时候涉及扣减库存、生成订单、更新账户余额这三个操作任何一个失败都意味着整个下单流程要回滚。如果你自己写事务管理得用JTA的UserTransaction挨个手动begin、commit、rollback代码里到处都是状态判断。而EJB把事务管理纳入了容器托管范畴你在方法上写一行TransactionAttribute(REQUIRED)容器就会自动帮你搞定事务边界问题代码干干净净逻辑清清楚楚。再比如你要限制某个方法同时只能被10个线程访问常规做法是写Semaphore或者基于数据库的行锁但EJB容器本身就提供了线程池和并发控制能力声明一下锁级别就能实现。这就是EJB的核心价值把重复的、通用的、容易写错的企业级基础设施能力下沉到容器里让业务开发者只专注于业务本身。不过EJB的口碑经历过过山车EJB 2.x时代那套Home接口、Remote接口、Local接口、EJBObject、EJBHome光接口就要写四五个部署描述符上千行XML被无数人吐槽“太重了”。直到EJB 3.x转向注解驱动和约定优于配置才算彻底翻身。我们现在做入门项目直接学EJB 3.x及以上版本就行不需要再碰那些老古董。适合学EJB的人无非两种第一种是做Java后端但发现面试或工作时被问到企业级事务、分布式事务如何处理需要补这块知识第二种是维护老系统公司用的就是基于EJB架构的中间件很多金融、政务、物流行业的系统到今天仍然跑在EJB上。如果你想长期在Java企业开发这条路走下去EJB是绕不过去的一课。2. 动手前的准备搭建一套能跑的EJB开发环境我见过不少人卡在第一步不是代码写不出来而是环境根本起不来。EJB本身不是独立运行的框架它必须跑在完整的应用服务器里比如WildFly、JBoss EAP、GlassFish。这和Spring Boot那种内嵌Tomcat直接java -jar的方式完全不同所以思维得先切换过来。2.1 应用服务器选型为什么我推荐WildFly目前做EJB 3.x开发主流选择就是WildFly和GlassFish。GlassFish虽然是Java EE的官方参考实现但社区活跃度明显不如WildFly。WildFly前身是JBoss AS经历了十几年演进性能、工具链、资料都更完善而且它对资源的消耗在完整应用服务器里算是比较克制的了。我个人选择WildFly 23.0.0.Final做入门原因有三一是配置简单解压即用默认端口8080和9990没有复杂的安装向导二是管理控制台和命令行工具CLI都做得成熟调试部署非常方便三是它在EJB 3.2规范上支持得相当完整后面写有状态Bean、消息驱动Bean都验证过没问题。提示如果你只是验证EJB的最基本功能下载WildFly的Standalone版即可。生产环境用的Domain Mode域模式入门阶段先不用碰。2.2 开发工具与JDK版本搭配JDK选Java 8或者Java 11都行但注意WildFly版本对JDK版本有明确要求WildFly 23默认支持Java 8和Java 11再往上需要用更新的WildFly版本否则启动时会直接报UnsupportedClassVersionError。我在本地用的JDK 8配合IntelliJ IDEA Community版就够了不需要什么特殊插件因为代码结构就是普通的Maven工程。IDE方面强烈建议关掉自动部署功能EJB项目不像前端改完就刷新它的编译、打包、部署整个周期很长自动部署反而容易造成资源冲突。用手动部署每次想看效果就执行一次构建流程可控出了错也容易定位。2.3 Maven工程结构一个最简可运行的骨架我们建一个标准的Maven项目GroupId用com.exampleArtifactId用ejb-demo打包方式选择ejb或war都可以。我建议入门阶段打包成war理由是后面如果想顺便加Web层接口不需要额外改部署结构而且WildFly对war中的EJB解析完全没问题。pom.xml里需要引入的关键依赖就一个dependency groupIdjavax/groupId artifactIdjavaee-api/artifactId version8.0/version scopeprovided/scope /dependency注意scope必须是provided因为EJB的API由容器提供如果打成jar包反而会引发class冲突。这个依赖已经包含了EJB、Servlet、JPA、JMS等全部Java EE 8 API写入门代码完全够用。项目结构大概是这样的ejb-demo ├── pom.xml ├── src │ └── main │ ├── java │ │ └── com.example │ │ ├── service (放业务接口和实现) │ │ ├── bean (放实体类) │ │ └── client (放测试客户端) │ └── webapp3. 第一个EJB无状态会话Bean的完整实现与部署这一节我们做一个最经典也是最简单的无状态会话Bean功能是提供一个根据用户ID查询基本信息的方法。别小看这个过程它会让你完整经历EJB开发的全部流程定义业务接口、编写Bean实现类、部署到容器、通过JNDI查找调用。3.1 为什么开发EJB一定要先写接口EJB规范要求客户端面向接口编程而不是直接new实现类这背后是EJB容器使用动态代理机制的结果。当你发布一个无状态会话Bean时容器会生成一个代理对象这个代理负责拦截方法调用在调用前后插入事务管理、安全校验、方法级权限控制等横切逻辑。客户端拿到的永远是代理不是原始的Bean对象。所以业务接口先行是EJB开发的铁律。先定义一个简单接口public interface UserService { UserInfo getUserById(Long id); }这个UserInfo是一个普通的POJO用来承载返回数据字段就按实际需要写比如userId、userName、email这类的。接口可以不放任何业务注解纯Java接口就行。3.2 实现无状态会话Bean注解一行解决一半问题实现类用Stateless注解标注表示这是一个无状态会话Bean。为什么叫无状态因为容器会为这个Bean创建实例池多个实例没有可区分的用户状态。这就像一个共享的公共电话亭谁来了都能用但每个电话亭之间互不干扰。Stateless public class UserServiceImpl implements UserService { Override public UserInfo getUserById(Long id) { UserInfo info new UserInfo(); info.setUserId(id); info.setUserName(用户 id); info.setEmail(id example.com); return info; } }你肯定看出来了这里连mock数据都没做方法体非常简陋只有几行set。但对于入门来说这是最佳实践先把Bean的结构和生命周期搞清楚比一开始就塞一堆复杂业务逻辑要强得多。真正用的时候无非就是在方法里加几行依赖注入的代码调用Dao层或者JPA完成数据持久化无状态会话Bean的角色本身很简单就是处理一次性的业务请求。3.3 部署到WildFly打包上传的正确姿势在Terminal里执行mvn clean package构建成功后target目录下会生成一个ejb-demo.war文件。打开WildFly管理控制台地址是http://localhost:9990进入到Deployments页面点击Upload Deployment选择刚才的war包上传然后点击Enable即可。如果不想在图形界面里操作也可以用命令行方式$WILDFLY_HOME/bin/jboss-cli.sh --connect deploy target/ejb-demo.war --force--force参数很有用每次重新构建后执行这条命令就能强制覆盖部署省去undeploy再deploy的重复操作。部署成功后管理控制台会显示这个war包的状态为OK这就意味着EJB已经被容器成功加载并发布了。3.4 客户端怎么找到EJBJNDI名称全解析EJB部署完成后如何从外部调用它呢这里涉及JNDI命名空间的概念。你可以把JNDI理解成一个全局电话簿客户端通过查找名字找到对应的EJB代理对象。EJB的JNDI名称有固定格式否则就找不到这也是新手最常犯的错。标准JNDI名称格式是ejb:/应用名/模块名/Bean类全限定名!业务接口全限定名比如我这个示例应用名是ejb-demo模块名也是ejb-demowar包结构默认模块名和文件名一致那么JNDI名就是ejb:/ejb-demo/ejb-demo/UserServiceImpl!com.example.service.UserService注意用户接口名后面要带这个接口的完整类名这一点初写很容易漏掉。如果你用了Stateful类型的有状态BeanJNDI里还需要区分是否有Stateful标识稍微复杂一点但思路一致。3.5 写一个独立客户端验证调用在IDE里再建一个普通的Java类当作独立客户端运行注意要准备好wildfly-naming-client依赖否则客户端无法进行JNDI初始化。public class EJBClient { public static void main(String[] args) throws Exception { Properties props new Properties(); props.put(Context.INITIAL_CONTEXT_FACTORY, org.wildfly.naming.client.WildFlyInitialContextFactory); props.put(Context.PROVIDER_URL, http-remoting://localhost:8080); Context context new InitialContext(props); UserService userService (UserService) context.lookup( ejb:/ejb-demo/ejb-demo/UserServiceImpl!com.example.service.UserService); UserInfo user userService.getUserById(88L); System.out.println(user.getUserName()); context.close(); } }运行main方法后如果能够在控制台打印出“用户88”恭喜你你的第一个EJB已经完整跑起来了。这个过程在纯本地的Spring环境里可能要写好几套配置但在EJB体系里就是一次JNDI查找的功夫。4. 有状态会话Bean什么时候用什么时候千万别用接着无状态Bean我们再看看有状态会话Bean这也是面试和实战中都特别容易出混淆点的地方。4.1 状态从哪来会话状态的生命周期管理有状态会话Bean用Stateful注解标注。和无状态Bean的本质区别在于容器会为每一个客户端请求分配一个独立的Bean实例这个实例内部的字段值可以跨多次方法调用保持。这就像一个私人理发师你第一次去预约了剪发第二次去还是同一个理发师他能记得你的偏好和上次的状态。我做一个购物车的例子来演示购物车天然就是有状态的不同用户的购物车不能混在一起。Stateful public class ShoppingCartBean implements ShoppingCart { private ListString items new ArrayList(); public void addItem(String item) { items.add(item); } public ListString getItems() { return items; } Remove public void checkout() { System.out.println(结算完成购物车清空); items.clear(); } }这里有个特别重要的Remove注解标注在checkout方法上。当客户端调用这个方法后容器会销毁这个Bean实例释放它占用的资源。这个设计很聪明相当于给了一个明确的“生命周期结束”信号。4.2 有状态Bean的坑序列化与钝化有状态Bean实例的状态数据可能会被容器暂时写入磁盘这个机制叫钝化Passivation目的是在内存不足时释放资源。钝化要求Bean的内部状态必须可序列化所以在有状态Bean里你放的字段对象需要实现java.io.Serializable接口否则容器在执行钝化时会抛异常。这也是为什么我建议在有状态Bean里尽量放简单类型或DTO对象不要存放数据库连接、文件句柄这类无法序列化的资源。如果你发现服务器日志里频繁出现钝化相关异常多半就是内部放了不能序列化的对象。4.3 有状态和无状态的选择标准一句话讲透很多新手会问“我什么时候用哪个”我给一个非常直白的判断标准如果两次方法调用之间你不关心上一次调用留下了什么就是无状态如果必须记住上一次做了什么就是有状态。用现实场景类比便利店收银员是无状态的每一位顾客都是独立结算他不会记住上一个人买了什么而私人医生是有状态的他需要记住病史和长期健康数据。按照这个逻辑很多业务场景其实都不需要真正的有状态Bean因为我们可以把状态放到数据库或分布式缓存里。无状态Bean的扩展性远好于有状态Bean这也是为什么大多数EJB应用里Stateless占绝对多数。5. 消息驱动Bean让系统异步起来的关键组件再聊一个非常实用的EJB类型——消息驱动Bean注解是MessageDriven。它是EJB和JMS结合的产物专门用于处理异步消息。5.1 队列和主题两种消息模型的工作场景JMS定义了两种消息传递模型点对点队列Queue和发布订阅主题Topic。Queue模式下一条消息只会被一个消费者消费类似两个人打电话说完了就完了Topic模式下消息会被所有订阅者同时收到类似广播电台节目谁在听谁能收到。消息驱动Bean既可以监听Queue也可以监听Topic取决于ActivationConfigProperty里的配置。默认配置如下MessageDriven(activationConfig { ActivationConfigProperty(propertyName destinationLookup, propertyValue java:/jms/queue/myQueue), ActivationConfigProperty(propertyName destinationType, propertyValue javax.jms.Queue) }) public class OrderMessageBean implements MessageListener { public void onMessage(Message message) { try { TextMessage textMsg (TextMessage) message; System.out.println(收到异步消息: textMsg.getText()); // 处理业务比如更新订单状态、发送通知 } catch (JMSException e) { e.printStackTrace(); } } }5.2 什么时候需要消息驱动Bean最常见的场景是削峰填谷。比如用户下单后核心流程只需要快速返回“下单成功”至于后续的短信通知、积分增加、日志记录这些操作完全不需要同步阻塞等待。把这些任务封装成消息发到队列里让消息驱动Bean在后台慢慢消化页面响应速度能快好几倍。这种感觉和生活中的“餐厅叫号”很像你不用站在厨房门口盯着菜做好拿一个号就能干别的菜好了服务员会叫你。消息驱动Bean就是那个服务员它在后台等待消息到来然后处理对应的业务。5.3 消息驱动Bean的事务与异常处理消息驱动Bean的方法执行默认会参与容器事务。如果你在onMessage里处理消息时抛出了RuntimeException容器会认为消息没有被成功处理将自动触发重投也就是换一台消费者再试一次。如果你明确想让某条消息只尝试固定次数就丢弃可以在配置里设置最大重投次数。一个容易踩坑的地方是捕获了异常但没有重新抛出。你如果在一个脏数据的场景中捕获JMSException比如消息内容格式不对可以选择不重试这通常是合理的因为同样的消息再投递一次依然会解析失败。但如果你捕获了异常后不重新抛出容器会认为消息处理成功消息被ack确认并从队列中移除那么这条消息携带的业务数据就丢掉了。我建议处理消息时统一先记录日志再根据异常类型决定是抛出还是吞掉防止消息既没被处理又找不回来。6. 常见问题与排查技巧实录这部分我把实际开发中反复出现的几个典型问题集中整理一下你们可以直接对照排查。6.1 JNDI找不到BeanClassCastException或NameNotFoundException出现找不到Bean的异常九成是JNDI名称写错了。我建议在WildFly管理控制台查看完整的JNDI绑定列表可以直接在http://localhost:9990的Runtime标签页里找到“JNDI View”展开后能看到所有已部署EJB的名称。这里的名字就是标准格式照着复制过去用就不会出错。还有一个注意点是客户端和服务器端要使用同一个接口类。如果你在客户端的类路径中放了一个和服务器完全相同包名但版本不一致的接口类强转时就会抛出ClassCastException。最稳妥的方式是把接口单独抽成一个jar包服务端和客户端都引用同一个jar。6.2 EJB部署失败启动时如何查看错误日志部署失败最常见的原因是Bean类里写了构造时会报错的代码容器在初始化实例池时会执行构造方法如果构造方法抛异常整个模块启动就会失败。WildFly的日志文件在$WILDFLY_HOME/standalone/log/server.log有些IDE集成的终端只显示最后几行容易把关键错误信息淹没掉。排查技巧是用grep过滤关键字比如grep -A 30 ERROR standalone/log/server.log | tail -50这样可以快速定位具体是哪一行抛出了什么异常。比看满屏的INFO日志要高效得多。6.3 有状态Bean数量暴涨导致内存溢出如果系统长时间运行后发现OutOfMemoryError先检查是否过度使用了有状态Bean且忘了调用带Remove的方法。容器无法确定客户端何时不再使用某个有状态Bean所以在内存压力大的时候会触发钝化但如果业务代码每请求都创建新的购物车却不及时移除积少成多还是会把内存吃光。针对这种情况有两个对策业务上保证购物车在完成结算或超时后调用删除方法管理上调整有状态Bean的超时时间让容器在空闲后自动释放实例。设置方式是通过应用服务器的部署描述符或进行容器调优而不是在业务代码中硬编码。6.4 EJB和Spring Boot该怎么选这是很多入门者会纠结的问题。我的看法是如果你新起一个互联网项目团队也熟悉Spring生态那Spring Boot确实更轻便在别处也更容易招人但如果你所在公司在做金融、政务、物流等稳定性要求极高的项目或者你发现自己要维护的就是一套EJB老系统那EJB的容器管理事务、标准化的消息监听、内建的集群能力依然是一套经过了十几年生产环境检验的可靠方案。做入门项目最忌讳的是抱着一门技术就想替代另一门先把EJB的基本开发模式摸通真正需要做技术选型时就有了判断依据。6.5 本地调试EJB如何打断点看代码EJB部署在独立的应用服务器里本地IDE断点默认是不生效的。调试方式是开启远程调试模式在WildFly启动脚本里加入调试参数然后在IDE配置一个Remote JVM Debug的调试器端口选默认的8787。这样你可以像调试本地应用一样给EJB方法打断点观察参数值和执行流程。我调试无状态Bean时喜欢在方法的入口和出口各打一个断点确认事务启动和提交的时机是否符合预期。这个环节能帮你直观感受到容器接管事务的边界比看十篇文档都有用。7. 把EJB知识串起来一个完整的入门学习路线建议做完了上面这些示例项目你会发现自己其实已经接触到了EJB的三个核心类型无状态会话Bean、有状态会话Bean、消息驱动Bean再加上JNDI查找和容器部署这几乎是EJB入门需要掌握的全部东西。EJB 3.x的入门曲线远比网上说的平缓它真正复杂的地方在深入之后比如事务传播、安全角色、集群会话复制、分布式事务XA这些都需要在具体业务中大量实践才能吃透。如果你还想继续深入我建议按这个顺序走先试着给无状态会话Bean加上JPA持久化让查询方法真正操作MySQL数据库然后尝试写两个无状态Bean互相调用体会Bean之间的依赖注入EJB注解和事务传播行为之后可以模拟一个多模块的Maven项目把接口、实体、实现、客户端分别拆成不同module感受模块化工程如何管理EJB依赖最后再自己搭建一个ActiveMQ消息Broker把消息驱动Bean的消费逻辑和真实队列对接起来体会异步解耦的爽感。我自己当年学EJB时最大的感受就是这片领域真正难的不是语法而是缺乏一个能说服自己的使用场景。等你亲手把一个带事务、带消息、带JNDI的完整小系统跑起来再去读那些讲解容器原理的文档很多抽象概念会瞬间落地。建议把今天这个项目当成一个可以反复扩充的骨架每次学会一个新知识点就往上加一小块功能这套代码越攒越厚你对EJB的理解也就越扎实。本文还有配套的精品资源点击获取