背景
最近在做一个与硬件和第三方平台有关的项目,平台厂商扔过来一个DLL,但我方平台是Java编写的所以需要实现Java调用C的DLL。特此做了一些调研,现在记录一下。
主要实现途径
脚本
其实Java调用其他程序最简单的方式就是直接通过shell或是bat脚本调用,但这只局限于一些简单没有交互的应用,这里就不展开讨论。
JNI
简单介绍
JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。 [1] 从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。
使用JNI往往意味着复杂性提高一个数量级,对于未来的扩展和维护都极为不便,但可能 是不得不使用这样的技术,原因很可能是受限的SDK或是极高的性能要求。PS要是C与Java的性能差了一个数量级,主要原因有两个,一种是可能是没有充分的预热,java还没有走JIT,另一种可能是代码本身写的有问题。
基本流程
静态注册
原理:根据函数名建立 Java 方法和 JNI 函数的一一对应关系。流程如下:
- 先编写 Java 的 native 方法;
- 然后用 javah 工具生成对应的头文件,执行命令 javah packagename.classname可以生成由包名加类名 命名的 jni 层头文件,或执行命名javah -o custom.h packagename.classname,其中 custom.h 为自定义的文件名;
- 实现 JNI 里面的函数,再在Java中通过System.loadLibrary加载 so 库即可;
动态注册
原理:直接告诉 native 方法其在JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系,步骤:
- 先编写 Java 的 native 方法;
- 编写 JNI 函数的实现(函数名可以随便命名);
- 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
- 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
- 在 JNI_OnLoad 方法中调用注册方法;
- 在Java中通过System.loadLibrary加载完JNI动态库之后,会调用JNI_OnLoad函数,完成动态注册;
JNA
JNA是除了JNI在Java跨语言调用最为知名的库。JNA包括一个特定于平台的小型共享库,该库支持所有本机访问。由于实际项目使用的就是JNA,下面详细介绍一下JNA。
基本原理
也就是说JNA里面包括了一个DLL或是so库,你的JAVA代码调用JNA的jar包,这个jar包再去调用他的中间库,然后中间库再去处理真正的C/C++的库。
这么说起来是不是跟JNI的实现思路是一样,就是更加简单,不需要把你的java编译为头文件然后包含到对应库,而是借助中间库来实现。
默认类型对应
JNA面对的问题有一半是与类型有关的,所以在开发有关项目时搞清楚对应类型关系是十分重要的
Native Type | Size | Java Type | Common Windows Types |
char | 8-bit integer | byte | BYTE, TCHAR |
short | 16-bit integer | short | WORD |
wchar_t | 16/32-bit character | char | TCHAR |
int | 32-bit integer | int | DWORD |
int | boolean value | boolean | BOOL |
long | 32/64-bit integer | NativeLong | LONG |
long long | 64-bit integer | long | __int64 |
float | 32-bit FP | float | |
double | 64-bit FP | double | |
char* | C string | String | LPCSTR |
void* | pointer | Pointer | LPVOID, HANDLE, LPXXX |
这里面有一个特别的问题一定要注意。
- C/C++中的char相当于Java中的byte。
- C/C++中的char相当于Java中的byte。
- C/C++中的char相当于Java中的byte。
这个问题特别恶心,Java中的char是0~255,C/C++的char是-127到128。所以千万不要用Java中的char[]来接C/C++中的char数组或是指针。
代码示例
我的库文件是放在工程目录的natives中,名字叫dll。为了隔离工具类,做了一个DemoService中间层。所有的内部服务都调用DemoService不直接与JNA有任何关系。还做了一整套的DTO来隔离类型上的耦合。
pom.xml
<!-- https://mvnrepository.com/artifact/net.java.dev.jna/jna -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.5.0</version>
</dependency>
接口
public interface DemoLibrary extends Library {
DemoLibrary INSTANCE = Native.load("natives/dll", DemoLibrary .class);
int Do_Something();
}
中间Service
public class DemotService {
private static final String SDK_NAME = "dll";
private static volatile boolean initialized;
public TransportService() {
initialized = false;
}
@PostConstruct
public void init() {
DllUtil.loadNative(SDK_NAME);
initialized = true;
}
@PreDestroy
public void clear() {
}
public void do(){
getInstance().Do_Something();
}
private DemoLibrary getInstance() {
return DemoLibrar.INSTANCE;
}
}
工具类
package com.zw.ump.gateway4g.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
/**
* 加载DLL库的工具类
* @author zew
*/
@Slf4j
public class DllUtil {
public synchronized static void loadNative(String nativeName) {
String systemType = System.getProperty("os.name");
String fileExt = (systemType.toLowerCase().contains("win")) ? ".dll" : ".so";
String path = System.getProperty("user.dir")+ File.separator+"natives"+File.separator+nativeName+fileExt;
File sdkFile = new File(path);
System.load(sdkFile.getPath());
log.info("------>> 加载SDK文件 :" + sdkFile + "成功!!");
}
}
效率问题
- 直接使用原生的方法
- 不要使用非映射类型,比如String,直接使用原生方法
- Java原语数组通常比直接内存(指针,内存或ByReference)或NIO缓冲区使用速度慢
- 大型结构体也会有一定的性能问题
- 最后错误再抛出错误
JNative
基本跟JNA十分类似。
基本流程
- 下载jnative。jar 及JNativeCpp.dll
- 将使用的dll文件及JNativeCpp.dll拷贝至系统system32下
- 写代码
public class JNativeTest {
// 1.实现demo.dll 文件接口
public interface DemoLibrary extends Library {
// 2.PegRoute.dll 中 HCTInitEx方法
public int do(int Version, String src);
}
public static void main(String[] args) {
//3.加载DLL文件,执行dll方法
DemoLibrary libary = (DemoLibrary) Native.loadLibrary("demo",
DemoLibrary.class);
if (libary!= null) {
System.out.println("DLL加载成功!");
int success = libary.do(0,"");
System.out.println("1.设备初始化信息!" + success);
}
}
}
结论
- JNI没有额外的中间层,效率应该是最高的。当然我没有进行过多的调研和实际的基准测试,所以我只能说JNI应该比JNA效率高一点。
- JNA经过了额外的中间库,但是由于不需要JAVA编译h导入库中过程,使用起来较为方便,对于只会Java的同学是最为合适的。
- JNI用的人比较多,但相对来说比较麻烦要熟悉c并且要使用javac 及javah命令,同时JNI能做到JNA实现不了的那就是通过C也可以通过JNI来调Java的内容,JNA只能单向调用。
- JNA与JNative类似,但感觉JNA可以更好的分层。而且JNative要下载单独的dll。而且JNative貌似很久不更新了,并不建议。
参考
https://blog.csdn.net/u011627980/article/details/51970658
https://www.jianshu.com/p/ac00d59993aa
https://baike.baidu.com/item/JNI/9412164