Java本地方法(Native Method)通过JNI(Java Native Interface)提供的一系列API调用其他语言的函数实现的相对底层的功能。
当需要实现的功能依赖操作系统底层的特性,单纯依靠Java无法独立完成,需要借助其他语言;
保证Java在跨平台的同时实现对底层的控制;
在Java中直接调用其他语言已经实现的现成功能;
使用更加底层的语言实现程序对时间敏感性和性能的要求。
JNI
依赖动态链接库,但是动态链接库并不跨平台 **;Java通过JNI调用C/C++编译链接后的动态链接库来实现相对底层的功能。
但是C/C++或其他语言编译链接生成动态链接库又是依赖具体操作系统平台的,因此每种平台都有自己的一套动态链接库。而每个平台都有自己的JVM,不同平台下的JVM,会去加载某个固定类型的动态链接库文件,使得依赖于操作系统的功能可以被正常的调用,JVM才能正常跨平台,这一过程可以参考下面的图来进行理解:
package com.wkw.study.jni;
public class JNITest {
static{
//从 java.library.path 路径上加载动态链接库
System.loadLibrary("MyNativeDll");
}
//定义native方法
public static native void callCPPMethod();
public static void main(String[] args) {
//输出 java.library.path 具体路径
System.out.println("DLL path: " + System.getProperty("java.library.path"));
//调用动态链接库中的具体方法
callCPPMethod();
}
}
代码主要完成的工作:
loadLibrary
方法加载本地java.library.path
系统变量定义下的动态链接库,参数为不包含扩展名的动态链接库库文件名。在windows
平台下会加载dll
文件,在Linux
平台下会加载so
文件,在Mac Os
下会加载libXxx.jnilib
文件,在本例中,会加载libMyNativeDll.jnilib
动态链接库库文件。JDK命令生成头文件:
javac -h ./ JNITest.java #-h指定生成头文件的目录
该命名会生成两个文件:首先生成JNITest.class
,其次在指定目录下生成C/C++头文件com_wkw_study_jni_JNITest.h
头文件com_wkw_study_jni_JNITest.h
定义了一个方法Java_com_wkw_study_jni_JNITest_callCPPMethod
,这个方法对应定义的Java native方法public static native void callCPPMethod()
。
JNI在C/C++语言中定义的规则:Java_包名_类名_方法名
,头文件命名规则:包名_类名.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_wkw_study_jni_JNITest */
#ifndef _Included_com_wkw_study_jni_JNITest
#define _Included_com_wkw_study_jni_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_wkw_study_jni_JNITest
* Method: callCPPMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_wkw_study_jni_JNITest_callCPPMethod
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
可以看到头文件中只有函数声明,没有具体实现:
extern "C"
表示这部分代码使用C语言规则来进行编译和连接的;JNIEXPORT
和JNICALL
是头文件jni.h
中定义的两个宏,使用JNIEXPORT
支持在外部程序代码中调用该动态库中的方法,使用JNICALL
定义函数调用时参数的入栈出栈约定;Java前缀+包名+类名+方法名
组成,在该方法中有两个参数,通过第一个参数JNIEnv *
的对象可以调用jni.h
中封装好的大量函数 ,第二个参数代表着native方法的调用者,当Java代码中定义的native方法是静态方法时这里的参数是jclass,非静态方法的参数是jobject。#include "com_wkw_study_jni_JNITest.h"
#include
JNIEXPORT void JNICALL Java_com_wkw_study_jni_JNITest_callCPPMethod (JNIEnv *, jclass) {
printf("**CPP Method**\nprint from cpp");
}
引用头文件并实现其中的函数,也就是native方法将要实际执行的逻辑,CPP文件名随意,本例中为callCPPMethod.cpp
gcc -I"./" -I"/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include" -I"/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include/darwin" -shared -o libMyNativeDll.jnilib callCPPMethod.cpp
这里使用gcc
编译器进行编译:
-I
指定头文件的路径,本例中需要3个头文件jni.h
、jni_md.h
和com_wkw_study_jni_JNITest.h
,所以引入了3次:
com_wkw_study_jni_JNITest.h
在当前目录下;
jni.h
在/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include
目录下;
jni_md.h
在/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include/darwin
目录下;
2. -shared
指定生成动态链接库,如果不使用这个标志那么外部程序将无法连接;
3. -o
指定目标的名称,这里将生成的动态链接库命名为libMyNativeDll.jnilib
;
4. callCPPMethod.cpp
为被编译的C/C++源程序文件名。
gcc
命令具体过程:
当前已经完成自定义一个Java native方法的所有流程,来看下生成的所有文件:
接下来测试验证跑下JNITest.main
方法,异常:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no MyNativeDll in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at com.wkw.study.jni.JNITest.<clinit>(JNITest.java:6)
找不到动态链接库MyNativeDll
。这是因为System.loadLibrary("MyNativeDll")
是从系统变量java.library.path
指定的路径中找libMyNativeDll.jnilib
动态链接库文件并进行加载,因此需要将libMyNativeDll.jnilib
放到java.library.path
指定的路径下,有两种方法:
java.library.path
的路径指定为当前libMyNativeDll.jnilib
库文件所在的路径:-Djava.library.path=/Users/developmac/workspace/develop/ideaWorkspace/comprehensive-learning/web-study/src/main/java/com/wkw/study/jni/
libMyNativeDll.jnilib
库文件拷贝到默认的加载目录下,具体的路径可以通过System.getProperty("java.library.path")
获取,该方法可能会获得多个目录,放在任意一个目录下即可。再次启动,运行符合预期:
DLL path: /Users/developmac/workspace/develop/ideaWorkspace/comprehensive-learning/web-study/src/main/java/com/wkw/study/jni/
**CPP Method**
print from cpp
JNI调用过程:
从代理模式角度来看:
代理角色:包含native方法的JNI类
实现角色:C/C++或其他语言实现的动态链接库
客户端:调用native方法的java类程序
接口(抽象角色):在JNI中接口这一角色的存在感相对薄弱,因为JNI是跨语言的,所以说无法严格的定义一个接口并让它同时应用于Java和其他语言。但是通过生成的.h
头文件,在一定程度上实现了从接口规范上统一了Java中native方法和其他语言中的函数。
所以上图中让客户端的调用过程跳过了接口,直接指向了代理角色,再由代理角色调用实现角色完成功能的调用。
总的来说,JNI起到了一个代理或中介的作用,与常见代理不同的是这里只做方法的调用,而不实现逻辑上的增强。
参考:
Java筑基 - JNI到底是个啥
Mac OS上编译JNI的动态库