PlantUML 离线编译 TeaVM 依赖的占位方案:clide_stubs/teavm 编译期 Stub 设计、构建与集成
2026/9/23 1:24:09 网站建设 项目流程

PlantUML 离线编译 TeaVM 依赖的占位方案:clide_stubs/teavm 编译期 Stub 设计、构建与集成

【免费下载链接】plantumlGenerate diagrams from textual description项目地址: https://gitcode.com/gh_mirrors/pl/plantuml

本篇技术指南聚焦 PlantUML 仓库中tools/clide_stubs/teavm这一编译期专用 Stub 工程:它用极小的org.teavm.*类型占位实现,让clide/jdtls 在无网络、无法访问 Maven Central 的环境中也能编译 PlantUML 的 TeaVM 专用源码。读完本文,你将掌握该 Stub jar 的设计原理(为何能"只编译、不运行")、它所覆盖的 14 类 TeaVM 类型、ant构建与.clide/集成方式,以及 PlantUML 侧对等源码如何实际消费这些类型。

一、背景:为什么 PlantUML 需要一份 TeaVM 编译期占位 jar

PlantUML 具备一套基于 TeaVM 的浏览器端渲染前端,相关源码集中在 src/main/java/net/sourceforge/plantuml/teavm,并散落在PortableImageTeaVM.javaEmoji.javaOpenIconic.javaSignatureUtils.javaPathSystem.java等文件中。这些文件直接import org.teavm.*下的 JS 互操作类型,例如 GraphVizjsTeaVMEngine.java 中的org.teavm.jso.JSObject

问题在于:真实 TeaVM 依赖需要从 Maven Central 下载,而离线沙箱环境没有网络。为了让clide/jdtls 这类基于 Eclipse JDT Language Server 的代码工具能够编译这些源文件,仓库在 tools/clide_stubs/teavm 下维护了一组"最小化替身"。

这份 Stub jar 的定位在 tools/clide_stubs/teavm/README.md 中写得很明确:

  • 唯一目的是让clide/jdtls编译TeaVM 专用源码;
  • 绝不用于实际运行,不提供任何真实 JavaScript 互操作,纯粹是为了满足 Java 编译器;
  • 若真实 TeaVM API 无法到达(沙箱无网络),Stub 就作为编译期的类型占位。

同一思路也体现在仓库的其他两个 Stub 工程上:tools/clide_stubs/ant(Apache Ant 类型占位)与tools/clide_stubs/openpdf(OpenPDF 类型占位),说明这是一套体系化的离线编译解决方案。

二、Stub 覆盖的类型清单

Stub 源码位于 tools/clide_stubs/teavm/src/org/teavm,共 16 个文件,覆盖以下 14 种org.teavm.*类型(见原文档类型表):

类型种类
org.teavm.jso.JSObject标记接口(marker interface)
org.teavm.jso.JSBody注解
org.teavm.jso.JSExport注解
org.teavm.jso.JSFunctor注解
org.teavm.interop.Async注解
org.teavm.interop.AsyncCallback<T>接口
org.teavm.interop.PlatformMarker注解
org.teavm.jso.dom.xml.Element接口
org.teavm.jso.dom.xml.Document接口
org.teavm.jso.dom.html.HTMLElement接口
org.teavm.jso.dom.html.HTMLDocument接口(含一个抛异常静态方法)
org.teavm.jso.dom.html.HTMLCanvasElement接口
org.teavm.jso.canvas.CanvasRenderingContext2D接口
org.teavm.jso.canvas.ImageData接口

此外类型表中还有org.teavm.jso.typedarrays.Uint8ClampedArray接口(JSObject标记、set(int,int)方法)。这些类型并非照抄真实 TeaVM API 的全貌,而是按 PlantUML 实际调用面裁剪:接口只声明 PlantUML 源码真正用到的方法,其余一概省略。

例如 Element.java 只声明了setAttribute(String, String)appendChild(Element)setTextContent(String)三个方法;CanvasRenderingContext2D.java 只声明createImageData(int, int)putImageData(ImageData, int, int),对应 PlantUML PNG 导出路径的真实调用(详见第五节)。

三、如何保持"惰性":Stub 的三个设计支柱

原文档用 "How it stays inert" 一节解释了这份 jar 为何既能让编译通过、又不可能被误用于运行,其原理可归纳为三点,均有对应源码佐证:

