java通过JNA调用动态库
前言
老规矩,先说下为什么会有这篇文章。近期对接了一个项目,应接口提供方要求,必须通过动态库调用,一个是为了安全可控,调用方不用知道内部实现,加密、解密、具体的逻辑不需要考虑,只需要调用即可;另一个是封装了统一的GUI界面。总之就是非用动态库不可,然后我查了很多资料,请教了几个大佬,最后在运气的加持下,终于调通了,但整个过程特别坎坷,所以我觉有必要记录下。需要说明的是我们这里采用的是JNA的方式
什么是动态库
说实话,一般我们不会有调用动态库的需求,因为这不是web开发的范畴,出发你涉及到嵌入式的开发,或者客户端开发。动态库也叫动态链接库,英文简写DLL,简单来讲,就是Windows下开发的程序模块,类似于java下的jar(不知道可不可以这样 理解)。它是实现Windows应用程序共享资源、节省内存空间、提高使用效率的一个重要技术手段。windows下它是以dll结尾的文件,比如:msctf.dll
百度百科给的解释是这样的:
动态链接库英文为DLL*,是Dynamic Link Library*的缩写。DLL是一个包含可由多个程序,同时使用的代码和数据的库。在Windows中,这种文件被称为应用程序拓展。例如,在 Windows 操作系统中,Comdlg32.dll 执行与对话框有关的常见函数。因此,每个程序都可以使用该 DLL 中包含的功能来实现“打开”对话框。这有助于避免代码重用和促进内存的有效使用。 通过使用 DLL,程序可以实现模块化,由相对独立的组件组成。例如,一个计账程序可以按模块来销售。可以在运行时将各个模块加载到主程序中(如果安装了相应模块)。因为模块是彼此独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才加载。
咱也不是特别知道,咱也不敢问,你现在只有保证知道动态库这样的东西就行了。
开整
Talk is cheap. Show me the code。先上代码,然后再解释
public class DllTest {
static {
String filePath = "D:\\dll\\"; // 这里是你的动态库所在文件夹的绝对路径
// 这里引用动态库和他的依赖
System.load(filePath + "mfc100.dll");
System.load(filePath + "mydll.dll");
}
public static void main(String[] args) {
String strUrl = "http://127.0.0.1/test";
String InData = "{\"data\":{\"operatorId\":\"test001\",\"operatorName\":\"超级管理员\",\"orgId\":\"123\"},\"orgId\":\"1232\"}";
byte[] OutData = new byte[1024];
String msg = CLibrary.INSTANCE.test(strUrl.getBytes(), InData.getBytes(), OutData);
System.out.println(msg);
try {
System.out.println(new String(OutData, "GBK"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// 这里是最关键的地方
public interface CLibrary extends Library {
// FS_CheckCode是动态库名称,前面的d://test//是路径
CLibrary INSTANCE = (CLibrary) Native.loadLibrary("mydll", CLibrary.class);
// 我们要调用的动态库里面的方法。
String test(byte[] strUrl, byte[] InData, byte[] OutData);
}
}
动态库里面的方法是这么定义的:
char* __stdcall test(char* strUrl,char* InData,char* OutData)
解释
小朋友,你是否有很多的问号❓😂没事,接下来我们就详细说明下。首先要关注的是java定义动态库接口方法,对应代码:
public interface CLibrary extends Library {
// FS_CheckCode是动态库名称,前面的d://test//是路径
CLibrary INSTANCE = (CLibrary) Native.loadLibrary("mydll", CLibrary.class);
// 我们要调用的动态库里面的方法。
String test(byte[] strUrl, byte[] InData, byte[] OutData);
}
其中,loadLibrary方法是创建动态库对象实例,第一入参是你要调用的动态库的名字,test方法对应动态库中的方法,这里需要注意的是jNA和动态库直接数据类型的对应关系,具体的对应看后面的附表。
这里还有一个需要注意的问题是是,动态库加载的问题:
// 这里引用动态库和他的依赖
System.load(filePath + "mfc100.dll");
System.load(filePath + "mydll.dll");
如果你没有把动态库放到classpath下,而且没有上面加载的代码,会报如下错误:
Exception in thread "main" java.lang.UnsatisfiedLinkError: Unable to load library 'NationECCode':
找不到指定的模块。
遇到的问题
JDK版本
如果完成了相应改造工作,你就可以直接运行了。如果你的JDK是64位,但你的动态库是32位(X86),它肯定会报如下错误:
java.lang.UnsatisfiedLinkError: D:\workspace\learning\github\httputil-demo\dll\mydll.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1824)
at java.lang.Runtime.load0(Runtime.java:809)
at java.lang.System.load(System.java:1086)
或者这样:
Exception in thread "main" java.lang.UnsatisfiedLinkError: %1 不是有效的 Win32 应用程序。
at com.sun.jna.Native.open(Native Method)
at com.sun.jna.NativeLibrary.loadLibrary(NativeLibrary.java:278)
at com.sun.jna.NativeLibrary.getInstance(NativeLibrary.java:455)
at com.sun.jna.Library$Handler.<init>(Library.java:179)
at com.sun.jna.Native.loadLibrary(Native.java:646)
at com.sun.jna.Native.loadLibrary(Native.java:630)
当然如果你的动态库是64位,JDK是32位(X86),同样也会报错(应该会,没试过)
解决方法很简单:
- 更换JDK版本
- 联系动态库封装的人,重新封装对应的版本
找不到模块
这个问题上面已经说过了,就是因为没有加载动态库文件,而且也没有把它和它的依赖文件放到classpath下,就会报这个错。
数据类型错误
这个问题本质上就是没有搞清楚JNA和动态库数据对应关系,我之前也没搞清楚,反复试了好多次才成功。然后在今天写这篇文章的时候,发现了char*
作为出参和入参对应的类型是不一样的,才恍然大悟。希望小伙伴在自己搞的时候一定看清楚。
出参未分配空间
和java不一样,动态库方法是把入参传给方法的,而且需要给出参分配空间,如果不分配内存空间,会报错:
java.lang.Error: Invalid memory access
这个问题也很好解决,就是给出参分配足够的空间:
byte[] OutData = new byte[1024];
到这里,所有问题都解决了,动态库也完美运行起来了。
总结
收获就一句话:对于自己没有做过的事,要积极尝试,积极思考,积极请教,然后问题解决后要积极分享。好了,祝大家周末愉快!
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 |
补充表:
Native Type | Java Type | Native Representation |
---|---|---|
char | byte | 8-bit integer |
wchar_t | char | 16/32-bit character |
short | short | 16-bit integer |
int | int | 32-bit integer |
int | boolean | 32-bit integer (customizable) |
long, __int64 | long | 64-bit integer |
long long | long | 64-bit integer |
float | float | 32-bit FP |
double | double | 64-bit FP |
pointer | Buffer/Pointer | |
pointer array | [] (array of primitive type) | |
char* | String | |
wchar_t* | WString | |
char** | String[] | |
wchar_t** | WString[] | |
void* | Pointer | |
void ** | PointerByReference | |
int& | IntByReference | |
int* | IntByReference | |
struct | Structure | |
(*fp)() | Callback | |
varies | NativeMapped | |
long | NativeLong | |
pointer | PointerType |
然后又找到一些补充资料:
C语言 | Java |
---|---|
char* | String (作为入口参数)/ byte[] (作为出口参数) |
unsigned char* | String (作为入口参数)(不确定,没具体使用过)/ Pointer (作为出口参数) |
int* | IntByReference |