学习Java本地接口

293 阅读10分钟

Java本地接口

从Java 1.1开始,Java Native Interface(JNI)标准就是Java平台的一部分。它允许Java代码与用不同语言编写的代码进行通信。

尽管JNI主要是为原生编译语言开发的,但它并不限制你使用其他语言,只要有适当的调用约定。

我们都知道,Java的主要优势之一是灵活性,这意味着当我们创建一个代码编译时,其结果是一个与平台无关的字节码。简单地说,这个字节码可以在每一个可以运行java程序的系统或小工具上执行。

然而,在有些情况下,我们必须为一个特定的基础设施使用本地可执行代码。

我们采用原生代码的原因有很多,包括以下几点。

  • 当有处理硬件的时候。
  • 每次执行一个具有挑战性的程序时,其性能必须得到提高。
  • 当我们希望重用一个库,而不是在Java中重新创建它。

这个工具就是我们所说的Java Native Interface。JNI是一个编码模型,用于联系和调用其范围之外的应用程序(针对平台硬件和操作系统的包),以及来自Java虚拟机中的Java程序。

此外,用其他语言开发的库,如C、C++和汇编,也由JVM生成。

先决条件

要跟着学习,你需要。

  • 对使用C++创建对象有基本了解。
  • 对Java编程语言和如何创建类有基本了解。
  • 能够执行代码,使用G++编译器。在我的案例中,我使用oracle terminal online来运行代码。

目标

在本教程结束时,你应该能够理解以下概念。

  • 一般的Java本地接口,以及Java本地接口中的元素。
  • 使用本地语言(我们的例子是C++)实现Java本地方法。
  • 使用对象从本地代码应用Java方法。
  • 使用JNI的优势和限制。

本机方法

Java语言中的本地关键字指定了这种程序在外国代码中实现。

在创建外国或本地可执行程序期间,我们通常可以选择使用静态库或共享库。

  • Static libraries - 在链接过程中,我们会将所有库的二进制文件作为我们可执行文件的一部分。因此,我们将不再需要这些库,但我们的可执行文件的大小会增加。
  • Shared libraries - 在最终的可执行文件中只有库被引用,而不是代码本身。我们的程序在其中运行的配置必须能够访问我们的软件所使用的库中的每一个文件。

由于二进制文件不能在同一个文件中混合字节码和本地代码,后者对于Java本地接口来说是非常有意义的。

private native void theNativeMethod();

在上面的代码中,关键字(native)将我们的方法函数转化为一种抽象过程,例外的是,它将被部署在一个独特的、共享的外国包中,而不是由其他Java类负责实现它。

代码中的JNI元素(Java)

在java代码中发现的元素。

  • 正如我们已经讨论过的,一个术语被称为native ,它是任何被强调为外国的函数,应该在一个陌生的共享库中使用。
  • 一个字符串库名System.loadLibrary ,这是一个恒定的程序,它将一个可共享的库链接到一个系统的地址内,使其导出的功能可以被我们的Java代码访问。
  • Java虚拟机--一个框架,它允许我们通过引入线程、终止线程等方式控制当前的JVM(甚至从头开始构建一个)。
  • Java本地接口环境(JNIEnv )--一个具有从外国代码访问Java对象的方法的结构
  • java本地接口导出(JNIEXPORT)--将共享库中的一个函数标识为可导出。它将出现在方法表中,Java本地接口将发现它。
  • Java 本机接口调用 (JNICALL) - 保证我们的技术在与 JNIEXPORT 配套使用时可用于 JNI 框架。

Java 本机接口hello world

让我们来看看JNI是如何运作的。这篇文章将采用C++作为主要的编程语言,使用G++作为编译器和链接器。

我们可以选择任何我们喜欢的编译器,但这是要让G++在Ubuntu操作系统、Windows操作系统和Mac操作系统上运行的方法。

  • 在Ubuntu(Linux)中,我们将不得不运行Sudo apt-get ,在终端安装构建基本代码。
  • 在Windows操作系统中,我们将安装MinGW
  • 在macOS的终端中运行命令g++ ,如果它不存在,你需要先安装它。

第一步:创建Java类

通过编写我们的第一个Java本地接口程序,我们必须实现 "Hello World "类。首先是开发一个Java类,其中包含用于完成任务的本地方法。

package example.java;
public class HelloJavaJNI {

    static
        {
        System.loadOurLibrary("javanative");
          }

    public static void main(String args[])
           {
       new JavaNative().remarkHi();
    }

