知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 12 - 踩坑记录 > 这篇解决什么问题:前面的篇章都是"顺着流程把事做对",但真实开发里,你撞上的多半是"明明该对却报错"的拦路虎——引擎说 `language js is not installed`、`ProxyArray` 不能 `push`、`Java.type` 找不到类、`getCause()` 拿到 null、中文输出全变乱码。这些坑其实**都是可预测、可绕开的**,只是没人提前给你排一遍雷。 > 读完你会知道:8 个高频坑各自的现象、根因与解法——从环境选型(别用带 truffle 却没 js 组件的 GraalVM JDK)、依赖该怎么进 classpath、`ProxyObject`/`IOAccess` 藏在哪个子包、`ProxyArray` 定长的行为边界,到 Java 文本块转义、宿主异常的正确取法,以及 Windows 控制台中文乱码的与一招处理。 --- ## 1. 为什么值得单独开一篇"踩坑记录" 脚本引擎这种"跨语言、跨运行时"的东西,**报错往往不发生在你业务逻辑写错的地方,而发生在调用边界的配置/语义上**。而且这些坑有一个共同特点:一旦知道原理,就再也不会踩第二次——因为它们大多和语言引擎的固有行为绑定,而不是随机 bug。所以这一篇用"现象 → 原因 → 解决"三段式,把 8 个高频坑排好队,动手前先通读一遍,能帮你省掉大量盲目试错。 ### 环境类坑(① ②):多数人最先撞上的两道门槛 ## 坑 ①:GraalVM JDK 上报 `language js not installed` **现象**:在 GraalVM JDK(未安装 js 组件)上运行,报: ``` A language with id 'js' is not installed. Installed languages are: [] ``` **原因**:这里有一个非常反直觉的机制。当你用 Maven 那种方式引入 GraalJS(自带 Truffle 运行时)跑在某个 GraalVM JDK 上时,**两套 Truffle 会出现在同一个 JVM 里**:GraalVM 内置的 truffle 位于 boot 层(`lib/graalvm/`),而你引入的引擎则通过 **locator 机制**把"语言发现"重定向到 GraalVM 的安装目录去查找语言组件;如果那个安装目录里没有 js 组件,就会得出"这个 js 语言的实现不存在",于是报 language not installed。 ``` GraalVM JDK 启动 └─ boot 层 truffle(内置) └─ locator 重定向语言发现 → lib/graalvm/ └─ 没有 js 组件 → "language js is not installed" ``` **解决**:别去折腾 locator,直接换一个「**普通 JDK 17+**」来跑——它不带内置 truffle,你引入的引擎就是唯一的语言实现,冲突从根上消失。 > 附注:网上常见的 `-Dgraalvm.locatorDisabled=true` 在本场景**往往无效**——它禁用的是 locator 的 *jar 发现*,而你的问题根源是 *boot 层 truffle 与所引入引擎的版本/定位冲突*,不是一个 locator 开关能绕过的。 > 一句话:**用"带 truffle 却没装 js 组件"的 GraalVM JDK 反而报错——普通 JDK 17+ 反而干净。** ## 坑 ②:运行 classpath 漏掉了引擎的传递依赖 **现象**:示例明明引入了引擎依赖,一运行却报"引擎找不到 / 类缺失"。 **原因**:所引入的 `js` 依赖是一种"传递式"的类型(你引入的 polyglot `js` 是 `<type>pom</type>`),而某些运行方式(例如依赖构建工具的默认 classpath 计算)**并不会把 pom 型依赖传递进来的 jar 算进运行时 classpath**。结果就是:编译期看起来都在,运行期引擎实现根本没进 classpath。 **解决**:确保运行时 classpath 包含 pom 型依赖实际传递出的 jar。一个稳妥做法是用依赖构建工具显式地生成一份**完整 classpath 清单**,再借此启动运行——这样传递依赖一定被包含进来,不再丢。 > 一句话:**"依赖在 classpath"和"依赖传递的 jar 在 classpath"是两回事——显式生成完整 classpath 最稳。** ### 包路径类坑(③ ④):Polyglot API 的子包拆分布局 ## 坑 ③:`ProxyObject` / `ProxyArray` 其实在子包 **现象**:`import org.graalvm.polyglot.proxy.ProxyObject;` 找不到类,或误以为是 `org.graalvm.polyglot.ProxyObject`。 **原因**:Proxy 相关类**不在** `org.graalvm.polyglot` 根包里,而在 **`org.graalvm.polyglot.proxy` 子包**。 **解决**:用它在子包里的完整路径导入: ```java import org.graalvm.polyglot.proxy.ProxyObject; import org.graalvm.polyglot.proxy.ProxyArray; ``` ## 坑 ④:`IOAccess` 在 io 子包 **现象**:`import org.graalvm.polyglot.IOAccess;` 报错。 **原因**:IO 相关类在 **`org.graalvm.polyglot.io` 子包**。 **解决**: ```java import org.graalvm.polyglot.io.IOAccess; ``` > 与坑 ③ 同理:Polyglot API 把 `proxy`、`io` 等按功能拆进了子包。**凡是用到 `org.graalvm.polyglot` 下的能力,优先让 IDE 自动导入,别手敲猜测包路径。** > 一句话:**`proxy` 在 `.proxy` 子包、`IO` 在 `.io` 子包——手敲 import 不如 IDE 自动导入稳妥。** ### 行为类坑(⑤ ⑥ ⑦):涉及 GraalJS 深层行为 ## 坑 ⑤:`ProxyArray` 是定长的,不支持 `push` **现象**:JS 里对 `ProxyArray.fromList(...)` 代理出来的数组执行 `items.push('x')`,报错(如 `TypeError`)。 **原因**:`fromList` 创建的代理数组**长度固定**,等于 List 的 size,只支持下标读写,**不支持 `push`/`splice` 这类扩容操作**。它是对 Java `List` 的只读投影,而不是一个原生可变 JS 数组。 **解决**,按需二选一: - 需要 JS 侧扩容的场景:换用可扩容的桥接——例如自定义 `ProxyArray` 实现 `set`/`get` 并自行管理长度,或直接在 JS 用原生数组、事后把改动同步回 Java。 - 只做元素替换:直接用下标(示例 `items[0] = 'avocado'` 是合法的)。 > 一句话:**`ProxyArray.fromList` 是 List 的定长快照,能改元素、不能扩容——要 `push` 就换自定义代理或 JS 原生数组。** ## 坑 ⑥:Java 文本块里,反斜杠是转义序列 **现象**:把含正则的 JS 代码放进 Java 文本块(`"""..."""`),正则失效或直接编译报错。 **原因**:Java 文本块里 `\` 仍然是**转义序列**——你写 `\{`,实际进入字符串的是 `{`(`\{` 是无效转义,会被吃掉甚至报错)。要让 JS 源码里最终出现一个 `\`,就必须写 `\\`。**连注释里也不要写 `\/` 这类反斜杠组合**,一样会触发转义。 **解决**:JS 正则里每一个 `\`,在 Java 源码中要写成两个: ```java context.eval("js", """ tpl.replace(/\\{\\{#each ([^}]+)\\}\\}([\\s\\S]*?)\\{\\{\\/each\\}\\}/g, ...) """); ``` 实际传给 JS 引擎的,是被还原了一层之后的正则:`/{{#each ([^}]+)}}([\s\S]*?){{\/each}}/g`。 > 一句话:**Java 文本块里 `\` 仍是转义——正则里的每个 `\` 都要翻倍写,连注释里也别放反斜杠。** ## 坑 ⑦:宿主异常要用 `asHostException()`,而不是 `getCause()` **现象**:JS 调 Java 方法、Java 抛出异常后,Java 侧 `catch(PolyglotException)` 想拿原始异常,用 `e.getCause()` 却拿到 `null`。 **原因**:宿主(Java)的原始异常**不一定挂在 `PolyglotException` 的 cause 链上**;官方为该场景提供的取法是 **`asHostException()`**(这就是 08 篇反复强调的那条规矩)。 **解决**(对应异常处理篇场景四): ```java if (e.isHostException()) { Throwable host = e.asHostException(); // 取回原始异常 System.out.println(host.getClass().getSimpleName()); // IllegalStateException System.out.println(host.getMessage()); // database connection lost } ``` > 一句话:**宿主异常别走 `getCause()`(可能 null),认准 `asHostException()` 一步取回原始 `Throwable`。** ### 环境类坑(⑧):跨平台编码 ## 坑 ⑧:Windows 控制台中文乱码 **现象**:示例输出里的中文(如"折后总额""渲染结果")在 CMD/PowerShell 里显示成乱码。 **原因**:Windows 控制台默认代码页(如 GBK)与按 UTF-8 输出的字符不一致。 **解决**:运行前把控制台切到 UTF-8 代码页: ```bash chcp 65001 ``` 正文与源码只要你坚持 UTF-8 编码书写,乱码就只出在终端显示这一环,一行 `chcp 65001` 即可。 > 一句话:**乱码多半在"显示"不在"数据"——`chcp 65001` 切到 UTF-8,一行搞定。** --- ## 2. 踩坑地图(速查表) 把 8 个坑浓缩成一张速查卡,贴在面前: | # | 坑 | 一句话 | |---|---|---| | ① | language js not installed | 别用没装 js 组件的 GraalVM JDK,用普通 JDK 17+ | | ② | classpath 缺引擎 jar | pom 型依赖的传递 jar 易被漏,显式生成完整 classpath | | ③ | Proxy 类找不到 | 在 `org.graalvm.polyglot.proxy` 子包 | | ④ | IOAccess 找不到 | 在 `org.graalvm.polyglot.io` 子包 | | ⑤ | ProxyArray 不能 push | 定长代理,只支持下标替换 | | ⑥ | 文本块正则失效 | `\` 要写 `\\`,注释也别写 `\/` | | ⑦ | getCause() 为 null | 宿主异常用 `asHostException()` | | ⑧ | 中文乱码 | `chcp 65001` | #### 读这张表背后的规律 八个坑可以归成三族,理解了族,就理解了坑的本质: - **环境族(①②)**:JDK 选型 + 运行时 classpath 是两道最先撞上的门槛,多数"一上来就报错"是它们。 - **包路径族(③④)**:Polyglot API 的子包划分,本质是"组织方式",IDE 自动导入即可回避。 - **行为族(⑤⑥⑦)+ 编码族(⑧)**:代理数组定长、文本块转义、宿主异常取法、终端编码——都属于 GraalJS 的深层行为边界,读懂就再也不会踩。 下面这张速查图把 8 个坑的分布画出来,一眼抓重点:  *图:常见问题速查:8 个坑* > 一句话:**八坑分四族(环境/包路径/行为/编码),先分清坑属于哪族,排查方向就不偏。** --- ## 3. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 12 踩坑记录 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ ① 别用无 js 组件的 GraalVM JDK,普通 JDK 17+ 最稳 │ │ ② classpath 要显式生成,别漏了 pom 型传递依赖 │ │ ③④ proxy/io 在子包,import 让 IDE 自动带 │ │ ⑤ ProxyArray 定长,push 走自定义代理或原生数组 │ │ ⑥ 文本块里正则 \ 一律翻倍写 │ │ ⑦ 宿主异常认准 asHostException(),别用 getCause() │ │ ⑧ Windows 中文乱码就 chcp 65001 │ └──────────────────────────────────────────────────────┘ │ v 你已经走完这门课,把这些坑变成肌肉记忆 ──→ 回到实战篇亲手重做一遍,或把踩坑表存成速查 ``` > 一句话收束:**坑都是可预测的——知道原理,翻一遍速查表,就再也不会被拦在门外。** ## 思考题(自测) 1. 坑 ① 里 locator 重定向机制的原理是什么?为什么"禁用 locator"解决不了这个场景? 2. 坑 ⑤ 里,如果确实需要 JS 侧 `push`,自定义 `ProxyArray` 需要实现哪些方法、怎样维持长度计数? 3. 坑 ⑥ 之外,Java 文本块里还有哪些容易踩的转义点(如 `\n`、Unicode `\uXXXX`、`\"`)?你会在什么情况下干脆改用普通字符串拼接来存 JS 源码?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入