3.1 绝大多数类型是纯接口:抽象方法,无方法体

真实 TeaVM API 中,org.teavm.*的 JS 后端对象基本都是接口(JS-backed 对象,没有真正的 Java 实现),因此 Stub 同样以接口声明、方法无方法体。比如 JSObject.java 是刻意为空的标记接口——所有 JS 后端类型都继承它;Document.java 同样为空,仅继承JSObject

3.2 唯一带方法体的HTMLDocument.current():直接抛异常

PlantUML 代码获取 DOM 文档的唯一入口是静态方法HTMLDocument.current()。Java 8 起接口允许静态方法,所以它是整个 Stub 中唯一真正需要方法体的地方。其实现见 HTMLDocument.java:

static HTMLDocument current() { throw new UnsupportedOperationException( "TeaVM stub: HTMLDocument.current() is only meant to satisfy the compiler, never to run."); }

这一设计一举两得:既然current()永远抛异常,那么下游任何代码都无法获得这些类型的真实实例,其余所有方法(如createElementgetElementById)保持抽象、在实践上永远不可达——编译期它提供了类型形状,运行期它保证了绝对安全。

3.3 注解全部是纯标记:无任何逻辑

@JSBody@JSExport@JSFunctor@Async@PlatformMarker这五个注解在真实 TeaVM 中都带有编译器语义,但在 Stub 里只是"形状":

  • JSBody.java:真实语义是将被注解的native方法替换为指定 JavaScript 片段,Stub 中仅保留String[] params() default {}String script()两个成员;
  • JSExport.java:真实语义是把静态方法导出到生成的 JS 模块,Stub 为空标记;
  • JSFunctor.java:真实语义标记单方法JSObject接口为 JS 回调类型,Stub 为空标记;
  • Async.java:真实语义把异步 JS 调用变成"貌似同步"的native方法,Stub 为空标记;
  • PlatformMarker.java:真实语义让 TeaVM 编译器按目标平台替换方法体(例如isTeaVM()编译期常量),Stub 为空标记;
  • AsyncCallback.java:@Async方法底层接收的回调接口,保留complete(T)error(Throwable)两个抽象方法,但同样没有任何实现——因为 jar 永远不会发出真实实例。

由于这些注解不触发任何行为,PlantUML 被它们标注的方法保持native(或原样不变),Stub 无需提供任何运行逻辑。

四、构建与集成到 clide/jdtls

4.1 构建命令

tools/clide_stubs/teavm目录下执行:

ant

产物为teavm-stub.jar。这是典型的 Ant 工程结构(src/源码目录 +ant构建),与tools/clide_stubs下另外两个 Stub 工程保持一致。

4.2 集成到.clide/目录

构建出 jar 后,将它复制到项目的.clide/目录(该机制详见clide项目的JDTLS.md文档)。clideopen_project命令会自动拾取.clide/下的 jar 并加入 jdtls 的 classpath,从而让 TeaVM 专用源码得以解析。

4.3 验证方式

原文档给出了明确的验证口径:将 jar 放入.clide/后,通过clideprint_diagnostics errors确认,所有org.teavm相关的编译错误消失;此时剩余的错误均来自其他无关依赖(OpenPDF、XMLUnit、Mockito 等)。这意味着 Stub 的覆盖范围已经完整满足 PlantUML TeaVM 源码的编译需求。

五、PlantUML 侧的真实消费点:为何"够用"

要理解 Stub 为何按上述方法裁剪,可以对照 PlantUML 侧的实际调用。下面这些使用点与 Stub 类型一一对应,形成了"声明-使用"闭环:

5.1HTMLDocument.current():全局唯一文档工厂

  • SvgGraphicsTeaVM.java 的构造函数第一行就是this.document = HTMLDocument.current();,随后通过createElement/appendChild构建 SVG 根节点;
  • PortableImageTeaVM.java 的toPngDataUrl()HTMLDocument.current().createElement("canvas")创建离屏画布;
  • PlantUMLBrowser.java 用HTMLDocument.current().getElementById(elementId)定位渲染目标元素。

正因为current()是唯一的"实例入口",把它做成抛异常的方法即可从根上杜绝 Stub 被运行时误用——这正是第三节第 3.2 点设计的用意。

5.2@JSBody+@Async+@JSFunctor:Viz.js 异步渲染桥

