Delphi XE10调用Java SDK:java2op工具从环境到APK完整实战
简介面向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 开发的人尤其是手里还压着 XE10RAD Studio 10 Seattle项目的老伙计应该都记得那个叫 java2op 的小工具。那几年论坛里一旦有人问Delphi 里怎么调 Java 第三方 SDK答案基本都指向同一个流程用 java2op 把 Java 类翻译成 Object Pascal 接口再引用进工程。听起来很丝滑但实际跑起来环境变量、配置文件、类解析失败、编译报错随便卡一步就是半天。这里我就把在 XE10 上把 java2op 从环境搭建到生成代码、再到打进 APK 的完整链路重新捋一遍附带这几年攒下的坑和排查思路。1. XE10 里的 Java 调用为什么绕不开 java2op1.1 先搞清楚它到底解决了什么问题XE10 时代Delphi 的 FMX 应用在 Android 上本质是一个原生 .so 进程而 Android SDK、第三方 SDK 都是 Java 运行时里的东西。Object Pascal 和 Java 两套运行时之间唯一正路就是 JNIJava 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 配一个老实的 JDKjava2op 本身是一个可执行程序但它解析 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(TJavaGenericImportJTextUtils, 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_HOMEC:\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 的破坏性变更省得集成到一半再回退。本文还有配套的精品资源点击获取