简介:面向Embarcadero Delphi开发人员的Java互操作工具Java2OP for XE10,特别针对XE10.2版本优化,可将Java类库中的jar文件自动转换成Delphi可引用的.pas单元,解决跨语言复用问题。整个资源包共8个文件,涵盖主程序、运行库、配置模板与使用说明等类型,压缩包大小16.4MB,轻量且便于部署;目前已有255人学习下载。借助该工具,开发者只需三步即可完成转换:解析jar中的.class字节码,将类、接口和方法转为Delphi语法,再生成可直接加入工程的PAS文件,大幅减少重复编码工作量与排查成本。同时,内置的配置模板与说明文档可灵活调整转换规则,适配不同项目需求。尽管某些复杂Java特性可能无法完全映射,需手动修正,但在借助Delphi跨平台能力调用成熟Java生态方面,该资源依然能显著提升开发效率。 做 Delphi Android 开发的人,尤其是手里还压着 XE10(RAD Studio 10 Seattle)项目的老伙计,应该都记得那个叫 java2op 的小工具。那几年论坛里一旦有人问"Delphi 里怎么调 Java 第三方 SDK",答案基本都指向同一个流程:用 java2op 把 Java 类翻译成 Object Pascal 接口,再引用进工程。听起来很丝滑,但实际跑起来,环境变量、配置文件、类解析失败、编译报错,随便卡一步就是半天。这里我就把在 XE10 上把 java2op 从环境搭建到生成代码、再到打进 APK 的完整链路重新捋一遍,附带这几年攒下的坑和排查思路。
1. XE10 里的 Java 调用,为什么绕不开 java2op
1.1 先搞清楚它到底解决了什么问题
XE10 时代,Delphi 的 FMX 应用在 Android 上本质是一个原生 .so 进程,而 Android SDK、第三方 SDK 都是 Java 运行时里的东西。Object Pascal 和 Java 两套运行时之间,唯一正路就是 JNI(Java Native Interface)。你要是每个方法都自己写 JNI 桥接,不是不行,但工作量非常吓人:一个稍成规模的 Java SDK 动辄几十个类、几百个方法,类型转换、签名注册、异常处理统统手工来一遍,基本就没精力干正事了。
java2op 的出现就是把这个脏活自动化。它读入 Java 编译产物(.class、.jar 或者某个 classpath 下的类),分析每个类的继承关系、构造函数、静态方法、实例方法、字段,然后翻译成一整套带 JNI 映射元数据的 Pascal 接口定义。它不只是生成"方法原型",还会为每个接口生成 GUID,为类的加载生成 JNI 引用逻辑。生成的代码通过 Delphi 自带的 JNI bridge 运行时驱动,开发者感知到的就是"我好像在直接用 Delphi 类"。
打个不恰当的比方:它像一个"协议翻译器",输入端是 Java 类型签名,输出端是 Delphi 能直接引用的接口声明,在翻译过程中把 JNI 的复杂细节提前封装掉了。你不要把它想成什么高深魔法,它解决的就是一个非常朴素的痛点——让两套运行时能在类型层面安全对话。
1.2 其实你每天都在偷偷用它
很多人不知道,XE10 自带的 Androidapi.JNI.* 单元,比如 Androidapi.JNI.App、Androidapi.JNI.GraphicsContentViewText,本质上就是官方提前用 java2op 从 Android SDK 的 android.jar 批量生成的产物。也就是说,这不是一个冷门旁路工具,而是整个 Delphi Android 基础库的生成引擎。
所以遇到官方没有预生成覆盖的类,或者要接入第三方 JAR 时,正路不是自己手写 JNI 签名,而是用同一个工具、同一套规则做一次局部生成。我见过不少人试图绕过 java2op,自己照着 Java 文档手写接口,最后多半栽在类型映射和 GUID 上。有一个算一个,写到最后都回来找工具了。原因很简单:你手写的是"看起来对"的代码,工具生成的是"能跑通"的代码,两者隔着 JNI 桥的整套映射规则。
2. 跑通前先解决环境:JDK、SDK、配置文件一个都不能少
2.1 给 XE10 配一个老实的 JDK
java2op 本身是一个可执行程序,但它解析 Java 类时需要借用 JDK 的类解析能力,以及 Android SDK 里的 android.jar 作为框架类的解析参照。所以环境基础必须打牢,否则后面全是莫名其妙的问题。
- 安装 JDK 7 或 JDK 8。XE10 那个年代的 Android 工具链对高版本 JDK 支持非常糟糕。我试过拿 JDK 11 跑,java2op 各种花式报错,换回 JDK 8 立刻正常。如果你的机器上装了多个 JDK,建议把环境变量 JAVA_HOME 显式指到 JDK 8。
- 确认 Android SDK 已安装完整,至少存在一个 platforms\android-21 目录,里面有 android.jar。XE10 默认用 API 21 编译,SDK 管理器里能直观看到。
- 把 java2op.exe 所在目录(默认是 C:\Program Files (x86)\Embarcadero\Studio\17.0\bin)加进 PATH,或者直接用全路径调用。省得每次都在命令行里 cd 来 cd 去。
这三个里最容易忽略的是 JAVA_HOME。很多人明明装了 JDK,但环境变量没设,java2op 启动时找不到 jvm 相关文件,给出的报错又像模像样,很容易让人往配置文件的错误方向排查。
2.2 配置文件:先写 classpath,再列类清单
java2op 的正常用法是给它一个配置文件,然后执行 java2op -c 配置文件。下面是我放在项目里的典型配置,注释也写上,免得半年后自己都看不懂:
-classpath C:\Android\sdk\platforms\android-21\android.jar;D:\work\thirdlib\mylib.jar -javaClasses com.example.mylib.TextUtils com.example.mylib.HttpClient -o D:\work\Bindings\MyLibBinding.pas关键点拆开看:
- -classpath:分号分隔的路径列表,把 android.jar 和你要转换的 JAR 都放进去。漏掉 android.jar 的话,凡是继承 Android 类或者方法参数里出现 android.* 类型的类,基本都会解析失败。
- -javaClasses:列出真正要转换的类的完整限定名。不需要把所有类全部列完,java2op 会按其依赖关系追踪,但建议至少把直接使用的入口类列全。
- -o:输出 .pas 文件名。如果省略,工具会在当前目录生成一个默认文件名,找起来很别扭,所以我每次都显式指定。
2.3 运行命令与第一眼观察
准备好之后,打开命令行,执行:
java2op.exe -c mylib_config.txt正常运行的话,日志里会列出正在处理的类名、解析了多少个方法、最终输出到哪个文件。头一次跑建议先转一个小 JAR,比如只含两三个类的测试包,跑通流程再上大家伙。如果一上来就转几十 MB 的大 JAR,日志滚动半天,中途报错了你都不知道是哪一步、哪个类引起的。
另外提醒一句:生成的 .pas 不要当手写代码去维护。JAR 升级后,你只需要更新类清单,重新跑一次覆盖即可。我把所有生成的绑定统一放在项目下的 Bindings 子目录里,和手写代码隔离,出问题能快速定位是生成层还是业务层。
3. 实测:一个带 static 方法的 JAR,从生成到真机调用
3.1 准备一个能被验证的 Java 类
单纯讲概念没什么意思,我用一个最简例子走完整链路。假设第三方 JAR 里有这么个类:
package com.example.mylib; public class TextUtils { public static String getGreeting(String name) { return "Hello, " + name; } public int add(int a, int b) { return a + b; } }有人可能会觉得这不就是个玩具吗?但实践里很大一部分需求恰恰就是这么简单——某个库提供了一个静态工具类或者一个单例类,返回一个结果。把这条路跑通,复杂的 SDK 也只是更多类的重复劳动。
3.2 生成的 .pas 长什么样
用第 2 节的配置跑完 java2op 后,打开 MyLibBinding.pas,核心结构长这样(我做了精简,去掉无关元数据):
unit MyLibBinding; interface uses Androidapi.JNIBridge, Androidapi.JNI.JavaTypes; type JTextUtils = interface(JObject) ['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}'] function add(a: Integer; b: Integer): Integer; cdecl; end; JTextUtilsClass = interface(JObjectClass) ['{yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy}'] function getGreeting(name: JString): JString; cdecl; function init: JTextUtils; cdecl; end; TJTextUtils = class(TJavaGenericImport<JTextUtils, JTextUtilsClass>) end; implementation end.几个观察点:
- Java 包名会被折叠进类型的 JNI 类名映射里,你在代码里感知不到 com.example.mylib 这一层,但运行时它自动对应到完整 Java 类。
- 静态方法 getGreeting 出现在 JTextUtilsClass 接口上;实例方法 add 出现在实例接口 JTextUtils 上。这是 Delphi JNI bridge 的固定套路。
- TJTextUtils 是一个泛型导入类,TJavaGenericImport 把实例接口和类接口绑定在一起,由 JNI bridge 在运行时完成类加载和对象创建。这一套你不用深究,照着用法写就对了。
3.3 把 JAR 塞进 APK 并调用
生成的单元加入 Delphi 工程后,还有一步容易漏:把原始 JAR 打进 APK。打开 Project > Options > Android 下的 Libraries(不同 update 版本显示名略有差别,功能一致),添加 mylib.jar。这一步的作用是让编译工具链把 JAR 一并 dex 化,打包进 APK。否则 Delphi 代码在真机上调用时,Java 类根本不存在,只会在 Logcat 里留一句找不到类的异常。
工程编译没问题后,在 FMX 里随便写个按钮事件:
uses System.SysUtils, Androidapi.Helpers, Androidapi.JNI.Toast, MyLibBinding; procedure TForm1.Button1Click(Sender: TObject); begin // 调用静态方法 Toast(Format('static: %s', [JStringToString( TJTextUtils.JavaClass.getGreeting(StringToJString('Delphi')))]), ShortToast); // 调用实例方法 var obj := TJTextUtils.Create; try Toast(Format('instance: %d', [obj.add(2, 3)]), ShortToast); finally obj := nil; end; end;TJTextUtils.JavaClass 拿到的是类接口,用于静态方法;TJTextUtils.Create 会通过 JNI 的 newInstance 创建实例,上面生成的 init 就是干这个的。字符串一定要用 StringToJString / JStringToString 显式转换,这个步骤新手容易忘,忘了就是访问冲突或乱码。
3.4 调用各种类型的通用原则
方法参数和返回值涉及基本类型时直接对应 Pascal 类型;字符串一律 JString;数组在 Delphi 侧通常传 TJavaArray ;嵌套对象直接用对应的 J 接口。看到生成的签名里有你不认识的自定义类型名,多半是另一个 Java 类,需要确认它是否在生成清单里,或者已经被 java2op 自动带出来。如果自动带出来的类编译不过,把它补进 -javaClasses 再重新生成一次就行。
4. 踩坑实录:解析失败、编译报错的完整排查链路
4.1 类找不到与解析失败
最常见的现象:运行 java2op 后日志里报 ClassNotFoundException 或 Parse error,但 JAR 明明就在眼前。先别怀疑工具坏了,按顺序查:
- 看 -classpath 里有没有漏掉 android.jar。Android 框架类是最大的依赖源,漏掉它等于所有 android.* 引用全部断链。
- 看类清单里的名称是不是完整限定名。写 TextUtils 绝对找不到,必须是 com.example.mylib.TextUtils。
- 看 JAR 是否被其他进程占用。有些杀毒软件会锁住 JAR,导致读入不完整、解析到一半报错。这个坑很隐蔽,我遇到过一次,最后关掉实时监控就好了。
- 看 JDK 版本。JDK 太新,class 文件版本超出工具解析范围,会报 class file has wrong version,换 JDK 8。
4.2 重载方法冲突
Java 支持方法重载,但 java2op 生成的 Pascal 接口里,如果出现转换后无法区分的同名方法,Delphi 编译器会直接报 Duplicate method,位置通常指向生成的 .pas。很多人在这一卡就是大半天。
我的处理方式:先在生成的代码里找到冲突方法,保留实际会用到的那一个,把另一个注释掉,并加一行 TODO 注明冲突来源。Java 重载在类型擦除后参数数量一般不同,大多数情况下可以通过调整一个参数类型(比如把 JObject 换成更具体的 J 接口)来消除歧义。如果实在纠缠不清,宁可绕开这个方法,也不要伪造一个不存在的签名去硬凑,真机上的 JNI 调用会直接崩。
4.3 依赖链断裂的连锁反应
第三方 JAR 通常不是孤家寡人,背后往往还挂着 okhttp、gson 之类的依赖。java2op 解析目标类时,如果发现它的父类、方法参数、返回值指向一个依赖类,而这个依赖类既不在 classpath 里也不在类清单里,结果就不可靠:轻则生成一个挂空接口,重则直接中止。
靠谱实践是把所有依赖 JAR 全部加到 -classpath,需要生成的入口类再逐个加入。如果要偷懒,可以用 java2op 的自动追依赖参数,把相关类全部生成出来,好处是省事,代价是输出文件巨大、编译变慢,还可能带出一堆用不上的类。我的建议是小项目图省心可以自动追,正式 SDK 集成还是逐类确认,避免埋雷。
4.4 真机崩溃与日志定位
生成和编译都过了,不代表真机就稳。Delphi 的 Android 工程可以通过 Logcat 看崩溃日志。如果崩溃发生在 Java 侧,常见原因是 JAR 没打进 APK 的 dex(重新查 Libraries 列表),或者当前 JAR 与生成时用的 JAR 版本不一致。排查时先在 Delphi 里加 Application.OnException 日志输出,再配合 Logcat 按进程名过滤,范围一下就能缩小。
我个人习惯先验证"类能不能加载",再谈方法调用。随便弹一个 Toast,把 TJTextUtils.JavaClass.toString 的结果显示出来,能正常显示说明类加载没问题,后面就是方法签名和参数的问题;如果连这都不弹,优先查 JAR 打包和 dex 环节。
5. 运行时脾性:线程、引用与 API 版本
5.1 主线程与回调线程的边界
Java 侧很多 UI 相关类和回调要求调用发生在主线程。Delphi 的 FMX 主线程就是 Android 主线程,所以直接在按钮事件里调用一般没问题。但如果你从 Timer、Socket 回调、后台线程里发起调用,就要格外小心。Android 对 UI 操作有严格线程检查,跨线程调用轻则抛异常,重则应用无响应。
我的折中办法是把所有涉及 UI 的 Java 调用包进 TThread.Sync 或者 Delphi 的匿名线程队列,强制回到主线程执行。对于纯计算型、非 UI 的 Java 方法,则可以直接在子线程调用,实测是稳定的,但要注意 JNI 引用的生命周期,别让后台线程长期持有一个本应释放的 Java 对象引用。
5.2 本地引用表的隐藏压力
JNI 有一个经典大坑叫 Local Reference Table Overflow。如果你的循环里频繁调用 Java 方法,每次都会产生新的 JObject,比如在几千条记录上反复调用某个 SDK 方法,Delphi 的 JNI bridge 虽然会尽量管理引用,但积累过度仍然可能在 Logcat 里看到 reference table overflow。
我的习惯是:大循环里尽量避免让局部变量长期持有 Java 对象;每次处理完立刻把局部对象变量置 nil;如果 API 返回列表、游标之类的对象,调用其 close 方法或者直接置 nil 触发 finalize。循环体里也别频繁 Create 同一个类对象,能复用就复用。这条规则在两套运行时里都是通用的,省出来的崩溃次数远超你想象。
5.3 API Level:生成时务必对照设备下限
XE10 时代默认 API 21,也就是 Android 5.0,但很多实际在线的设备还停留在 4.4。如果你生成的绑定涉及高版本 API 的类,代码里最好用 TOSVersion.Check 做运行时判断:
if TOSVersion.Check(21) then // 调用高版本 API else // 降级处理或提示用户同一个类在不同 API Level 下,构造函数和方法集合可能有差异,java2op 生成的是你指定的 android.jar 里的版本。所以选哪个 API 的 android.jar,直接决定绑定内容。生成时选高 API,而设备是低版本,不做检查就会直接崩。反过来,生成时选过低的 API,高版本设备上反而拿不到新能力。稳妥的做法是以项目最低支持版本对应的 android.jar 为准。
6. 给还在 XE10 上坚持的人:我的自动化与维护习惯
6.1 让生成过程可复现
JAR 升级、SDK 变更都会导致绑定过期,所以我早就不手动敲命令了,而是把一个一键脚本放进项目根目录:
@echo off set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_45 "C:\Program Files (x86)\Embarcadero\Studio\17.0\bin\java2op.exe" -c mylib_bind.cfg echo done配合版本管理,绑定文件、配置文件、JAR 一起提交,同事拉下来照着脚本跑,五分钟就能重新生成,不需要翻日志猜参数。建议把临时生成物加入忽略列表,只保留配置和产物 .pas,保持仓库干净。
6.2 当 java2op 力不从心时
java2op 不是万能的。碰到解析不了的奇怪 JAR——比如字节码混淆过度,或者用太新 Java 特性编译的类——与其死磕,不如直接在 Java 侧写一个很薄的适配类:把所有需要暴露给 Delphi 的逻辑封装成简单的静态方法或监听器,再用 java2op 只转换这个适配类。这一招我用了很多次,等于把复杂度隔离在 Java 一侧,Delphi 侧永远面对一层干净接口,问题立刻少一大半。这是我做了好几个项目之后悟出来的:绑定层越薄越稳。
6.3 绑定单元的命名与注释
生成的 .pas 名字不要用 Unit1 这类默认名,尽量与 SDK 对应:PushBinding.pas、PayBinding.pas。同时建议在单元开头写清这个绑定对应哪个 JAR、哪个配置文件、哪个日期生成。半年后回头维护时,这些注释比任何教程都有用。每次重新生成后顺手打开扫一眼 diff,重点看方法签名有没有变化、有没有新增必要的依赖类,这一步看似浪费时间,实际上能帮你提前发现上游 SDK 的破坏性变更,省得集成到一半再回退。
本文还有配套的精品资源,点击获取