☰
XE10 Android java2op jar2pas 与 JNIBridge 实战排错
2026/10/28 3:44:52 网站建设 项目流程

简介:Java2OP for XE10 面向需要在 Delphi 10.2 环境中调用 Java 类库的开发者,围绕 jar 到 pas 的桥接需求,提供从 Java 字节码元数据到 Delphi 单元文件的转换工具。压缩包共 8 个文件,约 16.4MB,包含可执行主程序、两个 jar 依赖、inc 模板、xml 配置以及 txt 说明和 log 日志等,分别承担运行、转换规则支撑、参数配置与过程记录等用途,便于快速搭建转换环境。已有 260 人学习下载。借助包内工具,可将 jar 中的类、接口和方法声明转换为可引入 Delphi 工程的 PAS 文件,同时结合说明与日志检查转换结果,适合需要复用 Java 生态、实现跨语言调用或研究 jar2pas、class2pas 流程的中高级开发者参考。

1. 接手一个 XE10 的 Android 工程,只有一支 jar 没有源码

工程是 RAD Studio XE10(内部版本号 17.0),要在 Android 端接一支第三方 jar,没有源码也没有调用说明。bin目录里躺着java2op.exe,看着就是现成的 jar2pas 工具,我在命令行敲下第一条转换命令,窗口闪一下退出,只留一行 JVM 相关的报错。折腾半天才确定问题不在参数,而在本机装的是 64 位 JDK——32 位的转换工具加载不了那份 jvm.dll。

这个场景在 Delphi 做 Android 时太常见了:系统 API 之外的东西大多要靠 java2op 把 .class 或 .jar 转成 pas(也就是 class2pas 那条路),转出来的单元并进工程,运行时靠 JNIBridge 调 Java。转不干净,轻则编译一片 Undeclared identifier,重则编译过、真机一调就崩,日志只留一行 NoClassDefFoundError。下面记录的是我在 XE10 上从装 JDK、定位工具、跑转换到真机验证的完整走法。

2. 环境清单:XE10 的 java2op.exe 在哪、JDK 该装几位

动手前先把三件事固定下来:工具在哪、Java 环境几位、工作目录在哪。这三件事没固定,后面每一次报错都会变成玄学,同一个命令在别人的机器上能跑,在你这儿就是不行,最后只能靠猜。

2.1 三个版本目录对照:17.0 / 18.0 / 19.0 里的 java2op.exe

java2op.exe是随 RAD Studio 一起装进bin目录的,不需要单独下载,也不需要找什么安装包。它与 IDE 版本严格绑定,版本号写在路径里,这也是很多人第一次找不到它的原因——网上抄来的路径和自己机器上的对不上。

产品版本内部版本号相对位置
XE10 Seattle17.0<安装根目录>\Studio\17.0\bin\java2op.exe
10.1 Berlin18.0<安装根目录>\Studio\18.0\bin\java2op.exe
10.2 Tokyo19.0<安装根目录>\Studio\19.0\bin\java2op.exe
10.3 Rio20.0<安装根目录>\Studio\20.0\bin\java2op.exe

安装根目录按自己机器替换,默认在系统盘的程序目录下。确认位置最直接的办法是开一个资源管理器窗口搜java2op.exe,比翻文档快。顺带把javac、javap、jar三个 Java 工具也确认一遍,后面核对签名全靠它们。

提示:不要把java2op.exe单独拷到别的目录去用。它依赖同目录下的运行时组件,单独拷出来常见的结果是启动即退出、连帮助都打不出来。

版本差异要提前有数:17.0 上的参数集比后来的 19.0、20.0 少一些,网上的教程很多基于新版本写,参数直接照抄会报Invalid option。最稳的做法是先在命令行里裸跑一次java2op.exe,它会把自己的帮助打出来,以这份帮助为准,别信博客。

2.2 JDK 位数决定 jvm.dll 能不能被加载

这是整套环境里最容易翻车的一环。java2op.exe是 32 位进程,启动时要去加载 JVM 的动态库(jvm.dll),位数对不上就直接失败。机器上装的是 64 位 JDK,我当初就是在这里卡了整整一个下午,因为报错信息里只提 JVM,完全没提位数。

常见做法是单独准备一份 32 位 JDK 8,只给转换工具用,不去动系统里那套 64 位的。装完之后有两个目录必须进 PATH:<JDK32>\bin(提供 java、javac、javap)和<JDK32>\jre\bin\client(jvm.dll就在这儿)。顺序上要放在最前面,避免被系统里另一套 Java 抢先命中。

rem 检查当前窗口实际命中的是哪一份 Java where java where javac where jvm.dll