    // remark() is used as a native method that gets no argument and returns it void
    protected native void remark();
}

静态库是在静态块中加载的,使其能够被访问,并且在任何位置。

我们也可以只在这个小脚本中使用我们的本地函数之前加载该包,因为本地库没有其他用途。

第二步:在C++中实现我们的本地语言方法

我们可以用另一种编程语言实现我们的本地方法,使其处于本地情况。

在这种情况下,我们将利用C++语言来实现它。在C++中,定义和应用将按以下顺序进行:.hand,.ccpfile extensions。

首先,我们将使用java编译器的-h flag 来构建方法定义。

javac -h. HelloJavaJNI.java

上面的代码创建了一个名为example_java's_HelloJavaJNI.h 的新文件,其中保存了类中所有的本地函数。

JNIEXPORT void JNICALL Java_example_java_HelloJavaJNI.h_remarkHi
  (JNIEnv *, kobject);

上面的例子显示,方法类的名称是即时产生的,利用了正确的批准的应用程序、类和函数名称。

另外,我们可以看到,我们正在获得提供给该方法的两个实体:一个是现有JNI环境和java实例的标记,该函数与之相关,是我们HelloJavaJNI 类的实例。

对于我们的remarkHi 方法的实现,我们必须构建一个新的cpp目录,在这里我们将做一些事情,比如向这样一个控制台打印 "欢迎回来"。

JNIEXPORT void JNICALL Java_example_java_HelloJavaJNI.h_remarkHi
  (JNIEnv* enviroment, kobject thisObject)
  {
    std::cout << "Welcome back from C++ language" << std::endl;
}

在上面的插图中,我们调用了.cpp 文件,这将与包含名称的.h 文件非常相似。

我们将包括上面的代码来执行本地函数。

第三步:编译和连接

此刻,我们手头有我们需要的必要组件,并且有涉及它们的链接。

我们将不得不构建我们的共享库并从C++代码中运行它。为了完成这个任务,我们使用G++编译器。

注意:Java JDK的JNI头文件应该包含在安装包中。

JDK安装中的Java Native Interface头文件显示在下面的代码中。

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win64 example_java_HelloJavaJNI.cpp -o example_java_HelloJavaJNI.o

在我们的平台上编译时,我们必须将代码包含在一个新的共享库中,进入文件examplejava HelloJavaJNI.o 。提供给我们方法的争论点或参数是system.loadLibrary

我们将参数命名为java native,所以我们在运行java代码时需要加载它。

g++ -shared -o javanative.dll example_java_HelloJavaJNI.o -W0l,--insert-stdreturn-alias

然后我们可以利用命令行来运行我们的脚本。

然而,我们必须添加整个路径,告诉Java在哪里可以找到我们的本地库,如下图所示。

java -cpp . -Directoryjava.library.path=/JAVANATIVE_SHARED_LIBRARY_FOLDER example_java_HelloJavaJNI

一旦我们显示了路径,控制台的输出将是。

Welcome back from the C++ language

我们如何向本地方法添加属性

打招呼很可爱,但并没有特别的好处。在大多数情况下,我们要控制软件中Java和C++之间的数据传输。

一些参数必须包含在外来方法中。我们将制作另一个名为ParameterIllustrationJNI 的新类,其中有两个外国方法,使用不同类型的参数并返回。

private native short totalIntegers(int one, int two);
protected native String remarkILoveCoding(String names, boolean isMale);

之后,我们将需要使用javac-h 来产生另一个.h 文件,就像我们在前面的步骤中做的那样。

之后,我们必须构建一个a.cpp 文件,其中包含这些安装的新C++技术。

JNIEXPORT pshort JNICALL Java_example_java_HelloJavaJNI_totalIntegers
  (JNIEnv* enviroment, pobject thisObject, pint one, pint two)
  {
    std::cout << "C++:The digits got are!" << one << " plus " << two << std::endl;
    return (short)one + (short)two;
    }
JNIEXPORT pstring JNICALL Java_example_java_ParameterIllustrationJNI_remarkILoveCoding
  (JNIEnv* environment, pobject thisObject, pstring name, pboolean isMale)
     {
    const char* idenifyCharPointer = enviroment->GetStringUTFChar(names, NULL);
    std::string content;
    if(isMale)
         {
        content = "Sir ";
            }
    else {
        content = "Madam ";
    }

    std::string fullNames = content + nameCharacterPointer;
    return environment->NewStringUTF(fullNames.c_str());
}
解释

