Java底层-本地接口(JNI)

时间:2022-07-22
本文章向大家介绍Java底层-本地接口(JNI),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前面的文章我们讲述了类加载子系统、执行引擎、GC子系统、运行数据区的相关内容,那么HotSpot虚拟机三大子系统、两组件就剩下本地接口组件没有讲述了,所以这一篇文章对于本地接口进行介绍。

为什么会有本地接口的概念呢?我们在之前的文章说过Java语言不是面向硬件的,它无法直接调用操作系统API操控硬件,Java和硬件的交互正常都是通过JVM提供的API来完成的,但是当虚拟机提供的API不足以实现我们个别需求的时候,就需要本地接口了。由于JVM底层就有C++的影子,所以JVM也提供了JNI(Java本地接口:Java Native Interface,)技术作为其它语言(主要是C/C++)通信的API。在本地接口的相关的概念里,Java成为了一个调用方,其他语言成为了主角,这篇文章我们以C++为例,看一下如何调用本地接口,关于C++的部分尽可能简单的描述,如果大家还有疑问可以了解一些C++的基本知识。

首先编写调用本地方法的类,方法定义为native,表明调用的是本地接口。

package com.studyjava.email.jni.main;

public class Main {
    //加载Library,将Main资源库加载到内存中
    static {
        System.loadLibrary("Main");
    }
    public native void studyJava();
    public static void main(String[] args) {
        new Main().studyJava();
    }
}

定义完native方法之后,就需要我们使用C++去实现本地接口供Java使用,但是Java调用的本地接口并不是随意一个本地方法就可以调用的,这些本地接口定义是存在一些规律的,如果我们不知道这些规律可以通过javah命令生成,javah命令可以将java文件生成一个头文件(.h后缀的文件),这种转换会将native方法抽离到头文件中,我们可以认为头文件很像一个接口,Java可以通过头文件来调用资源的库程序,在一些特殊场合下,C/C++的源代码如果不便(或不准)向用户公布,那么只要向用户提供头文件和二进制的库即可。

javah -d jni com.studyjava.email.jni.main.Main

当然我们可以不使用javah命令,这里也可以自己去写.h文件,但是要保证格式是正确的,如下代码。

注:代码include就类似我们Java的import一样,

Java_com_studyjava_email_jni_main_Main_studyJava就是我们要实现的方法

#include <jni.h>
/* Header for class com_studyjava_email_jni_main_Main */

#ifndef _Included_com_studyjava_email_jni_main_Main
#define _Included_com_studyjava_email_jni_main_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_studyjava_email_jni_main_Main
 * Method:    studyJava
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_studyjava_email_jni_main_Main_studyJava
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

当头文件有了之后,我们我们就需要编写它的实现,首先定义.cpp文件(可以使用C++开发工具或者纯文本编写也是可以的), cpp文件中首先引入我们生成的头文件,引入头文件之后,我们就对上面头文件的方法 JNIEXPORT void JNICALL Java_com_studyjava_email_jni_main_Main_studyJava 进行实现,这里 使用print函数(或者cout)打印了"每天学Java"。

#include <jni.h>
#include <iostream>
#include "com_studyjava_email_jni_main_Main.h"
JNIEXPORT void JNICALLJava_com_studyjava_email_jni_main_Main_studyJava(JNIEnv *, jobject){
    printf("每天学Java");
    return;
}
int main(){
    return 1;
}

编写.cpp文件就类似我们写接口的实现类,当我们编写完之后,需要将cpp文件编译成.jnilib文件(我这里是Mac操作系统,如果是Windows需要转为.dll文件,Linux/Unix则为.so文件,这个命令的具体含义放在文末)。

g++ -shared -I /Users/.../include/Users/.../include/main.cpp-o libMain.jnilib

如果大家没有C++环境可以百度部署下,MacOS中如果安装XCode,会自带C++环境。

有C++环境之后,编译过程中可能会出现下面报错,

 'jni.h' file not found

这是因为我们引入的jni.h找不到,我们可以将jdk路径下的include复制出来,和上面编译的文件同级, 如下图,其中include下jni_md是从darwin中复制到include目录下,因为jni.h中存在对jni_md.h的引用,所以为了方便放到同级目录下

其中lib中存放的就是libMain.jnilib,也就是我们编译的本地资源库。到这里我们本地接口就算编写完成了,调用的时候需要指定Library。

-Djava.library.path=/Users/.../java/lib

否则会有如下报错:

java.lang.UnsatisfiedLinkError: no XXX in java.library.path

关于g++命令的参数这里简单的说下 :

-shared是说明要生成动态库。

-I的选项,是因为我们用到<jni.h>相关的头文件,放在<jdk>/include目录下。最

-o 选项,我们在java代码中调用的是System.loadLibrary("Main"),那么生成的动态链接库的名称就必须是libMain.jnilib的形式,否则在执行java代码的时候,同样会报 java.lang.UnsatisfiedLinkError: no XXX in java.library.path 的错误

头文件具体用处

(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

关于本地接口到这里就讲完了,这篇文章只讲述了我们如何去调用本地接口,但是原理并没有提及,大家可以追踪System.loadLibrary()方法进行学习。