知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 06 - JS 调用 Java > 这篇解决什么问题:上一篇是 Java 把数据送进 JS 算。但业务往往还要反着来——让一段 JS 脚本"回头"调用你写好的 Java 方法、操作 Java 集合,甚至用 `Java.type` 在 JS 里 new 一个 Java 对象。麻烦的是,GraalJS 的默认沙箱**禁止 JS 碰 Java**,直接 `Java.type(...)` 就报错。这篇把"JS 反向调用 Java"讲透:默认沙箱为什么锁死宿主、`allowHostAccess`(对象级)和 `allowHostClassLookup`(类级)两个开关各管什么、怎么注入 Java 对象、怎么用 `Java.type` 建 Java 类。 > 读完你会知道:GraalJS 默认"JS 碰不到 Java"的安全设计与背后逻辑、两个权限开关的分工(对象级 / 类级)、把 Java 对象暴露给 JS 的 `putMember` 用法、JS 直接操作 Java 集合有多自然,以及把宿主对象取回字符串前为什么要先 `String(...)` 收口。 --- ## 1. 默认沙箱:JS 一开始碰不到 Java 先说一个反直觉但重要的默认:**GraalJS 的默认配置(`Context.create`)是不允许 JS 访问 Java 宿主的**。在默认的 `Context` 里,JS 没有 `Java` 这个全局对象,你直接写 `Java.type('...')` 会**直接报错**。 这不是坑,而是精心设计的安全默认:  *图:JS 调用 Java:两个权限开关* ``` 默认(Context.create("js")) JS: Math.sqrt(144) ✅ 纯计算,允许 JS: Java.type('...') ❌ 默认没有 Java 对象 JS: 调用注入的 Java 方法 ❌ 默认无宿主访问权限 ``` **为什么要这么锁?** 一旦 JS 可以随意碰 Java 宿主,就相当于给了脚本访问整个 JVM 的能力——可以碰文件、碰网络、碰类加载,这是执行**不可信脚本**(比如从数据库读出来的一条规则)时绝不能接受的危险。所以 GraalVM 的哲学是"**默认什么都不给**,要用哪个权限开关、才放开哪块"。这是良好的最小权限原则。 ``` 放开后(Context.newBuilder("js")) .allowHostAccess(HostAccess.ALL) → 允许调用已暴露的 Java 对象方法 .allowHostClassLookup(s -> true) → 允许 Java.type() 查找/创建 Java 类 ``` > 一句话:**默认沙箱把"JS 访问 Java 宿主"彻底锁死,需要哪块能力就用对应开关放开——最小权限是安全设计的底线。** --- ## 2. 两个开关:一个管"对象级",一个管"类级" 要理解这套权限模型,抓住核心:**GraalJS 把"宿主访问"分成两个层面,各自独立开关**。 | 开关 | 控制什么 | 典型场景 | |---|---|---| | `allowHostAccess(HostAccess.ALL)` | JS 能否调用**已暴露的 Java 对象**的方法(如 `greeter.greet(...)`、`javaList.sort(null)`) | 把 `putMember` 注入的 Java 对象当普通 JS 对象用 | | `allowHostClassLookup(s -> true)` | JS 能否用 **`Java.type('全类名')`** 查找并实例化 Java 类 | JS 里直接 new Java 对象(如 `UUID.randomUUID()`) | 两者的分工可以一句话记:**`allowHostAccess` 管"对象级"(已经暴露给你的对象,它的方法能不能调),`allowHostClassLookup` 管"类级"(能不能按全类名去查/建类)**。两者彼此独立,可以只开一个: ``` JS 侧写法 Java 侧所需权限 greeter.greet('World') → allowHostAccess(HostAccess.ALL) + putMember Java.type('java.util.UUID') → allowHostClassLookup(s -> true) ``` 注意到 `greeter.greet(...)` 这一行,除了 `allowHostAccess`,还有个 `putMember`——因为"对象要被 JS 看见"得先由 `putMember` 注入;`allowHostAccess` 管的是注入之后"能不能调它的方法"。而 `Java.type(...)` 只需要 `allowHostClassLookup`,因为它走的是"按类名查找"这条独立的类级通道。 > 一句话:**`allowHostAccess`(对象级)管"已暴露对象的方法能不能调",`allowHostClassLookup`(类级)管"能不能 Java.type 查/建类",两者独立,可根据需要只开一个。** --- ## 3. 前置:一个准备被 JS 调用的 Java 业务类 反方向的调用,总得先有一个"被调方"。先准备一个普通的 Java 静态内部类: ```java public static class Greeter { public String greet(String name) { return "Hello, " + name + " (from Java)"; } public int add(int a, int b) { return a + b; } } ``` 这个类没什么特别的,就是普通的 public 方法。**注意**:`greet("World")` 会返回 `"Hello, World (from Java)"`,所以 JS 里 `greeter.greet('World')` 打印的就是这行。它告诉我们要点:**能被 JS 调用的,是 Java 类的 public 方法**——GraalVM 的宿主访问是建立在反射/访问控制之上的,非 public 方法不会被暴露。 --- ## 4. 放开权限:这两个开关是前提 ```java try (Context context = Context.newBuilder("js") .allowHostAccess(HostAccess.ALL) .allowHostClassLookup(s -> true) .build()) { ``` 这就是第 03 篇提过的"第二种创建方式"——用 `Context.newBuilder` 配置后 `build()`。**规则很简单**:本章要试"调 Java 方法"就要 `allowHostAccess(HostAccess.ALL)`,要试"`Java.type` 建类"就要 `allowHostClassLookup(s -> true)`。这两行是一个可以预见的开篇动作,别指望它们之外的开关还能自动帮你放行。 > 一句话:**要让 JS 调用 Java,「用 builder 建 Context」+「开对应权限开关」是前提,两个开关按需组合。** --- ## 5. 三种"JS 用 Java"的方式,逐一看 ### 5.1 putMember 注入对象 + 调用方法 ```java context.getBindings("js").putMember("greeter", new Greeter()); context.eval("js", "print(greeter.greet('World'))"); context.eval("js", "print('1 + 2 = ' + greeter.add(1, 2))"); ``` 拆开看三步: 1. `putMember("greeter", new Greeter())`:把 Java 对象注入 JS 全局作用域。JS 里 `greeter` 现在就是一个可用对象(这一步用上了第 03 篇的 `getBindings`)。 2. `greeter.greet('World')`:JS 直接调用 Java 的 public 方法。参数 `'World'`(JS 字符串)会**自动转成 Java `String`**——跨语言的自动传参在这一层发生了。 3. 输出两行:`Hello, World (from Java)` 和 `1 + 2 = 3`。 ``` Hello, World (from Java) ← greeter.greet('World'),参数自动转 String 1 + 2 = 3 ← greeter.add(1, 2),方法被 JS 调用 ``` ### 5.2 让 JS 直接操作 Java 集合 ```java context.getBindings("js").putMember("javaList", new java.util.ArrayList<>( java.util.List.of(3, 1, 4, 1, 5))); context.eval("js", """ javaList.sort(null); print('排序后: ' + javaList); print('size: ' + javaList.size()); """); ``` 把 `ArrayList` 注入后,JS 里可以像操作普通 JS 数组一样调它: - `javaList.sort(null)`:调用 Java 的 `List.sort(Comparator)`(传 `null` 表示自然排序)→ 原地把 `[3,1,4,1,5]` 排成 `[1, 1, 3, 4, 5]`。 - `javaList.size()`:调用 Java 方法返回 `5`。 - `'排序后: ' + javaList`:JS 把 Java 集合转成字符串拼进输出 → `[1, 1, 3, 4, 5]`。 ``` 排序后: [1, 1, 3, 4, 5] ← JS 调用 javaList.sort(null) 原地排序 size: 5 ← JS 调用 javaList.size() ``` 这一段展示了"JS 用 Java 集合"完全是"用对象方法"的思路——你不需要任何桥接,`sort`/`size` 见到就能调。 ### 5.3 Java.type() 在 JS 里造 Java 对象 ```java Value uuid = context.eval("js", "String(Java.type('java.util.UUID').randomUUID())"); System.out.println("JS 生成的 UUID: " + uuid.asString()); ``` 这行是 `Java.type` 的完整链路,拆开看四步: 1. `Java.type('java.util.UUID')`:按全类名**查找** Java 类(需要 `allowHostClassLookup`)。 2. `.randomUUID()`:调用这个类的静态方法,生成一个 `UUID` 对象。 3. `String(...)`:把 Java 的 `UUID` **转成一个 JS 字符串**——这是关键的一步。 4. 转完后,`eval` 返回的 `Value` 才是字符串,才能 `asString()` 取回。 为什么非要 `String(...)`?那是 5.4 的 Q 要回答的。 > 一句话:**`putMember` 注入对象后 JS 能直接调其 public 方法、像用 JS 数组一样操作 `ArrayList`;`Java.type('全类名')` 能在 JS 里按类名查找并调用静态方法造对象。** --- ## 6. 两个值得停下来想清楚的问题 ### 6.1 为什么开够了开关,还要显式 String(...) 收口 这是最容易误会的点:都开了 `allowHostAccess` 和 `allowHostClassLookup`,为什么 `randomUUID()` 的结果还要再套一个 `String(...)`? 根因在 `Value.asString()` 的能力边界:**`asString()` 只能转换"字符串"**。而 `randomUUID()` 返回的是 Java 的 `UUID` **对象**(宿主对象),不是字符串。如果不 `String(...)` 包装,Java 侧拿到的 `Value` 包装的是"宿主对象",`asString()` 会**抛异常**。 所以这里的 `String(...)` 是一个常见的"**收口技巧**":用 JS 内建的 `String()` 把宿主对象显式转成 JS 字符串,才轮到 Java 侧 `asString()` 去读。跨语言读值,先把类型收窄成字符串,再取回,最稳。 > 💬 **Q:`allowHostAccess` 和 `allowHostClassLookup` 都开了,为什么 `String(...)` 还要显式转换?** > > `randomUUID()` 返回的是 Java 的 `UUID` 对象(宿主对象)。`Value.asString()` 只能转换**字符串**;不 `String(...)` 包装,Java 侧拿到的 Value 是「宿主对象」而不是字符串,`asString()` 会抛异常。示例用 JS 的 `String()` 把宿主对象转成 JS 字符串,是常用的「收口」技巧。 ### 6.2 sort(null) 传的 null 到底是什么 `javaList.sort(null)` 里的 `null` 不是随便写的——它对应 Java 的 `ArrayList.sort(Comparator<? super E> c)`:**`null` 表示使用自然排序**(依赖元素自身的 `Comparable.compareTo`)。关键机制是:**JS 的 `null` 会自动映射为 Java 的 `null`**,所以传 `null` 等价于 Java 侧写 `javaList.sort(null)`,调用了"自然序"那套逻辑。这也说明**参数在跨语言边界的自动映射**(JS null → Java null)是真实发生的。 > 💬 **Q:`sort(null)` 里传 `null` 是什么意思?** > > 对应 Java 的 `ArrayList.sort(Comparator<? super E> c)`,`null` 表示使用自然排序(`Comparable` 的 `compareTo`)。JS 的 `null` 自动映射为 Java 的 `null`,所以等价于 Java 侧 `javaList.sort(null)`。 > 一句话:**宿主对象不是字符串,`asString()` 读不了,须先 JS `String(...)` 收口;`sort(null)` 的 null 是"自然排序",JS null 会自动映射成 Java null。** --- ## 7. 深水区:宿主访问的能力边界与安全心智 开关放开了、对象能调了,但"JS 到底能碰 Java 的哪些东西"仍是个需要划清界限的问题。把能力边界和安全取舍讲明白,你才不会在两个方向上踩坑(全不放开的无用 / 全放开的不安全)。 ### 7.1 能暴露的粒度:靠"开关"而非"全有全无" `HostAccess.ALL` 这个名字容易让人以为"全开"就是什么都给。其实它能被更细地裁剪:`HostAccess` 还提供 `build()` 让你**精挑哪些被暴露**——比如只允许调用无副作用的 `@HostAccess.Export` 标注方法。也就是说,**"开放宿主访问"这件事本身有颗粒度**,不是二进制开/关。示例图省事用了 `ALL`,真实生产若要处理不可信脚本,应当把暴露面收窄到最小。 ### 7.2 两个开关放大的风险要看清 把所有能力摊开,JS 一旦连上宿主,危险也不小: | 放开的开关 | 可能的危险 | |---|---| | `allowHostClassLookup`(全开 `true`) | JS 能 `Java.type` 出**几乎任何类**,包括能碰文件/进程/类的那些——等于给脚本发了半张"访问 JVM"的卡 | | `allowHostAccess` + 注入对象 | 只是给了"已暴露对象的访问权",范围相对可控,取决于你注入了什么 | 所以结论很明确:**只把"最小必要"的类/对象暴露出去**。比如 `allowHostClassLookup(s -> true)` 改成 `s -> s.startsWith("java.util.")`,就能把查找面锁在自己的包名下(这正是思考题 1 要你推敲的)。 ### 7.3 一个心法:宿主访问是"显式授权"的语言 把这两篇串成一句话:**Java 调用 JS 是默认就行的(JS 引擎本来就听你的),而 JS 调用 Java 是默认不行的(必须显式开开关)**。这个不对称正是安全设计——**反方向的调用越界,代价越大,就越要显式授权**。写任何"JS 回调 Java"的功能,默认心态都应该是"先别给,想清楚了再开"。 > 一句话:**宿主访问有细粒度(可用 Export 裁剪);类级开关全开风险大,应最小暴露;本质是"反方向调用越权代价大,须显式授权"。** --- ## 8. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 06 JS 调用 Java 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ 1. 默认沙箱锁死宿主:Java 对象不存在、方法不可调 │ │ 2. allowHostAccess(ALL) = 对象级,允许调已暴露方法 │ │ 3. allowHostClassLookup = 类级,允许 Java.type 查/建类 │ │ 4. putMember 注入对象 → JS 调 public 方法 / 操作集合 │ │ 5. Java.type 建类需要类级开关,Static 方法可直接调 │ │ 6. 宿主对象取回字符串前先 String(...) 收口 │ └──────────────────────────────────────────────────────┘ │ v 已经能让 JS 反叫 Java,下一步把"改数据还能写回"的共享打通 ──→ 下一篇:07 - 数据共享 Proxy ``` ## 思考题(自测) 1. `allowHostClassLookup(s -> true)` 里的 lambda 参数 `s` 是什么?如果改成 `s -> s.startsWith("java.util.")`,能拦到什么、放过什么? 2. 把 `ArrayList` 注入后,JS 的 `push` 能往 Java 的 `ArrayList` 加元素吗?这与你预想的"像 JS 数组"行为一致吗? 3. 只开 `allowHostAccess` 不开 `allowHostClassLookup` 时执行 `Java.type` 会怎样?为什么这两个开关需要彼此独立地设计?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入