GraphVizjsTeaVMEngine.java 是典型组合:

  • @JSBody提供vizMissingFallback()(L73-L79),检测window.Viz是否加载;
  • @Async修饰renderDotToSvg(String)(L81-L82),配合AsyncCallback<String>把 JS Promise 变成"貌似同步"的 Java 调用(L88-L90);
  • @JSFunctor声明StringCallback单方法回调接口(L153-L156)。

5.3@JSExport:导出浏览器渲染入口

PlantUMLBrowser.java 用@JSExport导出render(String[], String, JSObject)(L283-L284)与renderToString(...)(L315-L317),供 JS 侧调用;@JSBody解析options中的darkmaxSvgSize等字段(L339-L348)。

5.4@PlatformMarker:编译期死代码消除

TeaVM.java 的isTeaVM()@org.teavm.interop.PlatformMarker标注。在 TeaVM 编译时该方法被静态解析为true,于是if (TeaVM.isTeaVM())这类分支在编译期就被消解,未分支整体从生成的 JS 中剔除;在 JVM 上则始终返回false,不触发任何消除。

5.5 画布与像素数组:PNG 导出路径

PortableImageTeaVM.java 的toPngDataUrl()完整串起了 Canvas 相关 Stub:HTMLCanvasElementsetWidth/setHeight/getContext("2d")CanvasRenderingContext2DcreateImageData/putImageDataImageDatagetData()Uint8ClampedArrayset(...),最后@JSBody调用canvas.toDataURL('image/png')(L197-L198)。

5.6 其他 TeaVM 消费点

  • SvgGraphicsTeaVM.java 用@JSBody+XMLSerializer序列化 SVG 为字符串,用共享 canvas 做文本测量(L443-L445);
  • TeaVMSvgDocument.java 用@JSBody创建 SVG 命名空间元素;
  • Emoji.java 与 OpenIconic.java 用@JSBody读取window.PLANTUML_EMOJI_SHORTCUTwindow.PLANTUML_EMOJIwindow.PLANTUML_OPENICONIC等全局对象;
  • PathSystem.java 使用org.teavm.jso.JSObject

从源码结构看,Stub 类型的方法裁剪正是逐一对齐上述调用点的结果:getContext在 Stub 中返回通用的JSObject而非真实 TeaVM 的更宽泛返回类型,是因为 PlantUML 随即将其强转为CanvasRenderingContext2D,而接口到接口的强转无论有无继承关系都能编译通过(见 HTMLCanvasElement.java 的注释说明)。

六、使用边界与注意事项

  1. 严禁用于运行时:Stub jar 没有任何 JavaScript 互操作能力,HTMLDocument.current()的调用会立即抛出UnsupportedOperationException。它只服务于编译期类型解析,绝不可进入运行时 classpath。
  2. 按需最小化:Stub 方法签名是 PlantUML 实际调用面的最小集合,并非完整 TeaVM API。若 PlantUML TeaVM 源码未来新增对其他org.teavm.*方法或类型的引用,需要在tools/clide_stubs/teavm/src/org/teavm下补充对应占位。
  3. 编译验证的局限:Stub 只消除org.teavm相关错误。如原文档所述,其余依赖(OpenPDF、XMLUnit、Mockito 等)造成的编译错误需要分别由tools/clide_stubs/openpdf等其他 Stub 或对应处理机制解决,不能指望本 jar 一并覆盖。
  4. JAVA8 兼容性HTMLDocument.current()作为静态接口方法依赖 Java 8 语法,这也与 PlantUML 源码中// ::remove file when JAVA8这类条件编译标记(见 GraphVizjsTeaVMEngine.java)所反映的 Java 版本策略一致。

七、结语

tools/clide_stubs/teavm是一个"小而巧"的工程:用 16 个源文件、14 类类型,就为离线环境解开了 PlantUML TeaVM 前端源码的编译死结。它的价值不在于实现任何功能,而在于精准回答"编译器需要看到什么"——接口形状 + 注解骨架 + 一个抛异常的工厂方法。结合 tools/clide_stubs/teavm/README.md 与 tools/clide_stubs/teavm/src/org/teavm 的源码注释,读者可以完整复现这套"编译可用、运行必败"的占位策略,并理解它与 src/main/java/net/sourceforge/plantuml/teavm 真实使用点之间的映射关系。

【免费下载链接】plantumlGenerate diagrams from textual description项目地址: https://gitcode.com/gh_mirrors/pl/plantuml

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询