where是按 PATH 顺序输出的,第一行才是真正生效的那一份。三条命令里如果jvm.dll找不到,说明jre\bin\client没进 PATH;如果找到的是 64 位 JDK 目录下的那份,说明 PATH 顺序反了。这两个判断比读报错快得多。

还有两个细节值得顺手确认:JAVA_HOME指向的必须是 JDK 根目录而不是 JRE 目录;路径里不要有中文和空格,中间带空格的目录名在拼参数时非常容易被截断。

2.3 一份只对当前窗口生效的启动脚本

我不建议为了这个工具去改系统环境变量。改完之后,别的项目、别的命令行窗口都会受影响,过两个月自己都忘了改过什么,这类“环境漂移”是最难查的一类问题。写一个.bat,用setlocal圈住作用域,窗口关掉即失效,相当于给自己留了后悔药。

@echo off setlocal rem 32 位 JDK,专供 java2op 使用 set JDK32=C:\Java\jdk1.8.0-32bit rem RAD Studio XE10 的 bin 目录 set RADBIN=C:\RAD\Studio\17.0\bin rem 把 JDK 和自己的 bin 放到 PATH 最前面,优先级最高 set PATH=%JDK32%\bin;%JDK32%\jre\bin\client;%RADBIN%;%PATH% set JAVA_HOME=%JDK32% rem 三行自检:位数、版本、工具是否可见 java -version where jvm.dll java2op.exe endlocal

逻辑很直白:setlocal保证所有改动只在当前窗口有效;PATH前面两段解决 JVM 加载,最后一段解决java2op.exe直接可调用;不带参数跑一次java2op.exe会把帮助打出来,顺便验证它确实能被启动。java -version的输出里如果出现 64-Bit 字样,说明 PATH 还是被别处覆盖了,回头查顺序。

以后每次要转换,双击这个 bat,在它开出来的窗口里操作,不要在别的 cmd 里操作。血泪经验:同一个命令在两个窗口里结果不同,九成是环境变量不一样。

3. jar2pas 实操:java2op 命令行参数与第一次成功转换

环境固定之后才轮到参数。这一段的目标很明确:用一条命令把一支 jar 变成一份能编译的.pas,并且知道每个参数改动了什么。

3.1 三种输入模式:-classes、-jar、-list 怎么选

java2op有三种喂输入的方式,选错了要么转不全,要么转出一堆用不上的类。

-classes直点名,适合只调一两个类的情况,类名写全限定名(带包名),多个类之间用逗号分隔。优点是产物干净,缺点是要手工把整条继承链上的父类都点出来,漏一个父类,生成结果里的父接口就会退化成JObject。

-jar直接给 jar 文件路径,工具会把 jar 里能识别的类全部转出来,适合“不知道要用哪些类”或者“要用一大片”的情况。产物体积大,但不会漏。

-list读一个文本清单文件,一行一个类名,适合类数量多到命令行塞不下的场景。清单文件用 UTF-8 无 BOM 保存,带 BOM 时第一行类名会莫名识别失败。

我一般的取舍是:先用-jar全量转一次,看日志里哪些类报了 not found,再决定要不要缩到-classes。全量转一次的成本很低,但漏转一个类带来的排查成本很高。

3.2 输出侧参数:-unit、-out、-headerUnit、-includeConflict

输入解决“转什么”,输出参数解决“转成什么样、放哪儿”。

  • -unit:指定生成单元的名字。建议统一加Android.JNI.前缀,和 IDE 自带的桥接单元保持同一套命名习惯,工程里一眼能看出来这是生成物而不是手写代码。
  • -out:输出目录。不要和源码目录混在一起,单独建一个java2pas\out,重新生成时整个目录删掉重建,避免旧文件残留导致编译到过期版本。
  • -headerUnit:生成的单元要 uses 的头单元,默认是Androidapi.JNIBridge。工程里如果有自定义的桥接头,这里要跟着改,否则编译期找不到TJavaGenericImport。
  • -includeConflict:默认情况下,工具会跳过那些 RTL 里已经有桥接的类,避免重复定义。加上这个开关就强制生成。除非确实要做覆盖,否则不要加,加了很容易撞出一堆重复签名。
  • -skip:跳过指定类,BuildConfig、R这类纯资源类建议直接跳过,转出来也调不了。

参数名以本地帮助为准,17.0 上能用的组合比新版本少,敲错时工具会打一行Invalid option并退出,不会给你任何猜测空间。

3.3 跑通第一支 jar:命令、日志与生成的 .pas 长什么样

下面这条是我在 XE10 上跑通的最小命令。习惯上会把 android.jar 一起塞进-jar,让工具能解析android.*和java.*的父类,否则生成的接口父类全是JObject,方法表是空的。

