简介:一款针对 Delphi XE10.2 的 Java2OP 互操作工具,旨在帮助 Delphi 开发者解决 Java 的 JAR 包到 Delphi 可识别 PAS 单元文件的转换难题,使现有 Java 生态中的类库能够被直接复用。资源包为 RAR 压缩格式,共 8 个文件、总大小 16.4MB,包含 2 个 JAR 运行库、2 个 TXT 说明文档、1 个 LOG 日志、1 个 EXE 主程序、1 个 INC 与 1 个 XML 配置模板,完整覆盖程序启动、参数调整、运行记录和日志排查等环节。借助该工具,开发者无需手工编写桥接层,即可按步骤完成从解析 JAR 中类、接口和方法元数据,到生成对应 PAS 文件的全流程转换;随包配置模板还支持自定义包名映射、类型对应与调用约定,并能结合日志定位转换异常,整体上手门槛较低。需要注意,并非所有 Java 特性都能自动映射到 Delphi,随附说明文档针对这类边界情况提供了参考线索,能够帮助使用者快速调整生成结果,降低跨语言集成风险。目前已有 258 人学习下载,适合熟悉 Delphi 并希望对接 Java 资源、提升代码复用率的中高级开发人员在跨平台项目中应用。
1. java2op for XE10:把 Java 类库“搬”进 Delphi 的直通方案
做 Delphi XE10 Android 开发最头疼的一件事,就是 Java 生态里现成的 SDK 一大堆,但都是给 Android Studio 用的,Delphi 这边只能干瞪眼。java2op 就是 RAD Studio XE10 自带的一个命令行转换工具,它的作用是把 Java 类库自动转成 Delphi 能编译、能调用的 JNI 桥接单元。你手里有一个 jar 包或者一份 JNI 头文件,跑一条命令,就能生成对应的 .pas 文件,然后在 Delphi 里像调用普通类一样调用 Java 方法。这玩意儿不是万能的,但它能解决“Java SDK 怎么在 Delphi 里用”这个大问题。适合那些需要在 Delphi 里集成第三方 Java SDK 的开发者,比如支付、推送、音视频处理这类场景,能省掉大量手写 JNI 胶水代码的时间。
2. 动手前先摸清三个输入:jar 包、JNI 头文件与 Java2OP 的边界
2.1 从 jar 生成 JNI 头文件:javah 在 JDK 8 与新版 JDK 的参数差异
Java2OP 不能直接吃 jar 包,它需要的是 JNI 头文件(.h)。所以第一步是把 jar 转成 .h,这一步用的是 JDK 自带的 javah 工具。我在 XE10 时代用的是 JDK 8,javah 的命令很简单。假设你有一个 SDK.jar,包名是 com.example.sdk,类名是 MainApi,命令长这样:
# 设置 JAVA_HOME 后,在 jar 所在目录执行 javah -classpath SDK.jar -d . com.example.sdk.MainApi-classpath指定 jar 路径,-d指定头文件输出目录,最后是完整类名。这个命令会在当前目录生成一个com_example_sdk_MainApi.h文件,文件名是包名加类名,用下划线连接。如果 jar 里有多个类,需要每个类都跑一次,或者直接用com.example.sdk.*这样的通配符。
这里有个版本差异要注意:JDK 8 里 javah 是独立工具,JDK 10 开始 javah 被移除了,功能并进了 javac。也就是说,如果你用的是新版 JDK,得改用javac -h来生成头文件,参数也有变化。我的建议是,除非项目强制要求,否则 XE10 开发环境老老实实装一个 JDK 8,专门用来做转换,省得跟新 JDK 的兼容性问题纠缠。
2.2 Java2OP 的两种起点:选 .h 还是 .class?
Java2OP 其实支持两种输入:一种是上面生成的 .h 头文件,另一种是 .class 文件。实际工作中我几乎只走 .h 这条路。原因很简单:Java2OP 解析 .class 文件时,对内部类、匿名类、泛型的支持不稳定,生成的代码经常漏方法或者类型对不齐。而 .h 头文件是 C 风格的纯函数声明,解析起来干净利落。
但 .h 头文件也有一个问题:javah 生成的 .h 里,方法声明带有JNIEXPORT和JNICALL宏,还带了jobject、jstring这些 JNI 类型。Java2OP 能处理这些,不过它对头文件里的标准头引用(比如#include <jni.h>)是无视的,因为转换后的 .pas 单元本身就要依靠 Delphi 自带的 JNI 类型定义。所以你在看生成的 .pas 时,会发现它 uses 了Androidapi.JNI.JavaTypes和Androidapi.JNI.JNIConst,而不是去引用 jni.h。
2.3 生成前的准备:在 Delphi 里建立 Android 平台的库文件搜索路径
很多人忽略这一步,觉得生成 .pas 之后直接编译就行。实际上,生成的单元编译时需要 JNI 相关的支持单元,而这些单元在 Delphi 的默认搜索路径里不一定找得到。我一般在 Library 路径里把 RAD Studio 的$(BDSLIB)\android\release加进去,具体位置在 Tools -> Options -> Delphi Options -> Library 里操作。
还有一个更隐蔽的准备项:如果 jar 里用到了 Android 框架以外的类,比如 JSON 库、网络库,生成的 .pas 里会引用对应的 Java 类。这时候 JNI 桥接单元,比如Androidapi.JNI.JavaTypes、Androidapi.JNI.JavaNet里面可能没有对应声明,编译直接报错“找不到类型”。所以在动手之前,先打开 jar 看一眼它的依赖,心里有个数。常见做法是先把常见的java.*、android.*、org.json.*这些包扫一遍,确认它们在 Delphi 的 JNI 单元里都有映射,不然生成的代码根本过不了编译。
3. 用 Java2OP.exe 生成 Delphi 单元:完整命令与四类开关
3.1 核心命令:从 .h 到 .pas 的最小可运行示例
Java2OP.exe 在 RAD Studio 的 bin 目录下,XE10 对应的是 Studio 18.0 版本目录。最基础的一条命令:
"C:\Program Files (x86)\Embarcadero\Studio\18.0\bin\java2op.exe" -unit com_example_sdk_MainApi.h -o MainApi.pas-unit指定输入头文件,-o指定输出 .pas 文件名。跑完这条,你会得到一个MainApi.pas,里面的类名会是JMainApi,继承自JObject。这个命名规则是固定的:Java 类名 MainApi 前面加 J,所以是 JMainApi。
命令本身没什么玄学,但有一点要注意:java2op是按文件处理的,如果你的 jar 里有十几个类,那就得跑十几条命令。这不是问题,问题是生成的这些 .pas 单元之间存在相互引用,比如 A 方法返回 B 类型,生成的代码里 A.pas 会 uses B.pas。所以要么你一次把所有 .h 都转换,要么按依赖关系分批转换。我最开始偷懒一次只转一个类,结果编译的时候报一堆“未定义类型”,后来老老实实写了一个批处理脚本全量转。
3.2 -package 与 -namespace:Java 包到 Delphi 单元的映射策略
Java 的世界里,包名决定类的全限定名,比如com.example.sdk.MainApi。Delphi 这边没有包的概念,只有单元名和命名空间。Java2OP 提供了-package参数来指定只处理某个包下的类,-namespace参数可以把 Java 包名映射成 Delphi 命名空间,这样生成的代码里会把com.example.sdk.MainApi引用成MainApi。
实际项目里怎么配?我建议的映射策略是:保留包名前缀作为 Delphi 命名空间,这样能避免类名冲突。比如:
java2op.exe -unit com_example_sdk_MainApi.h -o SDK.MainApi.pas -namespace com.example.sdk=SDK-namespace后面的com.example.sdk=SDK表示把com.example.sdk这个包映射成SDK命名空间。效果就是生成的JMainApi类声明会带SDK前缀,调用的时候写SDK.JMainApi。这样如果两个不同的 jar 都有Utils类,不会互相覆盖。参数具体写法不用死记,用起来自然会有感。
3.3 配置文件的写法:/classes、/include 与类型映射表
参数只是控制转换行为的开关,真正决定转换内容是加载一个配置文件。Java2OP 默认会读取java2op.cfg,文件里可以指定要转换的类、需要排除的方法、以及类型映射规则。一个典型的配置文件长这样:
# java2op.cfg /classes com.example.sdk.MainApi /classes com.example.sdk.BuildConfig /exclude com.example.sdk.BuildConfig /include com.example.sdk.SDKConfig/classes指定要处理的类,/exclude排除某些类,/include强制包含某些默认会被忽略的类。Java2OP 默认会跳过 final 类、接口类这些它认为不可继承的类型,但有时候你正好需要调它的静态方法,就得用/include强拉进来。
配置文件里还可以写类型映射,作用是把 Java 类型映射成 Delphi 类型。最常见的一个:把java.util.Map映射成JMap,把java.util.List映射成JList,生成的代码就能直接用 Delphi 侧的定义,而不是反复引用 JavaTypes 单元。这个是 Java2OP 机制里比较关键的部分,因为默认映射经常把集合类型当成普通JObject,导致你拿到一个 List 却不知道里面是什么元素类型的尴尬情况。
3.4 生成结果的三个检查点:声明完整性、方法重载与泛型擦除
生成完 .pas,别急着进 Delphi 编译。我每次都会先打开文件,按照三个检查点过一遍:
检查点一是声明完整性。Java 的 public 方法有没有全部映射?我碰到过一个情况:Java 里一个方法名是getUserInfo(),生成的 .pas 里方法名变成了getUserInfo——这没问题。但如果 Java 方法名是setName(String name),而生成的代码里参数类型变成了JString而不是String,这是正常的,因为 JNI 层就是 JString。真正的问题是一些重载方法会被吞掉,Java2OP 输出重载方法时会加_1、_2后缀,比如setData_1。如果前缀对不上,说明解析头文件时出了问题。
检查点二是静态方法和实例方法。Java 的静态方法生成到 Delphi 里是类方法(class function),实例方法是普通方法。如果你发现本该是静态的方法变成了实例方法,那就是 Java2OP 没有正确识别static修饰符,这种情况一般出现在接口默认方法上。
检查点三是泛型擦除。Java2OP 不支持泛型参数解析,List<String>在生成的代码里是JList,List<Integer>也是JList。如果你需要精确区分泛型类型,比如要调用add(Integer)而不是add(Object),要么手写 JNI 辅助代码,要么修改生成的 .pas 把参数类型改掉。后者的血泪经验是不要直接改生成的文件,因为你以后重新生成还得再改一遍,最好写一个后处理脚本统一替换。
4. 类型映射与字符串/异常:转换后的代码为什么不能直接编译,以及怎么修
4.1 JString 与 String 的边界:UTF-16 与 UTF-16LE 的坑
java2op 生成的代码里,Java 的java.lang.String映射成JString。JString 在 Delphi 的 JNI 单元里定义是一个完整类型,它是对 JNI 字符串类型的封装,不能和 Delphi 的string直接互相赋值。要把 JString 转成 Delphi string,需要调用JStringToString方法,或者直接在表达式里赋值,Delphi 的 JNI 单元实现了隐式转换——但方向只有一个:JString 可以赋给 string,string 赋给 JString 不保证能行。
这个坑我踩过不止一次。Java 字符串内部是 UTF-16 编码,Delphi 的 string 在 Windows 和 Android 上也是 UTF-16。看起来天作之合,但 JNI 层的GetStringUTFChars返回的是 UTF-8,NewStringUTF接收的也是 UTF-8。Java2OP 生成的 JNI 调用代码,会自动在 JNI 边界做转换,但它默认的转换规则是基于 Android API 的 JNI 实现,绕过了 JVM 的 UTF-8。这个细节在字符串包含非 ASCII 字符时特别容易翻车——比如中文、日文、表情符号。
实际的做法是,如果你需要传一个中文参数给 Java 方法,不要直接写jMethod.setName(中文字符串)或者直接参数传一个 Delphi 中文字符串。先用CreateJString或者StringToJString封装一次,再把 JString 传给方法。Java2OP 生成的代码里没有自动做这一步,所以这是最容易出现“调通了但内容是乱码”的地方。
4.2 异常与 null 的语义差异:JNI 异常机制在 Delphi 里的表现
Java 方法抛异常的时候,在 JNI 层不会自动中断执行,它只是把异常挂在那里,等下一个 JNI 调用时返回“有异常待处理”的状态。Java2OP 生成的代码根本没有处理这个逻辑。也就是说,如果 Java 方法抛了异常,Delphi 端调完方法后,返回值是一个无效对象,代码继续往下走,直到你下次调用某个 JNI 方法时,才突然看到异常信息,程序才崩溃。这种问题排查起来相当抓狂。
常见的做法是在调用 Java 方法之后,手动检查JNIEnvironment^.ExceptionCheck来判断是否发生了异常。Java2OP 不生成这些检查代码,需要自己封装。一位老师教过我一个习惯:凡是够重要、返回值为对象的方法,外面包一层封装函数,在封装里检查异常并抛出 Delphi 侧的自定义异常。
至于 Java 的 null,在 Delphi 侧就是 nil(或 JNI 的 nil 对象)。Java 方法返回 null 时,生成的代码里返回类型是某个 JObject 的子类,Delphi 端拿到的是 nil。但问题是——Delphi 的对象方法调一个 nil 对象,会直接崩溃。所以判断if JResult = nil时,必须做空值检查,比 Java 端更严格。
4.3 静态方法与实例方法的调用差异:双参数 this 与 JNI 反射
Java2OP 生成的静态方法调用是走 JNI 的静态方法路径:CallStaticObjectMethod,不需要传对象实例。实例方法走CallObjectMethod,需要传 JNI 的jobject指针。这个差异在 Delphi 侧封装得比较干净,你不需要手动传 this,调用实例方法的时候,Delphi 会自动把当前对象作为第一个参数传进去。
但你会踩一个更加隐蔽的坑:Java 的Overloaded方法,比如同名方法一个有返回值一个没返回值(Java 允许通过返回值区分重载),Java2OP 生成的代码会用_1来区分,你要看清楚调的是哪个版本。更麻烦的是静态工厂方法返回的实例,比如 Java 的Singleton.getInstance(),它生成的返回值类型是JObject,你需要手动强转类型,JMainApi(wrapper.getLast())这种方法才能调用真正类的方法。
还有一个反射相关的坑:Java2OP 默认把 Java 类的字段(Field)生成为属性(property),但访问这些属性底层 JNI 调用路径是GetObjectField/SetObjectField。如果你 Java 类里有final字段,生成的 Delphi 属性只读部分有实现,写不进去——编译能通过,运行时会得到“字段不可修改”的 JNI 异常。最直接的解法是找到那个字段名,在生成的 .pas 里手动把该属性的写方法去掉,或者改成调用一个 Java 端的 setter。别跟生成器较劲,改文件即可。
5. java2op 实战避坑:编译错误、链接错误与运行时崩溃的 5 条排查记录
5.1 现象一:生成的 .pas 里 JNIConv 未定义
这个错误特别容易出现在第一次用 java2op 的 XE10 用户身上。生成的 .pas 文件里开头有一行uses Androidapi.JNI.JNIConst,但编译时提示找不到或类型不完整。原因是你手上的 RAD Studio 版本对应的 JNI 单元里没有提供JNIConv,或者你安装的版本默认没有把这些单元加进搜索路径。
解决办法不是去下载某个 JNI 支持包,而是先确认你的 Delphi 版本里Androidapi.JNI.JNIConst和Androidapi.JNI.JavaTypes是否真的存在。在 XE10 的安装目录里搜索一下.pas文件,找不到的话,说明你装的是精简版或者定制版,缺少 Android 相关组件。最保险的做法是重装时勾选 Android 开发组件。如果文件在但编译报错,就检查 Library 路径是否包含了 Android API 的映射目录。这个报错本质上是环境问题,不是代码问题。
5.2 现象二:调 JNI 方法时崩溃在本地代码边界
表现在 Delphi Android 应用运行到调用某个 Java 方法的代码行时,直接闪退,日志显示“SIGSEGV in native code”或者干脆没有崩溃日志。最典型的原因是 javah 生成的头文件和运行时的实际 Java 类方法签名不一致。Java2OP 按头文件生成方法签名,但 JNI 查找方法依赖完整的签名描述符,如果 Java 类版本更新过——方法参数变了、返回值变了——头文件没有重新生成,Java2OP 生成的代码还拿着旧签名去匹配 Java 方法,找不到方法时 JNI 会炸。
解决方法是重新生成一次 .h,再重新跑一次 Java2OP。如果重新生成后问题还在,排查一下是不是 Java 方法有 native 关键字,Java2OP 对 native 方法会生成一个 Delphi 外部函数声明,但实现需要你手动用汇编或 JNI 辅助代码去补,默认会指向空指针,调用必崩。遇到这种情况建议绕开。
5.3 现象三:字符串转换出现乱码或长度翻倍
传中文字符串给 Java,Java 端收到的是乱码;或者 Java 返回的字符串在 Delphi 端显示每个汉字中间多了个空格。Java2OP 对字符串的默认处理跟 Android 平台的编码强相关,而 Android 的 JNI 字符串转换函数在一些旧版本上默认编码跟 Delphi 的string不一致。正规做法是自己写一个字符串转换函数,比如传参前先构造一个JString给 Java 方法,而不是直接传 Delphistring让编译器隐式转换。
具体到代码上,把这种模式当成习惯:
function JavaStringToDelphi(AJString: JString): string; begin if AJString <> nil then Result := JStringToString(AJString) else Result := ''; end;如果用了这个方法还乱码,检查一下你的 Delphi 工程是不是开启了预编译宏,某些 Debug 版本会改变字符串管理方式,导致默认转换函数行为异常。这种问题很少见,但排查起来很费神。
5.4 现象四:同一个 jar 在 XE10 和 XE8 下生成的声明不一致
如果你手上有两个开发环境,或者从别人那里拿了一份旧的转换结果,你会发现在 XE8 下生成的 .pas 里某个类方法返回的是TJavaArray,而在 XE10 下生成的是TJavaArray<JByte>——泛型支持程度不一样,导致代码不能直接复用。这不是 Java2OP 的问题,是 Delphi 的 JNI 单元版本差异问题。很直接的影响是,你在 XE10 下写好的调用代码,放到 XE8 下可能编译不过。
规避方法是统一开发环境。交付给同事或客户时,明确标注生成这个 .pas 的 Delphi 版本。Java2OP 生成的代码没有版本标记,所以建议在批处理脚本里把版本号写进输出文件的注释头,不然半年后你根本想不起来这个文件是用什么版本生成的。
5.5 现象五:Delphi 10.1 Berlin 的 Android 目标与旧版 Java NDK 冲突
这个坑比较深。XE10 自带的 JNI 单元是配合特定 NDK 版本测试的,但你的开发机可能装了新版 NDK,或者用的 Android SDK 版本太新,导致生成的代码在编译阶段报“找不到 android/log.h”之类的头文件错误。报错位置通常不在 .pas,而是在 Delphi 的 JNI 相关支持库里。
我当时用 XE10 配一个比较新的 Android SDK,结果工程编译时链接器报了一堆jni.h相关的错误。折腾一番后发现是 SDK 版本太新,Delphi 10.1 Berlin 不认。解决办法是回退 Android SDK 版本,或者用 XE10 文档中明确兼容的 NDK 版本。这部分没有捷径,只能按官方要求配环境。建议在做任何 Java2OP 转换之前,先跑一个空的 Android 工程确认基本环境是好的,别等集成到一半再排查环境问题。
6. 批量处理与自动化:把转换脚本固化下来的三个习惯
Java2OP 最让人难受的是它对每个文件单独处理,类的依赖关系需要你自己管理。一个 jar 里有二三十个类是常有的事,所以手动一条条跑命令行是不现实的。我后来的做法是写一个批处理脚本,一次性处理所有 .h 文件,而且用配置文件统一管理排除和类型映射。
@echo off set JAVA2OP="C:\Program Files (x86)\Embarcadero\Studio\18.0\bin\java2op.exe" set SRC_DIR=.\headers set OUT_DIR=.\units for %%f in (%SRC_DIR%\*.h) do ( %JAVA2OP% -unit "%%f" -o "%OUT_DIR%\%%~nf.pas" -config=java2op.cfg ) echo Done.这个脚本干的事情很简单:遍历 headers 目录下所有 .h 文件,逐个调用 Java2OP 生成 .pas,输出到 units 目录。-config=java2op.cfg指定全局配置文件。有了这个基线,每次更新 jar 后只需要重新跑一遍脚本,不用重复手工操作。
在这个基础上,我建议固化三个习惯。第一,每次重新生成后用 Beyond Compare 对比旧的 .pas 和新的 .pas,确认改动对现有代码的影响范围,而不是直接覆盖。第二,对于生成后需要手工修改的类型或方法,做一个独立的补丁脚本,用文本替换来修改生成的代码,保证重新生成后能一键带补丁,避免手工改完下次生成又丢了。第三,在生成的 .pas 文件头部加一行注释,记录 jar 的版本号、生成日期和所用 Java 版本,为以后排查问题节省一大半时间。
验证生成的代码能不能用,我的一般流程是写一个最小的 Android 测试工程,只调用一个最简单的 Java 方法——比如一个返回 int 的 getVersion()——跑通了再逐步增加复杂方法。别一上来就调整个 SDK,JNI 层的问题没有日志,全靠二分法定位,一次引入一个变量才能快速锁定问题。这个习惯让我避免了好几次深夜 debug 的崩溃现场,也让我真正意识到 Java2OP 只是一个起点,真正决定效率的是你围绕它建立起的脚本和验证流程。希望帮到你。
本文还有配套的精品资源,点击获取