基于XDoclet的无状态会话Bean兼容问题:WebSphere 9.0.5中ClassCastException
问题背景
有一个基于Java 1.6开发、部署在WebSphere 8.5的遗留EJB应用,升级至Java 1.8 + WebSphere 9.0.5后,特定XDoclet驱动的无状态会话Bean出现异常:
- 该Bean标注
@ejb.util generate = "physical",当批处理任务先启动并创建Bean,后续应用调用时抛出ClassCastException:Caused by: java.lang.ClassCastException: org.omg.stub.javax.ejb._EJBHome_Stub incompatible with com.xxx.common.reference.interfaces.ReferenceDataBeanHome - 若应用先创建Bean,批处理再使用则正常。
- XDoclet生成的窄化代码无法修改,代码如下:
private static Object lookupHome(java.util.Hashtable environment, String jndiName, Class narrowTo) throws javax.naming.NamingException { // Obtain initial context javax.naming.InitialContext initialContext = new javax.naming.InitialContext(environment); try { Object objRef = initialContext.lookup(jndiName); // only narrow if necessary if (narrowTo.isInstance(java.rmi.Remote.class)) return javax.rmi.PortableRemoteObject.narrow(objRef, narrowTo); else return objRef; } finally { initialContext.close(); } } - 尝试改为
@ejb.util generate = "logical"后,应用先启动时批处理任务会失败。
问题解答
1. 批处理先启动时ClassCastException的原因
核心原因是类加载器隔离导致的类型不兼容:
- 当
@ejb.util generate = "physical"时,WebSphere会将EJBHome的存根(_EJBHome_Stub)直接绑定到物理JNDI节点。 - 批处理任务和主应用通常运行在不同的类加载器上下文:批处理先启动时,存根类由批处理的类加载器加载;后续主应用lookup时,拿到的是批处理类加载器加载的
_EJBHome_Stub实例,而主应用的ReferenceDataBeanHome接口由自身应用类加载器加载。 - JVM中,同一个类名如果由不同类加载器加载,会被视为完全不同的类型,因此强转时抛出
ClassCastException。 - 反之,应用先启动时,存根由应用类加载器加载,批处理lookup时会复用该类加载器的类,类型匹配,强转成功。
2. 确保两种初始化顺序兼容的方案
方案1:共享EJB接口类到全局类加载器
将com.xxx.common.reference.interfaces包下的所有EJB接口类(包括ReferenceDataBeanHome)部署到WebSphere的**共享库(Shared Library)**中:
- 在WebSphere管理控制台中创建共享库,将接口类的JAR包添加进去。
- 配置主应用和批处理任务的类加载器,让它们都引用这个共享库,且设置共享库的类加载器优先级高于应用自身的类加载器。
- 这样无论谁先启动,接口类和存根类都由同一个全局类加载器加载,从根源避免类加载器隔离导致的类型冲突。
方案2:包装JNDI查找逻辑,强制窄化
由于XDoclet生成的代码无法修改,可以在调用lookupHome的外层添加包装逻辑,捕获ClassCastException后强制执行窄化操作:
public static Object safeLookupHome(java.util.Hashtable environment, String jndiName, Class narrowTo) throws javax.naming.NamingException { try { return lookupHome(environment, jndiName, narrowTo); } catch (ClassCastException e) { // 强制使用PortableRemoteObject窄化,绕过原代码的判断逻辑 javax.naming.InitialContext initialContext = new javax.naming.InitialContext(environment); try { Object objRef = initialContext.lookup(jndiName); return javax.rmi.PortableRemoteObject.narrow(objRef, narrowTo); } finally { initialContext.close(); } } }
- 原代码中
narrowTo.isInstance(java.rmi.Remote.class)的判断逻辑存在缺陷:ReferenceDataBeanHome作为EJBHome接口,是Remote的子接口,但isInstance判断的是narrowTo类是否是Remote的实例(而非是否是其子类),导致跳过了窄化步骤,直接返回未窄化的存根,引发类型转换失败。强制窄化可以修复这个逻辑问题。
方案3:同时配置物理和逻辑JNDI绑定
保留@ejb.util generate = "physical",同时通过WebSphere管理控制台手动添加逻辑JNDI绑定:
- 进入EJB管理页面:
应用程序 -> 企业应用程序 -> 目标应用 -> 管理EJB模块 -> 目标会话Bean -> 绑定 - 添加新的逻辑JNDI名称,指向该EJB的Home接口。
- 让批处理任务使用物理JNDI名,主应用使用逻辑JNDI名(或反过来),两者通过不同的JNDI节点获取EJB实例,避免类加载器冲突。
方案4:统一类加载器上下文
- 调整批处理任务的启动时机,配置为在主应用完全启动后再执行,确保存根类由应用类加载器加载。
- 如果批处理是通过WebSphere的调度器执行,可修改批处理的类加载器配置,让它复用主应用的类加载器。
内容的提问来源于stack exchange,提问作者HariHaravelan
相关产品推荐
相关产品推荐