java2op.exe ^ -jar ".\libs\probe.jar;<AndroidSDK>\platforms\android-22\android.jar" ^ -unit Android.JNI.Probe ^ -out ".\java2pas\out" ^ -headerUnit Androidapi.JNIBridge ^ -skip "com.example.probe.BuildConfig" ^ > java2op.log 2>&1

几个点单独说明。多个 jar 之间用分号分隔,整段用引号包住——不带引号时,路径里的空格会把参数切成两半,工具只会认前半段,然后默默产出一个空单元,这个坑后面还会单独说。android.jar的 api 级别按你本地platforms目录里实际存在的那个填,别照抄。末尾把标准输出和错误都重定向到日志,是因为部分版本的提示只写到 stderr,不重定向的话窗口一关就没了。

跑通之后,java2pas\out下会出现一份Android.JNI.Probe.pas,结构大致长这样:

unit Android.JNI.Probe; interface uses Androidapi.JNIBridge, Androidapi.JNI.JavaTypes; type JProbe = interface; // 前向声明,供下面引用 JProbeClass = interface(JObjectClass) ['{生成的 GUID,不要手抄}'] {class} function greet(who: JString): JString; cdecl; // static 方法带 {class} {class} function version: Integer; cdecl; end; JProbe = interface(JObject) ['{生成的 GUID,不要手抄}'] // 实例方法在这里 end; TJProbe = class(TJavaGenericImport<JProbeClass, JProbe>) end; implementation end.

看结构就明白了:静态方法声明在JXxxClass接口里并带{class}标记,调用时走TJProbe.JavaClass.方法名,不需要创建对象;实例方法声明在JXxx接口里,得先有 Java 侧的对象引用才能调。两个 GUID 由工具生成,每次重生成都可能变,所以千万不要手工改这份文件——改了下次重新生成就全丢,正确做法是把它当纯产物,改逻辑放到外层的包装单元里。

4. class2pas 之后:把生成单元放进工程并编译通过

生成文件只是半成品,真正麻烦的是让它和工程里的其它单元和平共处。

4.1 单元命名、Uses 顺序与重复 JavaSignature

生成单元丢进工程目录后,先在 Project Options 的 Unit scope names 里确认有Androidapi,否则uses Androidapi.JNIBridge这种写法在部分配置下解析不到。单元名带点(Android.JNI.Probe)是可以的,Delphi 会当成普通标识符处理。

引用顺序上,Androidapi.JNIBridge和Androidapi.JNI.JavaTypes放在生成单元之前,生成单元放在业务单元之前。顺序错了最常见的表现是编译报E2003 Undeclared identifier,指向的却是生成文件里明明写着的类型。

重复签名是另一个高频问题:两个生成单元如果为同一个 Java 类各生成了一份接口,编译期可能过得去,运行期注册桥接时就出事。表现是调用时拿到的对象类型对不上,或者直接抛异常。解决办法不是改生成文件,而是在转换阶段就掐掉:用-skip排除,或者用-edl把已有 Delphi 单元映射进去,让工具知道这个类已经有主了。

4.2 依赖类缺失:父类、内部类、R 类怎么补

生成出来的接口如果父接口是JObject而不是你预期的那个类,基本可以断定父类没被解析到。原因通常是父类在另一个 jar 里,或者父类是android.*下的东西而android.jar没喂进去。补的办法就是把依赖 jar 一并放进-jar列表,或者用-classes显式把父类点出来。

内部类(类名带$的那些)要看用途:只是编译期引用的话,跳过没关系;如果是回调接口,比如各种Listener,那就必须转出来,否则事件没法接。R 类和BuildConfig一律跳过,它们只在 Java 侧有意义。

4.3 jar 随应用发布:Libraries 节点与 Deployment 两条路

这一步是纯运行期问题,编译期完全看不出来。工程里引用了生成的 pas,只代表 Delphi 侧认识了这个类;Java 侧的字节码还得跟着应用一起发到设备上。

常见做法是在 Project Manager 里展开 Android 目标,找到 Libraries 节点,右键 Add 把 jar 加进去,构建时会自动处理打包。工程里没有这个入口的版本,退一步用 Deployment 面板手工添加,注意远程路径要和包加载逻辑对得上。

漏掉这一步的典型现象是:编译零错误,真机上第一次调用就抛java.lang.NoClassDefFoundError,日志里只有类名,看不出是打包问题。我一般的习惯是先在设备上确认包里的文件清单,再回头怀疑代码。

5. 排查笔记:java2op 从命令行到真机的五个翻车点

下面五条都是我自己踩过的,按“现象 → 原因 → 解决”记,遇到同类问题时可以对着现象直接定位。

5.1 环境与加载类问题

