知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 05 - Java 调用 JS > 这篇解决什么问题:有些逻辑天然写在 JS 里更合适——业务规则想存库热更新、易变的折扣状态判断不想每次改 Java 重编译、模板或表达式要复用 JS 生态。要把"Java 的数据送进 JS 算、再把结果拿回来",需要一条完整的通道。这篇把"Java 调用 JS"这条路讲透:为什么要把数据传进 JS、函数引用怎么拿、`execute` 怎么传参、一个关键的工具 `ProxyObject` 怎样让 Java 的 `Map` "看起来像" JS 对象,以及结果怎么按类型回读。 > 读完你会知道:Java 调用 JS 的三步走套路(拿函数引用 → execute 传参 → getMember/asXxx 取结果)、函数声明与表达式两种取引用的差别、`ProxyObject.fromMap` 为什么是 `Map → JS 对象` 的桥梁,以及结果回读时要怎么按 JS 类型挑选转换方法。 --- ## 1. 为什么要"把 Java 数据传进 JS" 先回答"为什么要把逻辑交给 JS"——这不是炫技,而是几个**真实诉求**: - **规则即数据**:业务规则以 JS 表达式的形式存库,Java 只负责注入事实(facts)并取回结果;改规则只是改一条记录,不用改代码。 - **逻辑下沉**:把易变的折扣计算、状态判断等逻辑放进 JS,改规则不用重编译 Java。 - **复用 JS 生态**:模板引擎、表达式解析库等,可以直接在 Java 里调。 这些诉求共同指向一条通道:**「Java 数据 → JS 参数 → JS 计算 → 结果回 Java」**。  *图:Java 调用 JS:传参、执行与回读* ``` ┌─────────────────────────────────────────────┐ │ Java 侧 │ │ │ │ Map(order) ──ProxyObject.fromMap──▶ Value │ │ │ │ │ ▼ │ │ processOrder.execute(Value) ◀─ getBindings│ │ │ .getMember │ │ ▼ │ │ Value(result) ──getMember("total")──▶ 4048.65│ └─────────────────────────────────────────────┘ ``` 这张链路图就是本讲的骨架:**进**(把 Map 数据包装送进 JS)→ **演**(JS 函数做计算)→ **出**(从 JS 返回的对象里把结果取回来)。 > 一句话:**"规则即数据 / 逻辑下沉 / 复用 JS 生态"三类需求,都要走通"Java 数据 → JS 计算 → 结果回 Java"这条单向通道。** --- ## 2. 两个先要立起来的关键机制 ### 2.1 函数引用的两种拿法 承接第 03 篇的"表达式 vs 声明",取一个 JS 函数引用有两条路,别混: | 定义方式 | 取引用方式 | |---|---| | `function processOrder(order) {...}`(声明语句) | `getBindings("js").getMember("processOrder")` | | `(order) => ...`(函数表达式) | `eval` 直接返回函数 `Value` | **声明**的函数注册进全局作用域,要用 `getBindings` 去全局取;**表达式**的函数作为 `eval` 的返回值直接到手。下面示例用的是前者(`function` 声明 + `getBindings`)。 ### 2.2 ProxyObject:让 Java 的 Map "看起来像" JS 对象 这是本讲最关键的机制。看 JS 函数体期望什么:它要访问 `order.id`、`order.price`、`order.quantity`——它期待 `order` 是一个**能用点号访问属性的对象**。而 Java 侧的数据是一个 `Map`。**裸的 `Map` 直接传进去,JS 用 `order.xxx` 是访问不到的**——因为 `Map` 不是 JS 意义上的属性对象。 所以要用一个桥接工具把它"翻译"成 JS 眼中的对象: ``` Java Map ──ProxyObject.fromMap──▶ JS 眼中的对象 {id, price, quantity, vip} order.id / order.price / order.quantity / order.vip ``` `ProxyObject.fromMap(map)` 返回一个"对 JS 表现为普通对象的代理"——JS 里写 `order.vip`、`order.price`,都会被转发到这个 Java `Map` 上去读。这是"数据从 Java 进 JS"的必经通道(写回的机制是第 07 篇的事,这里只讲单向传入)。 > 一句话:**函数引用分"声明走 getBindings / 表达式走 eval 返回"两种取法;`ProxyObject.fromMap` 让 Java 的 `Map` 表现为 JS 可点号访问的对象,数据才能喂进 JS 函数。** --- ## 3. 逐段拆解:一条完整链路怎么搭 ### 3.1 第一步:定义 JS 处理函数(声明式,注册到全局) ```java context.eval("js", """ function processOrder(order) { const discount = order.vip ? 0.9 : 1.0; return { id: order.id, total: order.price * order.quantity * discount, status: order.quantity > 100 ? 'bulk' : 'normal' }; } """); ``` 拆开看这段 JS 在等什么、算什么: 1. **它是声明语句**(`function` 声明),按 2.1 只会注册进全局作用域,函数体此刻不执行(承接第 03 篇)。 2. 函数体里到处是 `order.vip` / `order.id` / `order.price` / `order.quantity`——这些属性其实就是**即将从 Java 传入的 `Map` 的关键字**,通过 2.2 的 `ProxyObject` 桥接过去。 3. 它返回一个 JS 对象:`id`(透传订单号)、`total`(`价格 × 数量 × 折扣`)、`status`(数量超 100 判 `'bulk'`,否则 `'normal'`)。 ### 3.2 第二步:取出函数引用 ```java Value processOrder = context.getBindings("js").getMember("processOrder"); ``` 从全局作用域取 `processOrder`,得到的是一个包装成 `Value` 的函数(`canExecute() == true`)。这就是 2.1 的"声明走 getBindings"。 ### 3.3 第三步:Java Map 包装后传给 JS ```java Map<String, Object> order = Map.of( "id", "ORD-001", "price", 29.99, "quantity", 150, "vip", true ); Value result = processOrder.execute(ProxyObject.fromMap(order)); ``` 这是全链路最传神的一步,拆开看: 1. `Map.of(...)` 创建订单数据——注意 `vip` 是 `true`,所以 3.1 里 JS 的 `order.vip ? 0.9 : 1.0` 会取 `0.9` 折扣。 2. `ProxyObject.fromMap(order)` 把 `Map` 包装成 JS 眼中的对象。 3. `execute(...)` 把这个"代理对象"作为参数调用 JS 函数,返回 JS 算完后的结果(也是一个 `Value`——JS 返回的那个 `{id, total, status}` 对象)。 ### 3.4 第四步:从结果对象里取回各字段 ```java System.out.println("订单号: " + result.getMember("id").asString()); System.out.println("折后总额: " + result.getMember("total").asDouble()); System.out.println("订单类型: " + result.getMember("status").asString()); ``` `result` 是 JS 返回的对象,用 `getMember` 逐个取字段(呼应第 04 篇 Value 的对象操作),并按字段的实际 JS 类型挑转换方法: - `id` → `"ORD-001"`(JS 字符串)→ `asString()` - `total` → `29.99 × 150 × 0.9 = 4048.65`(JS 数字)→ `asDouble()` - `status` → 因 `quantity = 150 > 100` → `'bulk'`(JS 字符串)→ `asString()` ``` 订单号: ORD-001 折后总额: 4048.65 订单类型: bulk ``` > 一句话:**Java 调 JS = 定义/取函数引用 → `execute(ProxyObject.fromMap(map))` 传参 → `result.getMember(name).asXxx()` 回读结果,三步走完一条单向通道。** --- ## 4. 两个必须想清楚的问题 ### 4.1 不包 ProxyObject 直接传 Map,会怎样 这是最容易踩的坑。如果直接 `execute(order)` 传裸 `Map`: ```java Value result = processOrder.execute(order); // ❌ 裸 Map ``` JS 函数里的 `order.id` 会变成 **`undefined`**——因为 JS 无法用点号去访问 Java `Map` 的成员。`Map` 是一个"没有任何 JS 可见属性"的 Java 对象,`order.id` 自然取不到。 `ProxyObject` 存在的意义,就是让 Java 数据结构"**表现得像一个 JS 原生对象**",从而让 JS 能按属性访问。**凡是"JS 要按属性访问 Java 数据"的场景,都要用 Proxy 包装**(第 07 篇会讲写回,这里只讲单向传入)。 > 💬 **Q:不包 `ProxyObject.fromMap`,直接把 `Map` 传给 `execute` 会怎样?** > > 直接传裸 `Map`,JS 函数里的 `order.id` 会变成 `undefined`(JS 无法用点号访问 Java 的 `Map` 成员)。`ProxyObject` 的存在就是让 Java 数据结构**表现为 JS 原生对象**。凡是「JS 要按属性访问 Java 数据」的场景,都要用 Proxy 包装(第 07 篇会讲写回机制,这里只讲单向传入)。 ### 4.2 为什么 total 用 asDouble、id 用 asString 回到第 04 篇的核心:**JS 的数字没有 int/double 之分**。`29.99 × 150 × 0.9` 是浮点运算,得到的 `4048.65` 是一个数值,`asDouble()` 取出 `4048.65` 最合适;而 `id` 是 JS 字符串,`asString()` 取出 `"ORD-001"`。 一句话:**取什么方法,取决于字段在 JS 侧的实际类型**,不是在 Java 侧猜的。这再次印证了第 04 篇的"先判断、后转换"。 > 💬 **Q:为什么 `total` 用 `asDouble()`,而 `id` 用 `asString()`?** > > JS 的数字没有 int/double 之分(第 04 篇),`29.99 × 150 × 0.9` 是浮点运算,`asDouble()` 得到 `4048.65`;`id` 是 JS 字符串,`asString()` 得到 `"ORD-001"`。**用什么方法取,取决于字段的 JS 类型**。 > 一句话:**不包 Proxy 传裸 Map 会让 `order.id` 变 `undefined`;结果回读按字段 JS 类型挑转换方法(浮点数 asDouble、字符串 asString)。** --- ## 5. 深水区:传参的类型映射、嵌套数据与结果形态 把最常用的一条链路走通后,再看三个多半会遇到的进阶问题。它们共同指向一个原则:**跨语言传参与结果的类型,遵循"JS 那边是什么,Java 侧就按什么取"**。 ### 5.1 execute 传参时的"自动映射"边界 `execute(...)` 传参时,不是什么都自动转换。把常见情况理成一张表: | Java 传入 | JS 侧接收到 | 备注 | |---|---|---| | `int` / `double` / `boolean` / `String` | 对应的 `number` / `boolean` / `string` | 原始值/字符串直接可用 | | Java `List` | JS 数组(只读视角,靠数组协议) | 一般得包 Proxy 才好用 | | Java `Map` | **不能直接当对象用** | 必须 `ProxyObject.fromMap` 包装(本篇核心) | | Java 普通对象 | 成员由宿主访问开关决定 | 交第 06 篇 | 记住一个判断法:**JS 要按"属性名"访问 → 必须 Proxy;原始标量 → 直接进**。这条能避免大多数"为什么 JS 拿到的是 undefined"的排查。 ### 5.2 嵌套数据:Proxy.fromMap 不会自动递归包装 思考题 1 已经点出这个坑:如果数据里有个 `order.address.city` 这种**嵌套对象**,`ProxyObject.fromMap(order)` 只包装了**第一层**。JS 里 `order.address` 取到的仍是一个"没按属性暴露"的内层 Map,`order.address.city` 大概率还是 `undefined`。 要支持多层点号访问,得把内层也包装成 Proxy(或专门设计 JS 函数去扁平化取数)。这是一个"包装深度"的话题,第 07 篇讲递归包装会有更完整的展开——这里先埋下"Proxy 只铺一层"的认知。 ### 5.3 结果形态:函数可能返回对象、也返回数组 本篇函数返回的是 JS 对象(`getMember` 逐字段取)。但 JS 函数也可能返回**数组**:比如返回 `[值, 状态]` 这种。那时你要用第 04 篇的 `getArraySize()` / `getArrayElement(i)` 来遍历。**"先判断返回值形态(对象 vs 数组 vs 基本值),再选对应读取方式"**——这是所有结果回读的统一心法。 > 一句话:**传参要按"属性访问需 Proxy、标量直接进"判断;Proxy 只铺一层,嵌套需递归;结果回读先判"对象/数组/基本值"再选读取法。** --- ## 6. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 05 Java 调用 JS 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ 1. 三类诉求走向:规则即数据 / 逻辑下沉 / 复用 JS 生态 │ │ 2. 三步走 = 取函数引用 → execute 传参 → asXxx 取结果 │ │ 3. 函数引用:声明走 getBindings,表达式走 eval 返回 │ │ 4. ProxyObject.fromMap = Map → JS 对象 的桥梁 │ │ 5. 不包 Proxy 传裸 Map → order.id 变 undefined │ │ 6. 结果按 JS 类型挑 asString / asDouble / asInt │ └──────────────────────────────────────────────────────┘ │ v 已经能把 Java 数据送进 JS 算并取回,下一步反方向让 JS 调用 Java ──→ 下一篇:06 - JS 调用 Java ``` ## 思考题(自测) 1. 如果订单数据里有嵌套对象(如 `order.address.city`),`ProxyObject.fromMap` 能直接处理吗?需要怎么包装才能让三层点号都读得到? 2. 如果 JS 函数返回的是一个数组(如 `[...]`),Java 侧怎么遍历这个结果?(提示:第 04 篇的 `getArraySize` / `getArrayElement`) 3. `processOrder` 每次调用都要 `getBindings().getMember("processOrder")` 一次,能不能把它取出的 `Value` 缓存起来复用?多次调用(传入不同数据)会有什么差异?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入