在我们上面的说明中,我们利用了通过这些JNI环境实例给出的这些方法。此外,我们使用了JNIEnvironment类型的指针*environment。

在这种情况下,JNIEnv允许我们在C++函数上输入java字符串,然后再返回,而不必担心实现问题。

从本地代码中应用Java方法并使用对象

在这最后一个例子中,我们将探讨如何在本地源代码中加入Java对象。

假设我们开始制作一个新的类,并将其命名为UtilizeInformation ;我们将存储用户信息。

package example.java;

public class UtilizeInformation
  {

    public String names;
    public float symmetry;

    public String getClientInformation()
     {
        return "[identity]=" + names + ", [weigh]=" + symmetry;
    }
}

上面的代码脚本显示了我们如何使用所需的类来存储用户的信息。

接下来,我们将不得不创建另一个java类。这一次,该类将管理所用类的对象。我们将使用ObjectIllustrationJNI 作为我们的类。

public native UtilizeInformation createUsers(String name, float symmetry);
public native String printUtilizeInformation(UtilizeInformation users);

我们刚刚创建了一个java类,在上面的代码中包含一个本地方法或函数。该函数在我们刚刚创建的java类中管理我们的对象。

在最后一步,我们将在新的.cpp 文件上构建我们对外操作的.h 头和一个C++实现。

JNIEXPORT pobject JNICALL Java_example_java_ObjectIllustrationJNI_createUser
  (JNIEnv *environment, pobject thisObject, pstring name, pfloat symmetry)

   {
    //Here we create an object of the class UtilizeInformation
    pclass utilizeInformationClass = environment->FindClass("example/java/UtilizeInformation");
    pobject newUtilizeInformation = environment->AllocObject(utilizeInformationClass);

    // How we set the UtilizeInformation fields
        pfieldID nameField = environment->GetFieldID(utilizeInformationClass , "name", "Sjava/lang/String;");
    pfieldID weighField = environment->GetFieldID(utilizeInformationClass , "weigh", "Q");

    environment->SetObjectField(newutilizeInformation, nameField, name);
    environment->SetDoubleField(newutilizeInformation, weighField, weigh);

    return newUtilizeInformation;
}

JNIEXPORT pstring JNICALL Java_example_java_ObjectIllustrationJNII_printUtilizeInformation
  (JNIEnv *environment, pobject thisObject, pobject utilizeInformation)
  {

    // Finding java method id to be summoned.
    pclass utilizeInformationClass=environment->GetObjectClass(utilizeInformation);
    pmethodID methodId=environment->GetMethodID(utilizeInformationClass, "utilizeInformation", "()Sjava/lang/String;");

    pstring result = (pstring)environment->CallObjectMethod(utilizeInformation, methodId);
    return result;
}

在上面的代码脚本中,我们使用JNIEnv*environment pointer ,从执行中的JVM访问相关的类、对象、字段和函数。

要检索一个Java类,我们只需要提供整个类。

在我们的本地代码中,我们甚至在制造一个类的实例example.java.UtilizeInformation 。一旦我们有了这个实例,我们就可以以同样的方式来操作这个实例的所有属性和技术,以实现Java的表示。

使用JNI的优势

  • 如果不可能完全使用java语言来创建一个操作,那么Java Native Interface将允许人们用其他本地语言编写程序。
  • 也可以采用它来改编现有的应用程序,并在不同的编程语言中建立,使Java应用程序可以利用它。
  • 它使所有的Java程序能够安全地、独立于平台地访问性能和对平台敏感的API实现功能。
  • JNI有助于解决互操作性的挑战。

JNI的局限性

  • 基于JNI的应用程序失去了Java提供的平台兼容性。
  • JNI框架不支持JVM的自动垃圾收集,没有使用本地代码分配资源的地址。
  • 错误处理是必须的,否则,Java本地接口方和Java虚拟机可能会崩溃。
  • 本地程序的运行时错误很难管理。

注意:为一个特定的平台编译代码比运行字节码要快得多。

每当我们需要在一个耗时的操作中快速完成一些事情时,它就会派上用场。

有时我们可能没有更好的选择,比如何时使用设备管理库。

然而,它是有代价的,因为我们必须为我们提供的每个平台跟踪额外的代码脚本。因此,只有在没有任何Java替代品的情况下,利用JNI几乎总是明智的。

总结

在这篇文章中,我们已经了解了Java本地接口,以及如何利用这一技术。

我们还介绍了更多关于Java本地元素和如何在本地方法中添加参数,使用java本地接口的优势和劣势。