现象:命令行窗口一闪就退,或者只留一行与 JVM 加载失败相关的报错,连帮助都打不出来。 原因:java2op.exe是 32 位进程,PATH 里命中的是 64 位 JDK,或者jre\bin\client根本没进 PATH,jvm.dll加载不到。 解决:装一份 32 位 JDK 8 专用,把<JDK32>\bin和<JDK32>\jre\bin\client放到 PATH 最前面,用where jvm.dll确认命中的是 32 位那份,再重开窗口跑一次。改完 PATH 一定重开窗口,旧窗口不会自动刷新。

现象:转换跑完了,但输出目录里只有一个 0 字节的.pas,日志干干净净。 原因:-jar后面的路径含空格或中文且没加引号,参数被截断,工具拿到的是一个不存在的路径,于是什么都没转。 解决:整段路径加双引号,多个 jar 用分号分隔后再整体加引号;工作目录尽量放在无空格、无中文的路径下;把命令写进 bat 文件而不是直接粘到 cmd 里,避免续行符被吃掉。

5.2 转换产物问题

现象:生成的.pas编译报E2003 Undeclared identifier,指向JString、TJavaGenericImport这类基础类型。 原因:转换时没带-headerUnit,或者工程 Unit scope names 里缺Androidapi,也可能是工程里同时存在两份不同时期生成的同名单元。 解决:转换命令补上-headerUnit Androidapi.JNIBridge;Project Options 里检查 Unit scope names;把输出目录整个清空重新生成一次,确认工程引用的是最新那份。

现象:生成结果里全是a.a.a、a.b.c这种类名,调方法时提示找不到。 原因:jar 被混淆过,原始类名和方法名已经不可读。 解决:让提供方出一份保留类名的构建,或者直接用javap -p -s -classpath xxx.jar a.a.a反查真实签名,按签名在包装单元里手写调用;同时把混淆规则里的 keep 配置要过来,否则每次对方发版都要重来一遍。

5.3 编译与运行期问题

现象:编译全过,真机一调就抛java.lang.NoClassDefFoundError,异常信息里只有类名。 原因:jar 只加进了工程引用,没进 Libraries 节点或 Deployment 面板,打包时没被带进去。 解决:在 Project Manager 的 Android 目标下把 jar 加到 Libraries,或手工在 Deployment 里补一条;改完做一次 Clean 再构建,增量构建有时不会重新打包。

现象:调用TJXxx.JavaClass的瞬间崩溃,栈里看不出有用信息。 原因:类名大小写写错(Java 侧严格区分大小写),包名与生成文件里的JavaSignature不一致,或者调用发生在非主线程导致 JNI 环境没挂上。 解决:用javap核对全限定名与签名,逐个字符比;调用代码放回主线程执行;确认生成单元的JavaSignature和 jar 里的实际包名一致。翻车最多的还是大小写,看着一样,其实不一样。

6. 装完怎么验证:用一个自写 jar 打通 JNI 调用链

验证不要拿真实业务库去试,坑太多,出问题分不清是环境还是库本身的毛病。我一般自己写一个只有静态方法的小类,把链路单独跑一遍。

三步走:先用javac -source 1.7 -target 1.7编译(高版本字节码在部分工具链上解析会出问题),jar cf probe.jar com打包;再用第 3 章那条命令转成 pas;最后在窗体上放个按钮,调用并显示返回值。

uses Androidapi.Helpers, Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Android.JNI.Probe; // java2op 生成的那份单元 procedure TFormMain.Button1Click(Sender: TObject); var LRet: JString; begin // 静态方法挂在 class 引用上,不需要 Create 实例 LRet := TJProbe.JavaClass.greet(StringToJString('delphi')); Memo1.Lines.Add(JStringToString(LRet)); end;

StringToJString和JStringToString来自Androidapi.Helpers,负责 Delphi 字符串和 Java 字符串之间的转换,方向别写反:传参用前者,取返回值用后者。静态方法不需要构造对象,这也是先用静态方法验证的原因——少一层对象生命周期,出问题只可能是环境和打包。

对照下面四项看结果,哪一项不符就回哪一章:

验证点期望结果不符时先查
转换命令日志无 Error,pas 非 0 字节JDK 位数、PATH、路径引号
编译无 Undeclared identifierUses 顺序、Unit scope names
真机调用Memo 里出现预期字符串jar 是否进包、类名大小写
logcat无 NoClassDefFoundErrorLibraries 节点、dex 转换日志

真机上还可以顺手用adb logcat盯一下异常栈,Java 侧的异常在 Delphi 里往往只呈现成一句笼统的报错,原始堆栈只有 logcat 里有。

从那以后我每接一支新 jar,都强制先跑这个只有静态方法的小类,链路通了再动真库。看着多花二十分钟,实际省下的是后面分不清是工具、是打包还是对方库有问题的几个小时。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询