java2op:将Jar包转为Delphi可调用的Pas,打通JNI桥接链路
简介java2op for xe10是一套面向Embarcadero Delphi开发人员的Java互操作转换工具可将Java jar包解析并生成Delphi可引用的PAS单元文件解决在XE10.2环境中复用Java类库的难题。资源共8个文件约16.4MB包含主程序Java2OP.exe、两个jar库文件、inc/xml/txt配置模板以及log日志工具链从解析、转换到生成均有对应组件适合需要连接Java生态的Delphi项目。已有258人学习下载。借助Java2OP.exe可直接解析JAR内类、接口、方法等元数据并转换为Delphi语法结构输出为PAS单元同时支持通过模板调整生成结果包内辅助Jar与日志文件可帮助排查转换异常、比对映射差异为手工修正类型或调用约定提供依据。针对XE10.2优化的生成规则使PAS单元能较好匹配该版本编译器特性降低集成成本进一步保障跨语言调用的稳定性。对于希望重用现有Java库、减少重复编码并拓展跨平台能力的Delphi开发者这份工具包能带来直接的效率提升与落地价值。1. 把 jar 变成 Delphi 能调用的 pasjava2op 解决了 Android 开发里最烦的一步在 Delphi XE10 做 Android 开发的人应该都有过这种经历项目需要调用某个 Java SDK 的接口但 Delphi 不认 jar 包里的类你只能在 Java 那边写一个中转壳子再通过 JNI 手动调用或者干脆放弃原生集成改用 Web 接口。java2op 这个工具的作用就是把你手里的 jar 包或 class 文件直接转成 Delphi 能识别的 pas 源文件让 Java 类像 Delphi 自己的类一样被 new、调方法、读属性。它解决的不只是转换而是整个 Android JNI 桥接链路的打通——从字节码解析到数据类型映射再到 JVM 调用代码生成。适合那些已经能在 Delphi XE10 里跑通基础 Android 工程、但被 Java 依赖卡住的人。2. JNI 桥接的原理java2op 到底在替你做什么2.1 为什么 Delphi 不能直接调用 Java 类Android 系统里的 Java 层运行在 ART 虚拟机里Delphi 编译出的原生代码跑在 Native 层两者之间隔着一道 JNI 边界。JNI 本身是一套 C 风格的函数接口Delphi 可以通过 System.JNI 单元直接调用它但问题在于你要调用的不是一个 JNI 函数而是上百个 Java 类方法每个方法都要手写 jclass、jmethodID 的获取逻辑还要处理字符串转换、对象引用管理、异常检查工作量非常大。java2op配合 jar2pas、class2pas做的事情就是把 Java 字节码里的类结构、方法签名、字段签名、继承关系全部解析出来生成对应的 Delphi 接口定义和实现代码。生成的 pas 文件里每个 Java 类对应一个 Delphi 接口和实现类方法被包装成 Delphi 函数内部自动完成 jclass 缓存、jmethodID 查找和参数转换。从运行机制上说工具生成的代码和你手写的 JNI 调用底层是同一套机制但它把重复劳动自动化了。你可以把它理解为代码生成器不是运行时框架——它不替你执行 Java 代码只是铺好管道。2.2 三种转换模式的适用边界这类工具链通常支持三种输入格式jar 包最常用、编译后的 class 文件、以及直接的 Java 源码文件。选择依据有两个你的依赖是完整 SDK 还是单文件、以及有没有混淆ProGuard 处理过。jar 包模式适合大多数场景。你从厂商那里拿到一个 aar 或 jar里面通常是编译好的字节码jar2pas 会把整个包内所有类批量转换。class 文件模式适用于你本机编译出的单个 Java 文件的场景比如你自己写了一个辅助类用来包装第三方 SDK那把 .class 转过去就够了。源码模式最省事工具会用内部编译器把 .java 编译成字节码再转换但前提是你本机安装了 JDK 并且环境变量配好了。参数上我一般习惯先在源码模式跑通一个小文件确认 JDK 路径和环境变量没问题再切换到 jar 模式做批量转换。直接上来就转大 jar 包报错了会让你分不清是工具问题还是 JDK 问题。2.3 jni 签名映射表转换结果正确与否的关键转换质量的核心在 JNI 签名的映射处理上。工具在解析 Java 字节码时需要把 Java 类型表到 JNI 签名再对应到 Delphi 类型。这个表是理解后续所有问题的钥匙Java 类型JNI 签名Delphi 类型说明booleanZBoolean1 字节byteBShortInt有符号 8 位charCCharUTF-16shortSSmallInt有符号 16 位intIInteger32 位longJInt6464 位Delphi 里是原生整数floatFSingle32 位浮点doubleDDouble64 位浮点StringLjava/lang/String;JString需通过 JNI 转换接口数组[类型签名TJavaArray见说明字符串和数组是转换中的重灾区。Java 的 String 对应 JString底层是一个 jstring 句柄Delphi 侧不能直接当普通字符串用必须调用 GetStringUTFChars 取出来再转成 Delphi string。工具生成的代码里会做这层包装但如果你自己补代码很容易漏掉释放那一步导致内存只在垃圾回收时释放。数组类型生成的代码默认是引用传递这在大多数调用场景下没问题。但如果你要从 Java 侧获取一个 byte[] 出来做文件写入建议先查一下生成代码里有没有提供元素访问的辅助方法没有的话自己用 TJNIResolver 包装。3. 环境准备与转换流程从 jar 到 pas 的三个操作步骤3.1 工具链安装与 JDK 环境校验先确保环境整齐。java2op 这类工具依赖 JDK 来解析字节码所以 JDK 要先装好。安装时注意Delphi XE10 的 Android 开发用的是 Android SDK带的是 Android 版本的 JDK 类库但工具解析 jar 包时用的是你机器上的 JDK 工具类版本不一致会报 class format 错误。我常用的环境组合是 JDK 8 Delphi XE10 某个较新更新版。JDK 8 能解析绝大多数 Android 字节码Android 的 class 版本通常控制在 50 及以下JDK 11 也能跑但遇到老项目容易提示 version 55 错误。装好后打开命令行验证一下java -version javap -version注意如果你看到 java -version 正常但 javap 提示找不到命令说明 JRE 在 PATH 里而 JDK 没在程序编译类文件时会失败。这种情况把 JDK 的 bin 目录加到系统 PATH 前面即可但加完之后重启一下命令行窗口才生效。Delphi 侧也确认一下你有 System.JNI 单元。打开 Delphi 后新建一个空 Android 工程在 uses 里手动加一下 System.JNI编译能过就没问题。它存在于 FMX 和标准库中不存在就说明你的 XE10 版本太老要先更新。3.2 用 java2op 转换命令参数与四处必调配置工具本身是命令行程序也有带界面的版本。命令行方式更可控我在脚本化处理多个 jar 包的时候都用命令行。基本用法如下java2op.exe -jar yourlib.jar -o output_dir -d DelphiVersion -c参数含义分几个层级-jar 指定输入 jar 包路径-o 指定输出目录-d 指定 Delphi 版本XE10 对应填 10-c 表示同时生成实现文件不填则只生成接口。这些参数里有两个坑值得说明。第一个坑是包名过滤。默认转换 jar 包里的所有类但你的 jar 可能带了第三方依赖比如某些 SDK 内置了 okhttp 或 gson 的代码。这种全量转换会让生成文件膨胀且容易出现符号冲突。我一般在 -jar 后面跟一个 -p 参数限定包前缀java2op.exe -jar yourlib.jar -o output_dir -d 10 -c -p com.example.sdk只转换 com.example.sdk 前缀下的类第三方依赖留给运行时由 jar 包本身加载。这样生成的 pas 文件会精简很多。第二个坑是输出文件的编码。老版本工具默认输出 UTF-8而 Delphi 的源码默认是 ANSI。如果你的 jar 包里的类名或注释包含非 ASCII 字符生成的 pas 文件在 Delphi 里打开会乱码甚至编译报错。解决办法是让工具输出 UTF-8 后再用编辑器批量转成 UTF-8 with BOMDelphi 对带 BOM 的 UTF-8 识别最稳定。文件多的时候我用一个小脚本批量处理不手工一个个转。命令行工具会在结束时打印生成的文件列表和警告信息。务必逐条看警告。最常见的警告是某个类引用了 jar 包之外的类工具会在生成的 pas 里标记为 external 引用意思是运行时从类路径里找。如果你确定这个类在你的 jar 包里那就是包前缀过滤太严格了放宽 -p 参数重新转一次。3.3 转换结果的结构检查拿到生成的 pas 文件后先别急着复制进 Delphi 工程。我习惯先打开文件做三项检查每项都省时间。第一看接口声明是否完整。Java 类对应 Delphi 接口接口里每个方法对应一个函数声明方法名和参数类型要和 Java 侧的 public 方法保持一致。如果有方法缺失检查是不是 Java 里方法被标记成了 protected 或包私有工具默认只转 public 方法。第二看类引用是否需要用 import 单元。生成文件开头会 contains 或者 uses 依赖单元比如 java.lang.System、java.util.ArrayList 这些基础类在 Delphi 里已经通过 System.JNI 和 AndroidAPI 覆盖不需要额外导入 jar 包。但如果生成的 uses 列表里有你从没见过的单元那说明工具尝试引用了某些辅助文件你要确认它们是否也生成在了同一目录里。第三看有没有生成独立的初始化和释放过程。接口和实现之外工具通常会在末尾生成两个方法JClassInit 和 JClassFinalize或类似名字分别在应用启动时和退出时调用。它们的职责是缓存 jclass 引用和释放全局引用。很多人忽略这两个函数导致第二次调用时 Java 类找不到或者引用泄漏。后面集成的时候必须调用它们。3.4 在 Delphi 工程里接入生成的 pas文件放进工程后uses 里加上对应单元然后在 Application 启动事件里先调用初始化。初始化那块我会写成这样uses System.SysUtils, System.JNI, uGeneratedSDK; procedure TMainForm.FormCreate(Sender: TObject); var JVM: TJNIResolver; LClass: JClass; begin JVM : TJNIResolver.GetJNIEnv; // 初始化生成的类引用 TJ_YourClass.JClassInit(JVM); // 验证类是否可以加载 LClass : TJ_YourClass.JClass; if LClass nil then raise Exception.Create(Java class load failed - check jar in classpath); end;关键点在于 TJNIResolver 的获取时机。它必须在 Java 虚拟机已启动之后才能正常工作。Delphi Android 应用启动时 JVM 已经由系统创建好了FMX 框架会处理好这一点你只需要确认这行代码不是在后台线程执行的。JClassInit 方法会通过 JNIEnv 去查找 jclass 并缓存起来这个操作本身是重量级的只应该在启动阶段做一次不要在每次调用时重复执行。初始化成功之后再调用具体方法var LJavaStr: JString; LResult: Integer; begin LJavaStr : StringToJString(test input); LResult : TJ_YourClass.JMethod(LJavaStr); JStringToString(LJavaStr); // 回收临时引用 end;注意最下面的 JStringToString 调用这行不是必须的但 StringToJString 生成的局部引用会在当前 JNI 帧结束后自动释放不必显式回收。如果你在循环里大量构造 JString建议在循环末尾手动调用一次 DeleteLocalRef避免局部引用表溢出——这个坑后面还会细说。4. 项目集成实战以模拟项目X为示例完整走一遍4.1 准备一个带第三方依赖的 jar 包为说清楚整个过程的边界问题我拿一个模拟项目X来演示这个项目里有一个 Java SDK 的 jar 包它依赖了通用 JSON 库类似 org.json 那种。jar 包里既有 SDK 自己的类也有被打进去的第三方包。很多人第一反应是用默认参数直接整体转结果生成的 pas 文件巨大编译还报重复定义。我先用压缩软件打开 jar 包看一下目录结构确认第三方包的包名前缀。然后用上面说的 -p 参数限定 SDK 自己的包前缀把第三方依赖排除在外。这么做的原因很简单第三方 JSON 库在 Android 系统里往往已经有同版本实现运行时直接由系统提供Delphi 侧不需要也不应该引用它。转换完成之后我在生成的 pas 文件里搜一下有没有 JSONObject 相关的引用。正常情况应该找不到因为在生成 Delphi 代码里这些类型没有对应定义。如果你在调用时需要把 Java 的 JSONObject 当作参数传进去就得在 Delphi 侧用 JObject 泛型接口来承接运行时它真正指向的对象是 Java 侧创建的。4.2 封装调用层把生成的接口包一层自己的类直接生成的 pas 文件可以放进工程调用但接口风格是扁平化的每个 Java 类对应一个接口、一个实现类读起来不直观。我更推荐再包一层 Delphi 类把业务相关的调用收敛起来。举个例子SDK 里有一个 OrderService 类里面有 createOrder 和 queryOrder 两个方法。生成的接口里就是两个函数我封装成type TOrderServiceWrapper class private FOrderService: JOrderService; public constructor Create; function CreateOrder(AUserId: string): Integer; function QueryOrder(AOrderId: Integer): string; end;构造函数里做初始化工作业务方法里做类型转换和状态检查。这样的好处是 UI 层不用关心 JString 和 String 的转换也把 JClassInit 的调用时机收敛到了一个对象里管理。注意封装时不要丢掉异常信息。Java 方法抛出的异常在 Delphi 侧表现为 JNI 函数返回异常状态但不会直接变成 Delphi 异常。工具生成的代码通常不做异常检测你需要自己检查异常状态并抛出一个 Delphi 异常来承接。我在封装类里写了一个异常检查过程每个方法调用后统一执行一遍procedure TOrderServiceWrapper.CheckJNIException; var LEnv: PJNIEnv; LThrowable: JThrowable; LMsg: JString; LMsgStr: string; begin LEnv : TJNIResolver.GetJNIEnv; if LEnv.CheckException(LEnv) then begin LThrowable : LEnv.ExceptionOccurred(LEnv); LEnv.ExceptionClear(LEnv); LMsg : LThrowable.toString(LThrowable); LMsgStr : JStringToString(LMsg); raise Exception.CreateFmt(Java exception: %s, [LMsgStr]); end; end;这段代码的主要作用是调用 ExceptionClear 把 JNI 异常状态清除掉否则下一次 JNI 调用会直接失败而且不报错。用 toString 拿到的消息能辅助定位问题比看 logcat 更快。4.3 调用时机与生命周期边界Delphi Android 应用里JNI 调用有几个时间窗口是安全的主界面的初始化事件里、用户事件响应里、定时器回调里。有几个位置千万不能调用——Application 的 OnCreate 之前此时 Java 侧可能还没完成初始化、OnPause 之后有些 Java 对象状态可能已被系统回收、以及后台线程直接调 UI 相关 Java 方法不一定崩但偶发异常会让你排查到怀疑人生。另外系统会在某些时刻触发 Java 侧的垃圾回收。工具生成的代码里缓存了 jclass 全局引用这个引用不受垃圾回收影响但方法返回的局部引用jobject、jstring会在当前 JNI 帧结束时被释放。如果你在循环里拿到了一个对象引用并保存下来下次循环再取时会发现它已经失效。针对这个生命周期问题我用一个工具方法处理所有从 Java 侧返回的对象引用。它的作用是复制一个全局引用并保存到 Delphi 对象里方法返回前用 DeleteLocalRef 清掉局部引用。这个写法虽然会让代码看起来啰嗦但能保证在 Java 侧发起垃圾回收后Delphi 侧拿到的引用始终有效function RetainGlobal(LEnv: PJNIEnv; const LObj: JObject): JObject; begin Result : LEnv.NewGlobalRef(LEnv, LObj); LEnv.DeleteLocalRef(LEnv, LObj); end;NewGlobalRef 创建的引用必须成对释放用完后调用 DeleteGlobalRef否则会泄漏 Java 堆内存。Delphi 这边封装类最好在析构函数里统一释放所有保存的全局引用。4.4 接口回调场景Java 侧调回 Delphi 代码调 SDK 时最常见的需求是异步回调。Java SDK 接收一个 listener事件发生时调回来。这个场景在 JNI 桥接里属于高难度工作工具不会帮你生成 Delphi 实现类给 Java 侧调用。做法是写一个 Delphi 类继承 JObject 并实现 Java 侧的接口然后通过 RegisterNatives 或直接调用 Java 的接口实现类来传递。我一般这样处理先用工具生成接口类的 pas 定义然后手写一个 Delphi 类实现这个接口。关键点是类的继承关系Delphi 里实现 Java 接口的类必须继承自 TJavaGenericImport 或直接实现 JNI 层的回调函数。这个环节是整套方案里最容易翻车的地方有两个前置条件必须满足一是 Delphi 类必须重写 GetJNIClass 方法返回正确的 JNI 类名二是方法注册要在主线程完成。如果 SDK 初始化在后台线程回调注册事件又发生在不同线程则必须用 Delphi 的 TThread.Synchronize 把回调事件切回主线程否则后续 UI 更新会触发异常。整体上我建议回调越简单越好——收到回调后立刻将相关数据拷贝成 Delphi 侧的基本类型不在回调里做耗时操作。5. 避坑与排查五个常见问题及处理记录5.1 生成的 pas 文件编译报找不到 JClassInit 方法现象编译 Delphi 工程时报错提示找不到 JClassInit 或类似初始化函数只报了一个或几个类有这问题。原因转换时生成策略可能只生成了接口文件实现文件没生成或者 -c 参数没带到命令行里。另一种可能是这些类的 Java 定义里只有静态方法没有实例方法工具跳过了实现代码生成。解决重新执行转换并确认 -c 参数带上了。对有静态方法的类检查生成文件的尾部是否生成了静态方法的实现块。如果确实没有生成但调用时需要就手动在 Pascal 里用 TJNIResolver 缓存 jclass 并写一个初始化过程只维护你需要的静态方法。5.2 JString 转换后中文变成问号或乱码现象从 Java 侧拿到的字符串在 Delphi 里显示乱码特别是中文内容变成问号。原因JNI 标准的 GetStringUTFChars 输出的是 Modified UTF-8Delphi 的 JStringToString 在早期 XE10 版本里对代理对的转换不完整。遇到 emoji 或特殊字符时会丢字节。解决换用 JNI 提供的 GetStringChars 获取 UTF-16 内容再手动转 Delphi string。在工具生成的代码基础上自己写一个转换函数替代默认的 JStringToString核心是取 UTF-16LE 字节数组后交给 Delphi 的 String 解码。一个额外的建议在 Java 侧不要用四字节 emoji 作为业务数据不是转换不了而是后端的处理链路里很多环节都会出问题。5.3 相同 jar 包在某些设备上崩、在另一些设备上正常现象同一个 APK 在同一品牌不同型号的设备上表现不一致一部分正常另一部分首次调用就崩溃或报 ClassNotFound。原因厂商 ROM 对 Java 类库的裁剪。某些设备商将 Android 系统里部分不常用的 SDK 类移除了你的 jar 包编译引用到这些类时在特定设备上执行到对应代码段才崩溃。这种崩溃和调用代码本身无关是系统差异问题。解决先用一个启动时自检的 Java 类把所有关键类逐一加载一遍得到一份兼容性清单对缺失的类再决定是换用系统 API 还是降级功能。我一般会在 Java 侧写一个静态方法批量校验后返回布尔数组Delphi 启动时读取并记录日志这样用户报错时能看到明确原因而非崩溃堆栈。5.4 循环调用 JNI 后程序内存持续上涨现象程序长时间运行或频繁刷新时内存占用稳步增加最终被系统回收或卡顿。原因大量局部引用未及时释放。JNI 局部引用在方法返回时会释放但如果你在 Delphi 里保存了对象引用或者在循环里调用 Java 方法局部引用表可能已经满了JNI 出于保护会拒绝分配新引用调用直接失败或崩溃。解决循环场景里增加局部引用表清理。可以在循环每完成一定次数后调用 EnsureLocalCapacity 检查容量也可以把循环体拆到一个单独 Delphi 过程里让局部引用随栈帧释放。代码层面数据获取完就立刻删除局部引用不要保留 Java 对象的引用超过一次调用周期除非用全局引用确保持久。5.5 同一个 jar 包在 Debug 模式正常、Release 模式崩溃现象调试运行没问题打包发布后运行到某个 Java 调用点就崩。原因Release 模式下 Delphi 的连接器会剔除编译单元里未引用的代码而你生成 pas 里的初始化过程可能没有被显式调用导致相关全局变量的初始化代码被优化掉了。JNI 相关代码如果没在主模块中被引用会有类似问题。解决在工程的某个 AlwaysLink 单元或主单元里显式引用所有生成类的初始化过程。我习惯在工程启动时逐个调用生成类的 JClassInit 方法并保留调用结果在同一源代码里防止链接器认定该代码不可达。同时开启保持引用的链接器选项把这一项固定在工程配置里别依赖全局默认值。6. 进阶用法对生成的 pas 文件做三处手工裁剪让代码更稳更快6.1 第一处把约定俗成的常量收敛到一个配置单元生成的文件里会有大量 Java 常量值、字符串标签、方法名签名。我通常把它们系统地移到一个统一单元里统一管理。常量集中管理带来的直接好处是后续升级 jar 包时只需要重新生成库文件而不需要理会常量单元的内容除非 Java 侧确实改了常量值。另一个好处是代码审查时搜索常量定义只查一个文件对照 Java SDK 的字段表非常快。unit uSDKConstants; interface const SDK_STATE_IDLE 0; SDK_STATE_RUNNING 1; SDK_STATE_ERROR -1; METHOD_SIGNATURE_CREATE_ORDER (Ljava/lang/String;I)I; implementation end.把签名常量单独声明还有一个实际作用如果工具生成的方法调用代码出错你可以直接用这些签名做手动 JNI 调用不用重新解析 jar。6.2 第二处把使用频率高的字符串转换做成内联函数生成代码里 StringToJString 和 JStringToString 调用点非常多。每次转换都至少涉及一次局部引用创建和一次释放。这类纯转换逻辑适合做成内联函数减少调用开销。function JStr(const S: string): JString; inline; begin Result : StringToJString(S); // 临时局部引用随 JNI 帧自动释放 end;注意内联函数只适合无状态、无资源持有场景。上面这个 JStr 函数返回的 JString 是局部引用如果调用方把它保存下来超过当前 JNI 帧引用会失效。这种情况下签字要改成全局引用版本并在调用方用完显式释放。6.3 第三处给所有 Java 方法调用加一个超时保护JNI 调用本身没有超时概念Python 那种 Hanging 的情况在 JNI 里同样存在——如果 Java 侧的方法因为底层网络或锁竞争长期不返回Delphi 主线程会被卡死。在调用很可能涉及网络、IO 或锁的方法时我会把调用放到后台线程执行同时在 Delphi 侧用 TThread 加一个超时判断。Java 方法本身通常不响应中断所以这个超时只能防止 UI 线程卡死不能真正终止 Java 侧执行。function CallWithTimeout(const AProc: TFuncInteger): Boolean; var LThread: TThread; LResult: Integer; begin Result : False; LThread : TThread.CreateAnonymousThread( procedure begin LResult : AProc(); TThread.Synchronize(nil, procedure begin Result : True; end); end); LThread.FreeOnTerminate : False; LThread.Start; LThread.WaitFor(3000); // 等待最多 3 秒 LThread.Free; end;这个写法属于兜底手段正常情况不会触发超时分支。一旦触发了更重要的是排查为什么 Java 方法花了超过预期的时间而不是在超时分支里做更多补救。这三处裁剪做完我拿到新的 jar 包时的处理流程就是固定的了先整体转换、删除不必要依赖、抽出常量、封装调用层、自检异常、跑一遍循环场景看内存曲线全部通过后才接进主工程。做过的项目多了以后我发现大部分疑难问题都出现在引用生命周期管理和线程切换上工具生成代码本身基本是稳定的。JNI 这层没有玄学可讲出现问题就是引用、线程、签名三者之一按照先查引用泄漏、再查线程切换、最后核对签名的顺序排查没有解决不了的问题。希望这些经验能帮你少走几步弯路。本文还有配套的精品资源点击获取