简介《精通EJB源代码 第二版》是一本面向Java中高级开发者的企业级开发实战指南聚焦EJB 3.x核心机制与工程落地帮助读者突破分布式事务、容器服务集成、JPA持久化及Web服务协同等企业应用难点。资源包共279个文件含79个Java源码涵盖Session Bean、MDB、Entity类实现、33个XML配置部署描述符与Spring整合配置、26个BAT脚本含compile.bat、startWebLogic.bat等环境构建与运行指令、38个CLASS字节码及10个JSP页面完整复现书中案例的编译、部署与调试全流程压缩包仅594KB轻量易用。已有133人学习下载资源结构高度对应第二版内容组织从基础组件建模到容器服务调用再到JPA映射与事务隔离实践辅以可直接执行的WebLogic集成脚本和日志调试支持是深入理解EJB设计思想与源码级实现不可多得的配套实践材料。1. 这不是一本讲EJB理论的书而是一份能让你在WebLogic或WildFly上亲手“拆开”SessionBean生命周期的源码手术刀你有没有遇到过这样的场景在某高校实验室调试一个遗留的Java EE订单系统明明Stateless注解写得一丝不苟但每次调用createOrder()方法后事务却没回滚——日志里连TransactionAttribute(REQUIRED)都没报错可数据库里就是多了一条脏数据。翻遍官方文档只看到“容器管理事务”却看不到UserTransaction到底在哪被begin()、在哪被commit()或rollback()。这时候光看API Javadoc是救不了你的。《精通EJB源代码 第二版》干的就是这件事它不教你“EJB是什么”而是把WebLogic 12c和WildFly 10两个主流商用/开源应用服务器中真正承载Stateless、Singleton、MessageDriven行为的核心类库源码包完整剥离出来按模块归档、加注释、配测试用例。它适合正在维护老Java EE系统、需要定位事务传播异常、远程调用超时、池化对象泄漏等黑匣子问题的开发者也适合想从源码反推规范设计逻辑的中级Java工程师——比如搞懂为什么Remove方法必须是public且不能有参数或者ejbCreate()为何在EJB 3.0之后彻底消失。这不是速成课但你只要照着它的目录结构在IDE里把ejb-container-core.jar和ejb-deployer.jar源码导入再跑通附带的OrderServiceTest集成测试就能亲手看到一个EJB实例从JNDI查找、到拦截器链注入、再到事务上下文绑定的完整链路。2. 环境准备与项目结构解析从解压到IDE识别的5个关键动作2.1 下载包内容与目录结构还原非Maven依赖需手动映射该资源并非Maven工程而是一个结构清晰的源码压缩包解压后呈现标准Java EE容器源码组织方式。主目录包含ejb-source-v2/ ├── docs/ # 含UML类图PlantUML源码、各模块职责说明PDF ├── lib/ # 编译依赖jarjboss-ejb-api_3.2_spec-1.0.0.Final.jar、weblogic-server-lib.jar精简版、javax.transaction-api-1.2.jar ├── src/ # 核心源码根目录 │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/ejb/ # 模拟项目X的业务EJB实现含OrderService、InventoryBean等 │ │ └── resources/ │ └── test/ │ └── java/ │ └── com/example/ejb/test/ # 集成测试MockContainerTest、JndiLookupTest、TransactionBoundaryTest ├── build.xml # Ant构建脚本支持compile、package、run-test └── README.md # 版本对应表EJB 3.2规范 vs WebLogic 12.2.1.4 vs WildFly 10.1.0提示lib/下的weblogic-server-lib.jar并非完整WebLogic安装包而是作者从$WL_HOME/server/lib/中提取的仅含EJB容器相关类的精简jar已去除非必要模块如JMS、JCA大小约4.2MB。WildFly部分同理取自$WILDFLY_HOME/modules/system/layers/base/org/jboss/as/ejb3/main/路径下的核心模块jar。2.2 JDK与应用服务器版本对齐为什么必须用JDK 8u202EJB 3.2规范要求运行在Java SE 7但实际源码中大量使用了JDK 8特性且与具体容器深度耦合。若强行用JDK 11编译会在com.example.ejb.container.interceptor.EjbInterceptorChain类中报错// src/main/java/com/example/ejb/container/interceptor/EjbInterceptorChain.java public class EjbInterceptorChain { // JDK 8u202才稳定支持的MethodHandle缓存策略 private static final MethodHandles.Lookup LOOKUP MethodHandles.lookup(); // ↓ JDK 11移除了sun.misc.Unsafe的默认导出此处会编译失败 private static final Unsafe UNSAFE Unsafe.getUnsafe(); }因此环境配置必须严格匹配JDKOracle JDK 8u202OpenJDK 8u212亦可但需确认sun.misc.Unsafe可用IDEIntelliJ IDEA 2019.3 或 Eclipse 2019-09高版本对JDK 8源码兼容性反而下降容器选型仅验证通过 WebLogic 12.2.1.4 和 WildFly 10.1.0README明确标注其他版本需自行适配ejb-container-core中的DeploymentUnitProcessor类2.3 Ant构建与测试执行绕过Maven的轻量级验证法项目未提供pom.xml全部构建逻辑封装在build.xml中。执行前需先配置build.properties# build.properties jdk.home/opt/java/jdk1.8.0_202 weblogic.home/opt/oracle/wlserver wildfly.home/opt/wildfly-10.1.0然后终端执行# 编译主源码与测试源码 ant compile # 打包为ejb-source-test.jar含测试类与资源 ant package-test # 在嵌入式Mock容器中运行基础测试无需启动完整AS ant run-mock-testrun-mock-test目标会启动一个极简的MockEjbContainer它模拟了JNDI注册、拦截器链加载、事务上下文创建三个核心环节输出类似[echo] Starting MockEjbContainer... [junit] Running com.example.ejb.test.JndiLookupTest [junit] Tests run: 3, Failures: 0, Errors: 0 [junit] INFO MockContainer: Bound ejb/OrderService to instance of OrderServiceBean这步成功证明源码结构、类路径、基础依赖均已就绪可进入调试阶段。2.4 IDE导入实操IntelliJ中让ejb-container-core源码可跳转很多开发者卡在“源码导入后无法CtrlClick跳转到Stateless实现类”。根本原因是Stateless等注解由javax.ejb-api.jar定义但其容器端实现即真正处理该注解的类在ejb-container-core.jar中。需手动关联在IntelliJ中右键项目 →Open Module Settings→Modules→Dependencies点击→JARs or directories→ 选择解压包中lib/ejb-container-core.jar重点选中该jar → 点击右侧Source path→Add...→ 选择src/main/java/即源码根目录再点击Documentation path→Add...→ 选择docs/api/含Javadoc HTML完成此操作后当你在OrderServiceBean上按CtrlClickIDE将直接跳转至com.example.ejb.container.stateless.StatelessComponentDescriptor——这才是Stateless在容器内被解析的真实入口。2.5 核心模块职责速查表5大包名对应什么功能包名功能定位关键类举例调试价值com.example.ejb.containerEJB容器主干生命周期管理、部署单元解析EjbContainer,DeploymentUnitProcessor查start()方法看容器初始化顺序断点processAnnotations()看注解扫描逻辑com.example.ejb.container.stateless无状态SessionBean专属实现StatelessComponentDescriptor,StatelessInstancePool池大小配置、实例创建/销毁钩子、并发控制策略com.example.ejb.container.interceptor拦截器链执行引擎EjbInterceptorChain,AroundInvokeInterceptorInterceptors、AroundInvoke执行时机与异常传递机制com.example.ejb.container.transaction事务上下文绑定与传播TransactionContextManager,JtaTransactionPolicyREQUIRED/REQUIRES_NEW如何影响UserTransaction实例复用com.example.ejb.container.remoteRMI-IIOP远程调用适配层RemoteHomeHandler,EjbObjectProxy序列化白名单、stub生成、连接池超时参数这个表不是背诵清单而是你下次遇到“为什么Interceptors没生效”时第一时间该去哪个包下设断点的路线图。3. 核心模块拆解一个Stateless请求从JNDI查找进入businessMethod()的完整链路3.1 JNDI查找阶段InitialContext.lookup(ejb/OrderService)背后发生了什么当客户端调用InitialContext.lookup(ejb/OrderService)时表面是JNDI查询实则触发了容器的动态代理生成。关键路径在com.example.ejb.container.remote.RemoteHomeHandler// src/main/java/com/example/ejb/container/remote/RemoteHomeHandler.java public class RemoteHomeHandler implements InvocationHandler { private final EjbComponentDescriptor descriptor; Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 若调用的是create()方法则触发实例创建流程 if (create.equals(method.getName())) { return createRemoteInstance(); // ← 跳转到3.2节 } // 2. 若调用的是remove()则清理资源 if (remove.equals(method.getName())) { cleanupInstance(); return null; } throw new UnsupportedOperationException(Unknown home method: method.getName()); } private Object createRemoteInstance() { // 3. 从StatelessInstancePool获取实例非new是池化复用 Object instance descriptor.getInstancePool().obtain(); // 4. 绑定事务上下文若当前线程已有事务 TransactionContextManager.bindToInstance(instance); // 5. 返回EJBObject代理后续所有业务方法调用都走此代理 return Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class[]{OrderService.class}, new EjbObjectHandler(instance, descriptor) ); } }逻辑说明RemoteHomeHandler是OrderServiceHome接口的动态代理处理器。它不负责业务逻辑只做三件事①create()时从池取实例② 绑定事务上下文③ 返回另一个代理EjbObjectHandler处理后续业务方法。descriptor.getInstancePool()返回的是StatelessInstancePool实例其obtain()方法内部会检查池是否为空、是否达到最大容量、是否需要预热——这就是为什么你在ejb-jar.xml里配置max-pool-size后容器启动时会立刻创建一批实例。3.2 实例池化与生命周期StatelessInstancePool如何避免频繁GC无状态Bean的性能关键在于池化。StatelessInstancePool类实现了典型的“懒加载上限控制”策略// src/main/java/com/example/ejb/container/stateless/StatelessInstancePool.java public class StatelessInstancePool { private final QueueObject availableInstances new ConcurrentLinkedQueue(); private final ListObject allInstances new CopyOnWriteArrayList(); private final int maxSize; private final EjbComponentDescriptor descriptor; public Object obtain() { // 1. 先尝试从空闲队列取 Object instance availableInstances.poll(); if (instance ! null) { return instance; // 复用成功 } // 2. 队列空且未达上限则新建 if (allInstances.size() maxSize) { instance descriptor.createInstance(); // ← 调用OrderServiceBean构造器 allInstances.add(instance); return instance; } // 3. 池满阻塞等待非无限等有超时 return waitForAvailableInstance(3000); // 3秒超时抛异常 } public void release(Object instance) { // 归还时重置状态清空成员变量、关闭资源等 descriptor.resetInstance(instance); availableInstances.offer(instance); } }参数说明maxSize来自ejb-jar.xml中stateless-session-bean下的poolmax-pool-size20/max-pool-size/pool。descriptor.resetInstance()是关键钩子——它会调用OrderServiceBean中PostConstruct标记的方法如有并清空Resource注入的DataSource连接避免连接泄露。这也是为什么你永远不该在StatelessBean中用static变量存状态resetInstance()会把它干掉。3.3 拦截器链执行AroundInvoke如何在createOrder()前后插入逻辑所有业务方法调用最终都落入EjbObjectHandler.invoke()它启动拦截器链// src/main/java/com/example/ejb/container/interceptor/EjbInterceptorChain.java public class EjbInterceptorChain { private final ListInterceptor interceptors; // 按Interceptors、AroundInvoke顺序排列 public Object proceed(Object target, Method method, Object[] args) throws Throwable { if (currentInterceptorIndex interceptors.size()) { // 链尾真正执行业务方法 return method.invoke(target, args); } Interceptor interceptor interceptors.get(currentInterceptorIndex); try { // 1. 前置调用interceptor.aroundInvoke() Object result interceptor.aroundInvoke(new InvocationContext(target, method, args)); currentInterceptorIndex--; // 为后置留位置 return result; } catch (Exception e) { // 2. 异常时触发AroundInvoke的异常处理分支 interceptor.aroundInvokeException(e); throw e; } } } // src/main/java/com/example/ejb/container/interceptor/AroundInvokeInterceptor.java public class AroundInvokeInterceptor implements Interceptor { Override public Object aroundInvoke(InvocationContext ctx) throws Exception { long start System.nanoTime(); try { // 执行下一个拦截器或业务方法 Object result ctx.proceed(); long cost System.nanoTime() - start; log.info(Method {} executed in {}ms, ctx.getMethod(), cost / 1_000_000); return result; } catch (Exception e) { log.error(Method {} failed with {}, ctx.getMethod(), e.getMessage()); throw e; } } }逻辑说明proceed()方法是链式调用的核心。每个拦截器调用ctx.proceed()时currentInterceptorIndex递增直到索引越界才执行真实业务方法。这意味着①AroundInvoke方法可以修改args数组如添加审计ID② 可以捕获并包装异常③ 可以完全跳过业务方法直接return mock结果。这也是AOP在EJB中最底层的实现形态——比Spring AOP更早介入且与容器生命周期强绑定。3.4 事务边界控制TransactionAttribute(REQUIRED)如何决定begin()时机事务管理由TransactionContextManager与JtaTransactionPolicy协同完成。关键逻辑在EjbObjectHandler.invoke()中// src/main/java/com/example/ejb/container/interceptor/EjbObjectHandler.java public class EjbObjectHandler implements InvocationHandler { private final Object target; private final EjbComponentDescriptor descriptor; Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { TransactionAttributeType txType descriptor.getTransactionAttribute(method); TransactionContext context null; try { // 1. 根据注解类型决定事务行为 switch (txType) { case REQUIRED: context TransactionContextManager.getCurrent(); if (context null) { // 当前线程无事务 → 容器启动新事务 context TransactionContextManager.begin(); } break; case REQUIRES_NEW: // 挂起当前事务如有强制开启新事务 context TransactionContextManager.suspendAndBegin(); break; case NOT_SUPPORTED: // 挂起当前事务以非事务方式执行 TransactionContextManager.suspend(); break; } // 2. 执行业务方法可能抛异常 Object result method.invoke(target, args); // 3. 成功则提交若本方法开启了事务 if (context ! null context.isNewlyStarted()) { TransactionContextManager.commit(context); } return result; } catch (Exception e) { // 4. 异常时回滚若本方法开启了事务 if (context ! null context.isNewlyStarted()) { TransactionContextManager.rollback(context); } throw e; } } }参数说明isNewlyStarted()是关键判断——它确保只有容器主动begin()的事务才会被commit()或rollback()避免误操作客户端已开启的事务。这也是为什么TransactionAttribute(REQUIRED)在嵌套调用时不会重复开启事务外层方法已开启内层getCurrent()返回非null直接复用。3.5 远程调用序列化RMI-IIOP Stub生成与writeObject()陷阱当客户端位于不同JVM时OrderService代理需通过RMI-IIOP传输。RemoteHomeHandler生成的Stub需满足严格序列化规则// src/main/java/com/example/ejb/container/remote/RemoteHomeHandler.java private Object createRemoteInstance() { // ... 实例获取逻辑 return Proxy.newProxyInstance( cl, new Class[]{OrderService.class}, // 接口必须是public且可序列化 new EjbObjectHandler(instance, descriptor) ); } // OrderService.java 必须声明为public interface public interface OrderService extends EJBObject { // ← EJBObject继承java.rmi.Remote Order createOrder(OrderRequest request) throws CreateException, RemoteException; }注意RemoteException必须显式声明在方法签名中否则RMI运行时会抛UnmarshalException。这是EJB 2.x遗留的硬性约束EJB 3.x虽弱化但仍需遵守。若你删掉throws RemoteException编译通过但运行时调用必失败——错误日志只会显示“connection refused”实际是序列化失败导致stub无法生成。4. 常见问题排查5个血泪经验总结的“玄学”报错及解法4.1 现象InitialContext.lookup(ejb/OrderService)抛NameNotFoundException但jndi.properties配置无误原因ejb-jar.xml中ejb-name与jndi-name不匹配或容器未完成部署。ejb-container-core源码中DeploymentUnitProcessor在解析ejb-jar.xml时会将jndi-name值注册到JNDI树若XML语法错误如jndi-name标签闭合缺失解析失败但无日志输出导致JNDI树为空。解决打开src/main/resources/META-INF/ejb-jar.xml确认session节点内ejb-name为OrderServiceBean且jndi-name为ejb/OrderService用ant debug-deploy目标启动观察控制台是否输出Bound ejb/OrderService to ...。4.2 现象Interceptors(AuditInterceptor.class)不生效AuditInterceptor.aroundInvoke()从未被调用原因AuditInterceptor类未被容器扫描到。ejb-container-core采用ClassPathScanner扫描META-INF/ejb-jar.xml中interceptor-binding指定的类但若AuditInterceptor在src/test/java/下编译后不会打包进ejb-jar导致扫描不到。解决将拦截器类移至src/main/java/或在ejb-jar.xml中显式声明interceptor-binding ejb-nameOrderServiceBean/ejb-name interceptor-classcom.example.ejb.interceptor.AuditInterceptor/interceptor-class /interceptor-binding4.3 现象createOrder()方法内抛RuntimeException但数据库事务未回滚脏数据写入原因OrderServiceBean类上TransactionAttribute注解作用于类级别但createOrder()方法上又加了TransactionAttribute(NOT_SUPPORTED)覆盖了类级设置导致该方法以非事务方式执行。解决检查方法级注解是否误覆盖或统一用类级注解方法内不加事务注解。ejb-container-core中EjbComponentDescriptor.getTransactionAttribute()优先读取方法级再回退到类级。4.4 现象StatelessInstancePool.obtain()长时间阻塞CPU飙升原因max-pool-size设为10但min-pool-size为0且业务高峰时所有实例都在处理耗时SQL池中无空闲实例。waitForAvailableInstance(3000)循环检测availableInstances.poll()但因ConcurrentLinkedQueue无锁特性高并发下poll()空转消耗CPU。解决在ejb-jar.xml中设置min-pool-size5/min-pool-size保证冷启动时预热或修改StatelessInstancePool将waitForAvailableInstance()改为LockSupport.parkNanos(timeout)避免忙等。4.5 现象WildFly环境下Resource DataSource注入为null但WebLogic正常原因ejb-container-core中ResourceInjectionHandler针对不同容器有差异化实现。WildFly使用Resource时需配合jboss-ejb3.xml声明资源映射而WebLogic用weblogic-ejb-jar.xml。源码包中docs/config-examples/目录下有对应配置模板但未自动复制到src/main/resources/。解决将docs/config-examples/jboss-ejb3.xml复制到src/main/resources/并确保resource-env-ref中resource-env-ref-name与Resource(namejdbc/MyDS)一致。5. 二次开发实战给StatelessInstancePool添加监控埋点实时查看池使用率5.1 改造目标在不侵入业务代码前提下暴露availableInstances.size()和allInstances.size()指标我们不修改StatelessInstancePool原有逻辑而是通过JMX MBean暴露监控接口。这是EJB容器最标准的可观测性扩展方式。步骤1定义MBean接口// src/main/java/com/example/ejb/monitor/StatelessPoolMonitorMBean.java public interface StatelessPoolMonitorMBean { int getCurrentPoolSize(); // 当前总实例数 int getAvailableInstances(); // 当前空闲实例数 int getMaxPoolSize(); // 配置的最大池大小 double getUsageRate(); // 使用率 (总-空闲)/总 void resetStats(); // 重置统计可选 }步骤2实现MBean并注入到StatelessInstancePool// src/main/java/com/example/ejb/monitor/StatelessPoolMonitor.java public class StatelessPoolMonitor implements StatelessPoolMonitorMBean { private final StatelessInstancePool pool; public StatelessPoolMonitor(StatelessInstancePool pool) { this.pool pool; } Override public int getCurrentPoolSize() { return pool.getAllInstances().size(); // ← 新增getter } Override public int getAvailableInstances() { return pool.getAvailableInstances().size(); // ← 新增getter } Override public int getMaxPoolSize() { return pool.getMaxSize(); } Override public double getUsageRate() { int total getCurrentPoolSize(); if (total 0) return 0.0; return (double) (total - getAvailableInstances()) / total; } Override public void resetStats() { // 无状态Bean无统计累积此方法留空 } }注意StatelessInstancePool需新增两个publicgetter方法getAllInstances()和getAvailableInstances()因为ConcurrentLinkedQueue和CopyOnWriteArrayList本身是线程安全的直接暴露引用无风险。步骤3在容器启动时注册MBean// src/main/java/com/example/ejb/container/EjbContainer.java public class EjbContainer { private final MapString, StatelessInstancePool pools new HashMap(); public void start() { // ... 原有启动逻辑 registerJmxBeans(); } private void registerJmxBeans() { MBeanServer mbs ManagementFactory.getPlatformMBeanServer(); for (Map.EntryString, StatelessInstancePool entry : pools.entrySet()) { String objectName com.example.ejb.monitor:typeStatelessPool,name entry.getKey(); try { ObjectName name new ObjectName(objectName); StatelessPoolMonitor monitor new StatelessPoolMonitor(entry.getValue()); mbs.registerMBean(monitor, name); log.info(Registered JMX bean: {}, objectName); } catch (Exception e) { log.error(Failed to register JMX bean for {}, entry.getKey(), e); } } } }步骤4验证与使用重新ant compile package打包启动WebLogic或WildFly使用JConsole连接本地JVM在MBeans选项卡中找到com.example.ejb.monitor节点展开StatelessPool→nameOrderServiceBean→Attributes实时查看UsageRate编写Shell脚本定时抓取# monitor-pool.sh jps | grep weblogic | awk {print $1} | xargs jstat -gc $1 1000 3 | tail -1 # 或用JMX命令行工具 java -jar jmxterm.jar -l localhost:7001 -e get -d com.example.ejb.monitor:typeStatelessPool,nameOrderServiceBean UsageRate5.2 进阶技巧用PostActivate/PrePassivate替代PostConstruct做资源预热很多开发者误以为PostConstruct是实例初始化的唯一入口但在StatelessInstancePool中PostConstruct只在createInstance()时调用一次而实例会被反复obtain()/release()。若你想在每次实例被取出时初始化DB连接应使用PostActivateStateless public class OrderServiceBean implements OrderService { Resource(name jdbc/MyDS) private DataSource ds; private Connection conn; PostActivate public void onActivate() { try { this.conn ds.getConnection(); // 每次从池取出时建立连接 } catch (SQLException e) { throw new EJBException(e); } } PrePassivate public void onPassivate() { try { if (conn ! null !conn.isClosed()) { conn.close(); // 归还前关闭连接 } } catch (SQLException e) { log.warn(Failed to close connection, e); } } }为什么有效StatelessInstancePool.release()在归还实例前会调用descriptor.resetInstance()而resetInstance()内部会触发PrePassivate同理obtain()拿到实例后立即调用PostActivate。这比在PostConstruct里建连接更精准——避免连接长时间闲置超时。5.3 血泪教训从那以后我每次改ejb-jar.xml都强制走一遍ant validate-xml脚本ejb-jar.xml是EJB容器的“宪法”一个没闭合整个部署就静默失败。我在某跨平台系统中曾花3天排查NameNotFoundException最后发现是jndi-name标签写成了jndi-name少了个/。后来我给build.xml加了校验目标!-- build.xml -- target namevalidate-xml xmlvalidate failonerrortrue lenientfalse fileset dirsrc/main/resources/META-INF include nameejb-jar.xml/ /fileset xmlcatalog refiddtd-catalog/ /xmlvalidate /target xmlcatalog iddtd-catalog dtd publicId-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN locationlib/ejb-jar_2_0.dtd/ /xmlcatalog现在只要ant validate-xml通过我就知道XML结构绝对合法。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?