PAYATHON 2026

tomee 10.1.4 使用 scope="request" 时为何抛出 LinkageError?

排障赵师傅

结论

这份异常指向 TomEE 在集成 Faces、CDI 和类加载时出现的代理类重复定义问题。<c:set scope="request"> 本身是合法用法,触发异常并不说明页面写错了。

建议先向 Apache TomEE 报告,并附上最小复现项目。堆栈涉及 OpenWebBeans 的代理生成和 TomEE/OpenEJB 的类定义逻辑,可由维护者判断是否需要转交 OpenWebBeans 或 MyFaces。目前还无法仅凭堆栈确定问题归属、具体缺陷编号或修复版本。

异常实际说明了什么

排查应从最底层异常入手:

java.lang.LinkageError:
loader org.apache.tomee.catalina.TomEEWebappClassLoader ...
attempted duplicate class definition for
org.apache.webbeans.custom.Map$OwbNormalScopeProxy14900025290

同一个 TomEEWebappClassLoader 已经定义过这个代理类,随后又收到相同类名的定义请求,因此 JVM 拒绝加载。

创建实例和定义类是两回事:

  • 同一个代理类可以有多个实例。
  • 同一个类加载器重复定义同名类,会触发 LinkageError。

问题出在代理类的生成、查找、缓存或加载过程中。堆栈尚不能确定具体哪个环节出了错,也无法确认是否涉及并发或代理类名计算冲突。

调用路径可以简化为:

Faces 渲染页面
  → EL 求值
  → OpenWebBeans WebBeansELResolver
  → 获取 normal-scoped bean 的引用
  → NormalScopeProxyFactory 创建代理
  → TomEE/OpenEJB ClassDefiner 定义类
  → LinkageError

外层的 RuntimeException 和 InvocationTargetException 都是异常包装,排查时应保留完整的 Caused by 链。

为什么加上 scope="request" 会触发它

这段代码把值放入请求属性中:

<c:set var="demo" value="demo" scope="request"/>

scope="request" 指定的是变量的存储范围,并没有为字符串 "demo" 声明 CDI @RequestScoped bean。

页面还访问了:

#{param['someparam']}

Faces 的 param、requestScope 等隐式对象支持以 Map 形式访问数据。异常中的代理类名包含 Map$OwbNormalScopeProxy,可以重点检查这些隐式对象在 Faces/CDI 集成中的解析和代理过程。

不过,堆栈没有记录当时正在解析哪个 EL 标识符,因此无法断定 param 与 requestScope 生成了相同的代理名。目前只观察到加上 scope="request" 会触发异常,确切原因还需要通过复现和源码定位确认。

如何排查和处理

1. 在干净环境中确认复现条件

使用全新的 TomEE 10.1.4 实例,只部署复现应用,并记录:

  • JDK 的完整版本;
  • TomEE 的发行包类型;
  • 实际加载的 MyFaces、OpenWebBeans 版本;
  • 首次请求是否失败,以及是否只有热部署后才失败。

如果项目使用 Maven,可以先检查依赖:

mvn dependency:tree

还应检查最终 WAR 的 WEB-INF/lib,确认是否额外打包了与服务器重复或不匹配的 Faces、CDI、EL 实现。需要排除依赖冲突,但异常本身并不能证明存在重复 JAR。

2. 分别移除触发因素

依次测试以下变化,每次只改一处:

  1. 保留 scope="request",移除 #{param['someparam']}。
  2. 保留参数读取,移除 scope="request"。
  3. 分别单独访问 #{param['someparam']} 和 #{requestScope.demo}。

通过这些对照,可以判断异常是否需要多个 Map 类型的隐式对象共同参与,还是单独访问某个对象就会触发。提交报告时应附上实际测试结果,保留推测与已确认根因之间的区别。

可尝试的规避代码

只需要页面变量时,去掉显式作用域

如果 demo 只供当前 Facelets 页面使用,可以尝试:

<!DOCTYPE html>
<html lang="en"
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="jakarta.faces.core"
      xmlns:c="jakarta.tags.core">
<f:view>
    <body>
        <c:set var="demo" value="demo"/>
        #{param['someparam']}
        #{demo}
    </body>
</f:view>
</html>

省略 scope 后,Facelets 会通过变量映射机制处理这个变量,它不再具有原来的请求属性语义。如果 Servlet、过滤器或其他代码还需要通过 request.getAttribute("demo") 读取它,就不能直接这样替换。

请求级业务数据可放入 CDI bean

如果这个值属于请求级业务状态,可以尝试通过具名 bean 暴露:

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;

@Named("demoBean")
@RequestScoped
public class DemoBean {

    public String getValue() {
        return "demo";
    }
}

页面改为:

#{param['someparam']}
#{demoBean.value}

这样,页面会通过明确的 bean 属性访问业务数据。这种写法仍然使用 CDI,也不会创建名为 demo 的请求属性。能否避开当前异常,需要在复现环境中验证。

这两种改法都只是待验证的规避方案,即使有效,也不能说明底层缺陷已经修复。

应向哪个项目报告

建议优先向 Apache TomEE 报告,因为故障发生在 TomEE 发行版中,调用链也跨越了 MyFaces、OpenWebBeans 和 TomEE/OpenEJB。

报告中应附上复现项目、固定的提交版本、启动步骤、访问地址、完整异常链,以及上述对照测试结果。如果后续能在独立的 OpenWebBeans 环境中复现,或将问题定位到其代理生成逻辑,再向 OpenWebBeans 提交对应问题会更准确。

不要仅凭 Map$OwbNormalScopeProxy 的类名就自行替换服务器内的 OpenWebBeans JAR。验证其他版本时,优先测试整套 TomEE 发行包,并保持 JDK 和应用条件相同。目前无法确定某个升级版本一定能解决问题。

备注:内容仅供参考。