WCPoint、WebPageClient和Accessor类源码

WCPoint 源码深度解析:WebKit 与 JavaFX 之间的"坐标原子"

WCPointcom.sun.webkit.graphics 包中一个极其轻量级的数据传输对象(DTO) 。它比我们之前分析过的 WCRectangle 还要简单,但它的存在却揭示了 JavaFX WebView 底层渲染管道中一个重要的设计原则:"可变与不可变的分离"

它的核心使命:在 WebKit(C++)与 JavaFX(Java)之间传递单个二维坐标点的数据,用于描述路径锚点、变换原点、鼠标点击位置、渐变起点/终点等。


一、类声明与设计意图

java 复制代码
public final class WCPoint {
    final float x;
    final float y;
    // ...
}
  • final 修饰:不可继承,表明它是一个纯粹的值对象(Value Object)。
  • 字段是 final :这是它和 WCRectangle 最本质的区别!WCRectanglex, y, w, h 是包可见且可变的setBounds 会修改它们),而 WCPointx, y不可变的(Immutable)
  • 包可见性(无 private :允许同一包内的其他类(如 WCGraphicsContextWCTransform)直接访问 xy 字段,省去 getter 调用的开销。同时,final 保证了外部即使直接读取字段,也无法修改它们。

二、为什么 WCPoint 是不可变的,而 WCRectangle 是可变的?

这是一个极其精妙的性能与安全权衡设计:

可变性 设计理由
WCRectangle 可变(Mutable) 在渲染循环中,矩形区域(如脏区、裁剪区)被频繁修改和复用 。例如,在滚动过程中,同一个 WCRectangle 对象需要反复调用 translate 来更新坐标。如果每次都创建新对象,会瞬间压垮 GC(垃圾回收器)。因此,允许修改内部状态(setBoundstranslate)是为了极致的内存性能。
WCPoint 不可变(Immutable) 点数据通常是作为方法参数 (如 drawLine(x1, y1, x2, y2))或返回值 (如 getMinX)传递。它极少需要原地修改。使用 final 字段确保了它在多线程环境(JNI 回调可能来自渲染线程)下的绝对线程安全,且作为 HashMap 的键时哈希值不会改变。

一句话总结:矩形是"工具",需要反复移动和变形(可变);点是"坐标",只需要精确地读出,然后丢弃(不可变)。


三、构造器与 Getter 方法

java 复制代码
public WCPoint(float x, float y) {
    this.x = x;
    this.y = y;
}
public float getX() { return x; }
public float getY() { return y; }
public int getIntX() { return (int)x; }
public int getIntY() { return (int)y; }
  • 全参构造器:唯一构造方式,确保了对象创建后状态不可变。
  • getIntX / getIntY :与 WCRectangle 一样,提供整数版本。这在调用 JavaFX 的 Node 坐标(如 setLayoutX)或进行像素级碰撞检测时非常方便,避免了调用方反复强转。

四、与之前分析过的类的区别与联系

1. 与 WCRectangle 的对比(最接近的"亲戚")

维度 WCPoint WCRectangle
数据量 2 个 floatx, y 4 个 floatx, y, w, h
可变性 不可变final 字段) 可变 (字段非 final,有 setFrametranslate
用途 表示点、向量、偏移量 表示区域、边界、视口
线程安全 天然线程安全 需外部同步(通常仅在 JNI 回调线程使用)
内存分配 在 JNI 回调中大量创建,但轻量且快速回收 对象复用,修改内部状态避免频繁分配

2. 与 Ref 的区别(资源 vs 数据)

  • Ref资源句柄 ,有 id 和引用计数,需要 WCGraphicsManager 管理生命周期。
  • WCPoint纯数据 ,无生命周期管理,不涉及 refMap,用完即抛。

3. 与 WCGraphicsContext 的协作

WCGraphicsContext 中,多个方法直接使用 WCPoint 作为参数:

java 复制代码
public abstract WCGradient createLinearGradient(WCPoint p1, WCPoint p2);
public abstract void drawPattern(..., WCPoint phase, ...);

这里 WCPoint 承载了渐变起止点和平铺相位等几何信息,是绘制指令中"位置"语义的标准载体。


五、在 JNI 管道中的角色(与 C++ 的协作)

在 C++ 的 WebKit 层,存在一个完全对应的结构体(通常是 WebCore::FloatPoint)。当 C++ 层需要传递一个点坐标给 Java 时,JNI 层会直接创建一个 WCPoint 对象(或将两个 float 分别传递,由 GraphicsDecoder 组装)。

例如,fwkGetScreenRect 可能返回 WCRectangle,而某些 JNI 回调会直接返回 WCPoint(如获取鼠标在文档中的偏移量)。

这种"一对一"的映射保证了数据传输的低失真。 C++ 的 FloatPoint 对应 Java 的 WCPoint,C++ 的 FloatRect 对应 Java 的 WCRectangle


六、为什么不直接使用 javafx.geometry.Point2D

这个问题与 WCRectangle 为什么不使用 Rectangle2D 的原因完全一致:

  1. 精度匹配 :WebKit 使用 float,而 javafx.geometry.Point2D 使用 double。使用 float 避免了跨语言边界的精度转换开销。
  2. 性能优先Point2D 是不可变的(使用 double),且位于公共 API 包中,内部分配和缓存机制较复杂。WCPoint 是专门为高频 JNI 回调设计的最简 DTO。
  3. 依赖隔离com.sun.webkit 包避免对公共 API 包的过度依赖,保持内部实现的自洽性。

七、总结

WCPoint 是 WebView 渲染管道中最低级、最简洁的"几何原子":

  • 它是不可变的,代表"此刻在这里"的快照,适合作为参数在栈上快速传递。
  • 它是 WCRectangle 的"对立面":矩形是可变的工具(便于复用),点是不可变的信息(便于安全传递)。
  • 它与 Ref 无关,不涉及任何资源生命周期管理,纯粹的数学载体。
  • 它贯穿整个图形管道 ,从 C++ 布局引擎的 FloatPoint,到 JNI 回调,再到 WCGraphicsContext 的渐变创建、模式平铺,无处不在。

一句话人话总结:

WCPoint 是 WebKit 发给 Java 的一条"坐标纸片"。它简单到无法再简单------就两个小数。正因为简单,它可以在程序里飞来飞去(跨线程、跨语言边界),而不需要担心被修改或产生内存负担。它和 WCRectangle 的区别,就像"图钉"(点)和"纸盒"(矩形)的区别------图钉用来标记位置,纸盒用来框住区域,各司其职。

WebPageClient 接口源码深度解析:WebKit 与 JavaFX WebView 之间的"外交大使"

WebPageClient<T>com.sun.webkit 包中的一个核心接口 ,它在 JavaFX WebView 的底层架构中扮演着 "桥梁" 的角色。如果说 WCGraphicsManager 是图形资源的"总调度中心",WCRenderQueue 是绘制命令的"快递员",那么 WebPageClient 就是"外交官" ------它负责让 WebKit 引擎(通过 WebPage)能够与 JavaFX 的宿主容器(WebView)进行通信,处理那些与图形绘制无关与宿主环境紧密相关的事务,如光标样式、焦点、工具提示、屏幕信息、坐标转换、控制台消息等。

不是 一个可见的类,而是一个契约。它定义了 WebKit 需要从宿主环境中获取哪些服务,以及如何向宿主环境发送通知。


一、接口声明与设计意图

java 复制代码
public interface WebPageClient<T> {
    // 方法定义...
}
  • 泛型参数 T :代表宿主容器的类型。在 JavaFX 中,这个 T 就是 WebView(即 WebPageClientImpl 实现了 WebPageClient<WebView>)。
  • 位置 :位于 com.sun.webkit 包(内部 API),与 WebPage 类紧密耦合。
  • 设计模式"依赖倒置原则(Dependency Inversion)" 的完美体现。WebPage(高层模块)不直接依赖具体的 WebView(低层模块),而是依赖 WebPageClient 这个抽象接口。这使得 WebKit 核心代码可以独立于 JavaFX 的 UI 控件,便于测试和扩展。

二、方法逐一详解(宿主服务的"菜单")

这个接口定义了 WebKit 可以使用的所有"宿主服务"。我们按功能分组来剖析:

1. 用户交互反馈(光标、焦点、提示)

setCursor(long cursorID)

  • 作用 :通知宿主(WebView)改变鼠标光标样式。
  • 背景 :当 CSS 的 cursor 属性变化,或悬停在不同元素上时,WebKit 会计算出对应的光标 ID(如 IDC_HANDIDC_TEXT)。
  • 调用链WebKitWCFrameView.setCursor(long)pageClient.setCursor(cursorID)WebView 设置 Cursor

setFocus(boolean focus)

  • 作用:请求宿主获取或释放键盘焦点。
  • 背景 :当 HTML 中的 input 元素通过 focus() 方法请求焦点,或用户点击页面空白区域时。
  • 调用链WCFrameView.requestFocus()pageClient.setFocus(true)WebView.requestFocus()

transferFocus(boolean forward)

  • 作用:将焦点转移给宿主中的下一个/上一个可聚焦元素(Tab 键切换)。
  • 背景:当用户在页面内按 Tab 键,且当前焦点在最后一个可聚焦元素时,需要将焦点移出 WebView。
  • 实现 :通常调用 WebView 的父容器的 traverse 方法。

setTooltip(String tooltip)

  • 作用 :显示或更新 HTML 元素的 title 属性或 tooltip 提示。
  • 背景 :当鼠标悬停在元素上时,WebKit 会解析 title 属性并调用此方法。
  • 实现 :JavaFX 的 Tooltip 机制。

2. 宿主环境信息(屏幕参数)

WCRectangle getScreenBounds(boolean available)

  • 作用:获取屏幕的尺寸(全屏或可用区域,即排除任务栏等)。
  • 背景 :WebKit 需要知道屏幕尺寸来计算 window.screen 对象、CSS 媒体查询(@media)等。
  • 返回值WCRectangle(几何数据),表示屏幕区域的左、上、宽、高。

int getScreenDepth()

  • 作用:获取屏幕颜色深度(每像素位数,如 24 位或 32 位)。
  • 背景:影响图像解码和颜色处理策略。

3. 坐标转换(坐标系之间的"翻译")

WCPoint screenToWindow(WCPoint ptScreen)

  • 作用:将屏幕坐标系(相对于显示器左上角)转换为窗口坐标系(相对于 WebView 控件的左上角)。
  • 用途:处理鼠标点击、触摸事件的坐标映射,或弹出菜单的定位。

WCPoint windowToScreen(WCPoint ptWindow)

  • 作用:反向转换,从窗口坐标系到屏幕坐标系。
  • 用途 :当 WebKit 需要显示一个原生弹出框(如 <select> 下拉菜单)时,需要知道其在屏幕上的绝对位置。

4. 容器引用

T getContainer()

  • 作用 :返回宿主容器对象本身。在 JavaFX 中,返回 WebView 实例。
  • 用途 :某些底层操作需要直接访问 WebView 的字段或方法(如获取 Skin、触发布局)。

5. 页面后退缓冲(Back Buffer)

WCPageBackBuffer createBackBuffer()

  • 作用:创建一个离屏缓冲区,用于在页面导航时保存当前页面的快照(实现"滑动后退"动画效果)。
  • 背景:当用户按下后退按钮时,WebKit 可能需要显示上一个页面的缩略图。

boolean isBackBufferSupported()

  • 作用 :询问宿主是否支持页面后退缓冲。如果支持,WebKit 会调用 createBackBuffer 并合理使用;否则,放弃该优化。

6. 开发者工具支持

void addMessageToConsole(String message, int lineNumber, String sourceId)

  • 作用 :将 WebKit 控制台(console.logconsole.error 等)的消息转发给宿主,以便显示在 JavaFX 应用程序的日志或调试面板中。
  • 背景:帮助开发者调试 Web 页面。

void didClearWindowObject(long context, long windowObject)

  • 作用 :当 JavaScript 的 window 对象被清除(如页面刷新或导航)后,通知宿主,以便重新注入 Java 对象(通过 JSObject)。
  • 背景 :允许 Java 代码向 Web 页面暴露 Java 方法(window.java 对象)。

三、与之前分析过的类的深度协作关系

1. 与 WCWidget / WCFrameView 的协作

WCWidget 中,我们看到了受保护的钩子方法:

java 复制代码
protected void setCursor(long cursorID) {}
protected void requestFocus() {}

WCFrameView 中,这些方法被重写:

java 复制代码
@Override protected void setCursor(long cursorID) {
    WebPageClient pageClient = getPage().getPageClient();
    if (pageClient != null) {
        pageClient.setCursor(cursorID);
    }
}

协作链条

复制代码
WebKit (C++) → JNI → fwkSetCursor() → WCFrameView.setCursor() → WebPageClient.setCursor()
  • WCFrameView 持有 WebPage 的引用,通过 getPage().getPageClient() 获得 WebPageClient 实例。
  • 它将具体的行为委托给了 WebPageClient,从而将"如何响应"与"由谁响应"解耦。

2. 与 WebPage 的协作

WebPage 类(在 com.sun.webkit 包中)是 WebPageClient 的"持有者":

java 复制代码
public class WebPage {
    private WebPageClient pageClient;
    public WebPageClient getPageClient() { return pageClient; }
    // ...
}
  • WebPage 在构造时接收一个 WebPageClient 实例(通常由 WebView 通过 WebPageClientImpl 传入)。
  • 所有 WCWidget 子类通过 getPage().getPageClient() 访问该客户端。

3. 与 WebView 的协作(通过 WebPageClientImpl

WebPageClientImplWebPageClient<WebView> 的具体实现,位于 com.sun.javafx.webkit 包(JDK 未包含,属于 JavaFX 内部实现)。它持有 WebView 的引用(通常通过 Accessor 模式间接持有),并实现了所有接口方法:

java 复制代码
public final class WebPageClientImpl implements WebPageClient<WebView> {
    private final Accessor accessor; // 间接持有 WebView
    @Override public void setCursor(long cursorID) {
        WebView view = accessor.getView();
        if (view != null) {
            // 将 cursorID 映射为 JavaFX Cursor
            view.setCursor(cursorManager.getCursor(cursorID));
        }
    }
    // ...
}

协作流程

  1. WebView 创建时,在内部初始化 WebPage 并传入一个 WebPageClientImpl 实例。
  2. WebPage 保存该实例。
  3. WCFrameView 需要改变光标时,调用 pageClient.setCursor(...),最终调用 WebView.setCursor(...),更新场景图的光标样式。

四、WebPageClientImpl 为何不在 JDK 中?

WebPageClientImpl 位于 com.sun.javafx.webkit 包,而 com.sun.webkit 包是 JDK 内部包。这种设计是为了隔离实现细节

  • com.sun.webkit :包含与 WebKit 核心引擎(C++)交互的抽象层(如 WebPageWCWidgetWCGraphicsManager),这部分代码对 JDK 可见。
  • com.sun.javafx.webkit :包含 JavaFX 具体平台的集成代码,如 WebView 的皮肤、事件处理、WebPageClientImpl 等。这部分代码不属于 JDK,而是 JavaFX 的一部分(通常随 JavaFX 运行时一起提供)。

这种分层保证了 JDK 中的 WebKit 抽象层不与具体的 UI 控件耦合,提高了模块化程度。如果你在使用 JavaFX,这些类会在运行时存在;如果你只使用纯 JDK(不引入 JavaFX),这些类自然不会被加载。


五、总结:WebPageClient 在全局架构中的角色

维度 角色
本质 定义了 WebPage 与宿主容器之间的通信协议
实现者 WebPageClientImpl(在 JavaFX 运行时中),连接 WebPageWebView
调用方 WCWidgetWCFrameView 以及其他需要宿主服务的内部类
方法覆盖 覆盖了光标、焦点、工具提示、屏幕信息、坐标转换、控制台、JS 注入等
设计模式 依赖倒置策略模式(宿主的策略通过此接口注入)

一句话人话总结:

WebPageClient 是 WebKit 引擎向 JavaFX 宿主"求助"的官方渠道。当引擎想改变光标、请求焦点或显示工具提示时,它不会直接操作 WebView(因为引擎压根不知道 WebView 是什么),而是通过这个接口说"我需要服务 X",由接口的实现者(WebPageClientImpl)去调用 WebView 的对应方法。这让引擎变成了一个"只提要求、不问执行"的甲方,保证了核心逻辑(WebKit)和 UI 框架(JavaFX)的松耦合。

Accessor 源码深度解析:JavaFX WebView 模块间的"秘密通道"

Accessorcom.sun.javafx.webkit 包中的一个抽象类 。它在 JavaFX WebView 的架构中扮演着一个极其特殊的角色------它是公开的 WebEngine / WebView API 与内部 WebPage / WebKit 底层实现之间的"后门(Backdoor)"或"访问器(Accessor)"。

如果 WebPageClient 是 WebKit 向宿主(WebView)"求助"的官方电话线 ,那么 Accessor 就是宿主(WebView)向 WebKit 核心(WebPage)主动"窥探"的万能钥匙**。** 它打破了模块间的强依赖隔离,让 JavaFX 的公共 API 层能够触碰到内部实现的核心数据。


一、类声明与核心设计意图

java 复制代码
public abstract class Accessor {
    // ...
}
  • 位置 :位于 com.sun.javafx.webkit 包(JavaFX 内部集成层),而非 com.sun.webkit(JDK 内部 WebKit 抽象层)。
  • 抽象类 :由具体的实现类(通常是 WebView 内部持有的 Accessor 实例)提供实现。
  • 核心目的 :提供一种安全且受控 的方式,让 WebEngineWebView 的公共 API 方法能够获取或操作私有的 WebPage 对象、管理子节点、监听视图变化,而无需将 WebPage 类暴露给公共 API(javafx.scene.web 包)。

二、静态部分(PageAccessorgetPageFor):模块边界的"破壁人"

java 复制代码
public static interface PageAccessor {
    public WebPage getPage(WebEngine w);
}
private static PageAccessor pageAccessor;
public static void setPageAccessor(PageAccessor instance) {
    Accessor.pageAccessor = instance;
}
public static WebPage getPageFor(WebEngine w) {
    return pageAccessor.getPage(w);
}

这是 Accessor 最精妙的设计------一个静态的"服务注册器"。

  • 问题背景 :JavaFX 的公共 API 包(javafx.scene.web)包含了 WebEngineWebView,但它们不能直接依赖 内部包(com.sun.webkit)中的 WebPage 类,否则会破坏模块化封装。
  • 解决方案Accessor 提供了一个静态的"钩子"。
    1. 在 JavaFX 运行时启动时(通常是 WebEngine 初始化时),内部代码会调用 Accessor.setPageAccessor(instance),向 Accessor 注册一个 PageAccessor 实例。
    2. 这个 PageAccessor 实例持有从 WebEngine 到其内部 WebPage 的映射关系。
    3. 当公共 API 代码需要获取 WebPage 时(例如在 WebView 的皮肤或事件处理中),它调用 Accessor.getPageFor(webEngine),通过注册好的访问器获取 WebPage 引用。

这种设计完美地绕过了 Java 的编译时依赖检查,实现了"依赖倒置"中的"接口隔离"。


三、实例方法(Accessor 的具体能力)

java 复制代码
public abstract WebEngine getEngine();
public abstract WebView getView();
public abstract WebPage getPage();
public abstract void addChild(Node child);
public abstract void removeChild(Node child);
public abstract void addViewListener(InvalidationListener l);

1. 核心对象获取

getEngine() / getView() / getPage()

  • 返回 Accessor 所关联的核心组件。通常,一个 Accessor 实例就代表着一个 WebView 及其内部的 WebEngineWebPage
  • 用途 :允许其他内部组件(如 WebPageClientImplGraphicsDecoder、事件处理器)同时获得这三个对象的引用,方便跨层调用。

2. 子节点管理(addChild / removeChild

这是 WebView 能够"弹出"原生控件(如 <select> 下拉框或 <input type="file"> 对话框)的关键机制。

  • 背景 :WebKit 在渲染页面时,有时需要弹出非网页内容的 UI 组件(例如 HTML 的 <select> 下拉菜单在 Windows 上可能显示为原生 ComboBox)。
  • 实现 :WebKit 内部通过 WCWidgetWebPageClient 通知 JavaFX 层创建对应的 Node(如 PopupComboBox),然后调用 Accessor.addChild(child) 将这个 Node 直接添加到 WebViewchildren 列表中。
  • 区别 :这与 WebView 通过 getChildren().add() 添加普通节点不同,AccessoraddChild 内部会确保该节点在 WebView 的布局和事件体系中具有特殊优先级(如悬浮于网页之上)。

3. 视图监听(addViewListener

java 复制代码
public abstract void addViewListener(InvalidationListener l);
  • 作用 :注册一个 InvalidationListener,用于监听 WebView 的"视图状态"变化。
  • 背景 :当 WebView 的尺寸、布局或可见性发生改变时,需要通知底层的 WebPage 重新计算布局或滚动条。addViewListener 允许 WebPageWebEngine 的某些内部组件(通过 Accessor)监听这些变化,而无需持有 WebView 的直接引用,从而避免循环引用。

四、与之前分析过的类的联系与区别

角色 Accessor 的关系
WebPageClient 定义"服务提供者"的接口(引擎 → 宿主) Accessor 是"服务消费者"的工具(宿主 → 引擎)。WebPageClientWebPage 调用的,而 AccessorWebView 用来访问 WebPage 的。
WebPage Java 层 WebKit 核心对象 AccessorgetPage() 方法直接返回 WebPage 引用,使得 WebView 可以调用 WebPagesetBoundspaint 等方法。
WebView JavaFX 场景图中的节点 AccessorWebView 内部提供实现,用于暴露其内部的 WebEngineWebPage 以及对子节点的管理能力。
WebEngine 非可视的浏览器引擎 Accessor 静态部分(getPageFor)允许公共 API 通过 WebEngine 获取其私有的 WebPage
WCWidget JNI 回调的"指令分发器" WCWidget 通过 WebPageClient 向上通信;而 Accessor 则用于向下(从 UI 层到逻辑层)获取数据或执行操作,两者方向互补。

关键区别

  • 通信方向
    • WebPageClient自下而上(WebKit 引擎 → JavaFX 宿主)。例如,引擎说"我想改变光标"。
    • Accessor自上而下(JavaFX 宿主 → WebKit 引擎)。例如,宿主说"我要获取 WebPage 来调整滚动位置"。
  • 可见性
    • WebPageClient 是接口,由宿主实现,暴露给引擎。
    • Accessor 是抽象类,由宿主实现,暴露给框架内部的其它组件(如 WebPage 的某些方法需要通过 Accessor 回调到宿主)。

五、Accessor 在全局架构中的角色

它是 JavaFX WebView 模块化设计的"润滑剂":

  1. 打破模块边界 :利用 PageAccessor 静态注册机制,使得公共 API(javafx.scene.web)可以安全地获取内部对象(WebPage),而不产生编译期循环依赖。
  2. 提供统一的门面 :将 WebViewWebEngineWebPage、子节点管理、视图监听等分散的功能聚合到一个统一的访问点,便于内部实现时快速获取所需资源。
  3. 作为"可插拔"的桥梁Accessor 的实现可以被替换(在测试或特定环境中),方便 Mock 或模拟 WebView 的行为。

一句话人话总结:

Accessor 是 JavaFX WebView 世界里的 "万能通行证" 。当外交官(WebPageClient)解决不了的问题(比如宿主需要主动拽出引擎内部的数据),就需要这张通行证来敲开 WebPage 的大门。它用静态注册的方式巧妙地避开了 Java 的包隔离限制,让住在"公共区"(javafx 包)的 WebView 能指挥住在"内部区"(com.sun 包)的 WebPage 去干活,而两边都不需要知道对方的具体门牌号。

相关推荐
vx-Biye_Design1 小时前
springboot中国传统节日宣传平台49078-计算机课程设计、毕业设计
java·前端·vue.js·spring boot·后端·课程设计·idea
野生技术架构师1 小时前
从向量检索到混合搜索优化:Spring AI 2.0.1 + pgvector RAG 实战
java·人工智能·spring
企业解惑小助手1 小时前
如何禁止文字复制?企业文档防复制项目需求拆解方案
运维·数据库·安全
IT枫斗者枫哥1 小时前
CompletableFuture 超时了,任务还在跑?用三个实验分清超时与取消
java
chushiyunen1 小时前
mockito笔记
java·开发语言·笔记
名字还没想好☜2 小时前
Python 用 tempfile 安全创建临时文件:NamedTemporaryFile、TemporaryDirectory 与别自己拼 /tmp 的坑
开发语言·后端·python·安全·编程语言
Doubbbbbbble云2 小时前
基于空间局部性的排序算法性能重构思路4
java·重构·排序算法
MetaLite2 小时前
Java 时间处理的两个坑:单位误判,半秒算成一秒
java·开发语言·前端
黄骨鱼骨2 小时前
DatI:给AI接入数据库,Agent时代的NL2SQL问数与多维表格应该怎么做
java·database·text2sql·nl2sql·chatbi·mcp·data agent