知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 07 - 数据共享 Proxy > 这篇解决什么问题:第 05 篇让 JS 改了数据,Java 侧看不见——因为它只是"单向传进去算"。但真实业务往往要**双向**:JS 改了字段,Java 立马能读到。`ProxyObject` / `ProxyArray` 正是为此而生——它们**不是拷贝**,而是**同一份内存的代理**:JS 侧一改,原 `Map` / `List` 就变。这篇把"数据共享"讲透:Proxy 如何做到同内存双向可见、写回的前提是什么(可变容器)、`ProxyArray` 为何定长不支持 push、以及为什么验证 JS 写入的字符串要从 JS 侧回读。 > 读完你会知道:`ProxyObject.fromMap` / `ProxyArray.fromList` 同内存代理的本质、写回前必须用可变容器(`HashMap`/`ArrayList`)的坑、`ProxyArray` 定长限制的根因、以及跨语言字符串写入后应该从哪里回读最直观。 --- ## 1. 从"单向传入"升级到"双向共享" 第 05 篇讲的链路是单向的:Java 数据 → 送进 JS → 算完取回。但当你要的是"**JS 改了字段,Java 侧真能读到**"时,单向就不够了——它需要一条**双向通道**。 `ProxyObject.fromMap(map)` 返回的代理**不复制数据**,它把读写操作**转发**到原 `Map`;`ProxyArray.fromList(list)` 同理,把下标读写**转发**到原 `List`。这就是"**同一份内存**"的含义:  *图:Proxy:同一份内存,双向可见* ``` Java 侧 JS 侧 ┌────────────────────┐ ┌────────────────────┐ │ Map config │◀──────▶│ config 对象 │ │ {timeout:3000, │ 读写 │ config.debug=true │ │ retries:3, │ 转发 │ 立即写回 Map │ │ debug:false} │ │ │ └────────────────────┘ └────────────────────┘ ┌────────────────────┐ ┌────────────────────┐ │ List items │◀──────▶│ items 数组 │ │ [apple,banana, │ 读写 │ items[0]='avocado'│ │ cherry] │ 转发 │ 立即写回 List │ └────────────────────┘ └────────────────────┘ ``` 关键点在于**双向的"◀──▶"**:不是 JS 拿了一份快照自己玩,而是每次 `config.debug = true`、`items[0]='avocado'` 都被代理转发回原容器,Java 侧立刻可见。 > 一句话:**`ProxyObject`/`ProxyArray` 不是拷贝而是同内存代理,读写直接转发到原 `Map`/`List`,所以 JS 一改 Java 立刻能见。** --- ## 2. 两条"映射"规则先立住 ```text Java Map -> ProxyObject.fromMap(map) -> JS 里像普通对象访问(可读可写) Java List -> ProxyArray.fromList(list) -> JS 里像普通数组操作(可读可写) ``` 对照记:**Map ↔ 对象**(点号访问、可读写),**List ↔ 数组**(下标操作、可读写)。三件工具的作用具体是: | Java 容器 | 包装工具 | JS 侧形态 | 读写 | |---|---|---|---| | `Map` | `ProxyObject.fromMap(map)` | 普通对象 | 可读可写 | | `List` | `ProxyArray.fromList(list)` | 普通数组 | 可读可写 | 这延续了第 05 篇的 `ProxyObject`,但把能力从"读"延展到了"写"——这就是本篇的增值点。 > 一句话:**Map 走 `ProxyObject.fromMap` 变 JS 对象、List 走 `ProxyArray.fromList` 变 JS 数组,两者都可读可写、写进原容器。** --- ## 3. 两个本机实测踩出来的坑 动手之前,先记住这两个在真实跑代码时会遇到的坑——它们决定了"写回"这件事不是无条件的。 ### 3.1 坑一:ProxyArray 是定长的,不支持 push 扩容 `fromList` 出来的数组,**长度固定为 List 的 size**。所以在 JS 里 `items.push(...)` 会**报错**——因为 push 是在"扩容",而代理没有向你承诺这种能力。要加元素,得在 Java 侧的 `List` 层面做(比如先 `list.add(...)` 再让 JS 用新 size)。这条在踩坑篇有明细,这里先立住"定长、不可 push"的认知。 ### 3.2 坑二:JS 写入的字符串在 Java List 里显示为 TruffleString JS 侧 `items[0]='avocado'` 写进去的,是 **JS 的字符串对象(`TruffleString`)**,不是 Java `String`。这意味着你如果直接去 Java 的 `List` 里 `get(0)`,拿到的可能不是能直接 `println` 的字符串,而是一个"TruffleString 对象"。示例因此选了一个聪明办法——**从 JS 侧回读**:用 `context.eval("js", "items.join(' | ')")` 在 JS 里把数组拼接成字符串再读回,最直观也最符合 JS 语义。 > 一句话:**写回有俩前提坑:ProxyArray 定长不支持 push;JS 写入的字符串在 Java List 里是 TruffleString,验证时从 JS 侧回读更直观。** --- ## 4. 逐段跑起来:双向共享的完整链路 ### 4.1 把可变 Map 包成能"写回"的代理 ```java Map<String, Object> config = new java.util.HashMap<>(Map.of( "timeout", 3000, "retries", 3, "debug", false )); context.getBindings("js").putMember("config", ProxyObject.fromMap(config)); ``` 注意这里**特意用了 `new HashMap<>(Map.of(...))`**——为什么绕这一圈?因为 `Map.of(...)` 返回的是**不可变 Map**,任何写操作(包括后面 Proxy 转发来的 `config.debug = true`)都会抛 `UnsupportedOperationException`。包一层可变 `HashMap`,写回才有了落脚点。**这是数据可写回的第一个前提:原对象必须可变**。 ``` Map.of(...) ← 不可变,写不了 new HashMap<>(Map.of(...)) ← 可变,JS 改写才能写回 ``` ### 4.2 把可变 List 包成可写的数组代理 ```java List<Object> items = new ArrayList<>(List.of("apple", "banana", "cherry")); context.getBindings("js").putMember("items", ProxyArray.fromList(items)); ``` 与 4.1 同样道理:`List.of(...)` 不可变,包成可变 `ArrayList`。要写回,容器必须先可写。 ### 4.3 让 JS 读取 + 修改(同一份内存生效) ```java context.eval("js", """ console.log('timeout = ' + config.timeout); console.log('retries = ' + config.retries); config.debug = true; // 写回 Java Map items[0] = 'avocado'; // 修改 Java List 元素 items[2] = 'coconut'; """); ``` 这四行 JS 干的事,全部会转发回 Java 原容器: - `config.timeout` / `config.retries`:JS **读** ProxyObject 的属性 → 转发到 Map → 输出 `timeout = 3000`、`retries = 3`。 - `config.debug = true`:JS **写**属性 → 转发到 Map → Java 侧 `config.get("debug")` 将变 `true`。 - `items[0] = 'avocado'` / `items[2] = 'coconut'`:JS 按下标**写数组** → 转发到 List。 ``` timeout = 3000 retries = 3 (这三行来自 JS 的 console.log,读 Proxy 的属性) ``` ### 4.4 Java 侧观察修改结果 ```java System.out.println("config.debug 已被 JS 改为: " + config.get("debug")); System.out.println("items 被 JS 修改后: " + context.eval("js", "items.join(' | ')").asString()); ``` 这是"双向共享是否成立"的验证,两条路分别解决两个不同的问题: 1. `config.get("debug")`:**直接从原 Map 读**——得到 JS 写回的 `true`,**证明写回成功**(同一个内存对象被改了)。 2. `items.join(' | ')`:因为 JS 写入的元素是 `TruffleString`,从 Java 侧直接读 `List` 会看到对象而非字符串,所以改从 **JS 侧拼串回读**,得到 `avocado | banana | coconut`。 ``` config.debug 已被 JS 改为: true items 被 JS 修改后: avocado | banana | coconut ``` > 一句话:**把可变容器包成 Proxy 注入后,JS 一切读写都转发回原对象;验证写回时,布尔直接在原 Map 读、字符串类从 JS 侧 join 回读。** --- ## 5. 两个必须想清楚的问题 ### 5.1 为什么 config.debug 能直接读 true,而 items 要从 JS 侧回读 写回机制**本身一样**(都写进了原对象),区别在**取值方式**,根子在类型: | 字段 | JS 写入 | 写回后 Java 侧形态 | Java 侧读取 | |---|---|---|---| | `debug` | `true` | 转回原生 `Boolean` | `config.get("debug")` 直接得 `true` | | `items[0]` | `'avocado'` | 存入的是 **TruffleString**(不是 Java String) | `list.get(0)` 拿到的是对象,println 只能显示类型 | 所以示例才用 `items.join(' | ')` 从 JS 侧拼串回读——最直观、最符合 JS 语义,绕过了"直接读 List 看到的是对象"这个认知障碍。 > 💬 **Q:为什么 `config.debug` 能用 Java 侧直接读到 true,而 items 要从 JS 侧回读?** > > 写回机制本身是一样的(都写进了原对象)。区别在**取值方式**: > > - `debug` 是 `boolean`,JS 写 `true` 后转回 Java `Boolean` 是原生类型,`config.get("debug")` 直接得到 `true` > - `items[0] = 'avocado'` 写入的是 **JS 字符串(TruffleString)**,Java 侧 `list.get(0)` 拿到的是 TruffleString 对象,`println` 只能显示类型信息 > > 所以在示例里,验证数组修改用 `items.join(' | ')` 从 JS 侧拼串回读,最直观也最符合 JS 语义。 ### 5.2 为什么必须用 new HashMap<>(Map.of(...)),而不是直接 Map.of 这是写回场景最容易踩、也最坑的一点。**`Map.of(...)` 返回不可变 Map**——任何写操作(包括 ProxyObject 转发来的 `config.debug = true`)都会抛 `UnsupportedOperationException`。共享的前提本就是"多个视角操作同一份内存",这份内存却不可变,写回自然崩。**要用 Proxy 做写回,原容器必须可变**,所以包一层 `HashMap` / `ArrayList`。 > 💬 **Q:为什么这里用 `new HashMap<>(Map.of(...))` 而不是直接 `Map.of(...)`?** > > `Map.of` 返回**不可变** Map,任何写操作(包括 ProxyObject 转发来的 `config.debug = true`)都会抛 `UnsupportedOperationException`。要用 Proxy 做写回,原对象必须可变——这是本系列里最容易踩的坑之一。 > 一句话:**写回同一份内存时,"布尔原样可回读、字符串是 TruffleString 要从 JS 侧回读";而 `Map.of`/`List.of` 不可变,写回必须用 `HashMap`/`ArrayList` 包一层。** --- ## 6. 深水区:Proxy 的本质、嵌套与并发的边界 双向共享很爽,但"同一份内存"的代价也要看清。这一节把 Proxy 的三个边界问题讲透,让你既用得上、又不被它的副作用坑到。 ### 6.1 Proxy 到底做了什么:是"转发器"而不是"镜子" `ProxyObject`/`ProxyArray` 的实现本质是一个**转发器**:它实现了一组"取值/设值"的回调接口,JS 侧的每次读、写,都被翻译成对底层 `Map`/`List` 的一次 `get`/`put`/`set`。它不是把数据"镜像"一份给 JS,而是"**所有操作实时转发回原对象**"。这也是为什么"JS 一改、Java 立刻见"——**根本没有第二份副本,只有原对象一份**。 ### 6.2 嵌套结构:Proxy 默认只铺一层,深层要递归处理 思考题 1 点出的坑要在这里定论:`ProxyObject.fromMap(config)` 只把**第一层**包装成 JS 对象。如果 `config` 里还有个 `List`(比如 `config.tags`),JS 侧 `config.tags` 拿到的是"没按数组协议暴露"的内层对象,`config.tags[0]` 大概率取不到。要支持任意深度的数据共享,得**递归地把每一层都包装成对应 Proxy**——这是一个不算难但容易漏的设计,做数据桥接时一定要记得"包装深度 = 数据深度"。 ### 6.3 同一份内存的另一面:并发一致性 双向共享的便利背后,藏着一个代价:**没有任何锁**。JS 侧正在 `config.debug = true` 的同一时刻,Java 主线程可能正在 `config.get("debug")`——看到的是旧值还是新值,取决于执行顺序,也就是**线程安全问题**。共享越深,并发风险越大。缓解思路有三条:共享结构仅在单线程下读写、或多租户业务各自独立 Proxy、或对关键共享区加同步。**记住一句:方便的双向可见,不等于是线程安全的**。 > 一句话:**Proxy 是"实时转发"而非"镜像复制";嵌套数据要递归铺 Proxy;双向共享无锁,并发一致性得自己保证。** --- ## 7. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 07 数据共享 Proxy 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ 1. Proxy = 同内存代理,JS 修改直接写回 Java 原对象 │ │ 2. 写回前提:原对象可变(HashMap/ArrayList 包一层) │ │ 3. ProxyArray 定长,不支持 push 扩容 │ │ 4. JS 写入 List 的是 TruffleString,验证从 JS 侧回读 │ │ 5. 布尔可原 Map 直接读,字符串从 JS join 回读 │ │ 6. Map↔对象、List↔数组 双映射 │ └──────────────────────────────────────────────────────┘ │ v 已经把"改数据还能写回"的双向共享打通,下一步处理跨语言的异常 ──→ 下一篇:08 - 异常处理 ``` ## 思考题(自测) 1. 如果 `config` 里嵌套一个 List(如 `config.tags`),`ProxyObject.fromMap` 会自动把它变成可操作的数组吗?还是需要递归地手动包装? 2. 双向共享同一份内存带来了便利,也带来了并发问题:JS 侧修改的同时 Java 侧正在读,如何保证数据一致性? 3. `ProxyArray` 不支持 `push`,如果业务需要"不定长"的数据共享(JS 侧能增删元素),有什么替代方案?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入