什么是JNI?

Java的JNI(Java Native Interface)是一个编程框架,也可以视为一种接口,允许Java代码其他语言编写的应用程序或库(通常是C和C++)之间进行交互。JNI对于需要使用已经存在的库(尤其是系统级库)或者为了性能优化需要直接访问底层系统的应用程序来说非常重要。

JNI的主要特点和用途:

  1. 本地方法调用:通过JNI,Java程序可以调用在其他语言(如C或C++)中编写的函数。这对于使用特定平台的功能或调用一个已经用这些语言编写的库非常有用。
  2. 性能优化:对于某些需要高性能的操作,使用JNI调用本地代码可能比纯Java代码执行得更快。
  3. 硬件访问:JNI可以用来直接与操作系统级别的资源或硬件交互,例如,直接从Java代码中访问文件系统的特定功能或硬件设备。
  4. 已存在代码的利用:如果有大量的用C/C++编写的遗留代码,JNI提供了一种方式来重用这些代码,而不是完全用Java重新实现它们。

JNI使用的基本步骤

  1. 声明本地方法:在Java类中声明native方法。这些方法是在Java代码中声明但实际上在本地代码(如C或C++)中实现的。
  2. 生成头文件:使用javah工具(或Java 8中的javac)从Java类文件生成C/C++头文件。
  3. 实现本地方法:在C或C++文件中实现这些本地方法。。
  4. 编译本地代码:将C/C++代码编译成动态链接库(如Windows上的.dll或Linux上的.so文件)。
  5. 加载和使用本地库:在Java程序中使用System.loadLibrary加载包含本地方法实现的库,并调用这些本地方法。

演示

步骤 1: 创建 Java 类并声明本地方法

在Java类中声明native方法。即使这些方法实际由其他语言实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

public class HelloJNI {
static {
System.loadLibrary("hello"); // 加载名为"hello"的本地库
}

// 声明本地方法
private native void sayHello();

public static void main(String[] args) {
new HelloJNI().sayHello(); // 调用本地方法
}
}

步骤 2: 生成头文件

使用 javac 编译器编译 Java 类并生成 C/C++ 头文件

1
javac HelloJNI.java

回到\src\main\java目录,生成org_example_HelloJNI.h

1
javah -jni -cp . org.example.HelloJNI

步骤 3: 实现本地方法(Visual Studio C++)

打开 Visual Studio,创建一个新的 C++ 项目。在项目中包含刚才生成的头文件,并实现相应的本地方法。

包含头文件

C/C++需要java标准库中的jni.h以及上一步生成的org_example_HelloJNI头文件,可以选择直接把.h文件直接拖到项目头文件中,也可以通过VS的项目属性配置来附加搜索的路径

右键Project->配置属性->C/C++->常规->附加包含目录

jni.h一般情况下在java安装目录下的include目录中

因为jni.h中还包含了jni_md.h,所以还需要对include/win32进行包含

1
2
3
4
5
6
......
/* jni_md.h contains the machine-dependent typedefs for jbyte, jint
and jlong */

#include "jni_md.h"
......

编写本地方法

根据org_example_HelloJNI.h中javah自动生成的声明函数进行实现,这里是sayHello方法,因为我们在java中用native关键字进行定义了。(不同版本的jdk生成的格式可能不同)

1
2
3
4
5
6
7
8
#include <jni.h>
#include "org_example_HelloJNI.h"

JNIEXPORT void JNICALL Java_org_example_HelloJNI_sayHello(JNIEnv*, jobject) {
// 本地方法的实现
printf("Hello, i'm from native method of C++\n");
}

步骤 4:编译本地代码

右键项目->配置属性->常规->配置类型->动态库(.dll)

最后生成解决方案,再把生成的DLL文件(不需要lib)放在java项目可以找到的路径即可。

在IDEA环境下,项目路径就在根目录,与pom.xml,.idea同级

步骤5:加载和使用本地库

以上步骤均正确完成后,java直接编译运行即可。可以看到java成功调用了C++实现的sayHello

JNI进阶

在上述的例子中,更多的是jni对本地代码的单向调用,那么如何在本地代码中对java进行一些操作呢?

还是以C++为例

首先观察javah生成的声明函数,可以发现有2个参数

1
2
JNIEXPORT void JNICALL Java_org_example_HelloJNI_sayHello
(JNIEnv *, jobject);

接下来对这2个参数进行讲解,它们在本地代码访问java时起到了关键作用。

JNIEnv *

JNIEnv * 是一个指向 JNI 环境的指针,它提供了接口函数访问 JNI 功能。每个 JNI 函数的第一个参数都是 JNIEnv * 类型。这个指针非常关键,因为它用于所有的操作和交互,如:

  • 创建和操作 Java 对象。
  • 调用 Java 方法。
  • 抛出和处理异常。
  • 访问字段和方法的 ID。
  • 管理 Java 对象的内存(比如创建、释放局部引用和全局引用)。

jobject

返回一个指向调用该native方法的java调用者对象。

大白话就是,在哪个对象里调用的这个native,这个jobject就是对谁的引用。

为了掌握使用`JNI对java对象进行操作,我们需要首先了解以下几种方法:

注意,接下来介绍的方法原型都是以C为例。而之后实际编写代码都是用的C++。这两者对于jni的使用是略微不同的。

在C++中,编译器会将JNINativeInterface_做一层封装,用途就是在C++中使用每个jni方法时不再需要传入*env,更加简洁。

官方注释:

/*

  • We use inlined functions for C++ so that programmers can write:
  • env->FindClass(“java/lang/String”)
  • in C++ rather than:
  • (*env)->FindClass(env, “java/lang/String”)
  • in C.
    */

jni.h中对C++环境做了一层封装:

1
2
3
4
5
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif

1. (*env)->FindClass

FindClass 用于在 JNI 代码中查找 Java 类。

  • 原型

    1
    jclass FindClass(JNIEnv *env, const char *name);
  • 参数

    • env: 指向 JNI 环境的指针。
    • name: 要查找的类的完全限定名,用斜线(/)分隔包名,而不是点(.)。例如,java/lang/String
  • 返回值

    • 返回一个 jclass 对象,它是找到的 Java 类的引用。
    • 如果类没有找到,返回 NULL

2. (*env)->GetMethodID

GetMethodID 用于获取 Java 类的实例方法的 ID。

  • 原型

    1
    jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
  • 参数

    • env: 指向 JNI 环境的指针。
    • clazz: 类的 jclass 引用。
    • name: 要查找的方法的名称。
    • sig: 方法的签名,用于描述方法的参数和返回类型。
  • 返回值

    • 返回一个 jmethodID,它是该方法的唯一标识符。
    • 如果方法没有找到,返回 NULL

3. (*env)->GetObjectClass

GetObjectClass用于获取一个 Java 对象的类。

  • 原型:

    1
    jclass GetObjectClass(JNIEnv *env, jobject obj);
  • 参数:

    • env: 指向 JNI 环境的指针。
    • obj: Java 对象的引用,你想获取的类的这个对象。
  • 返回值

    • 返回一个 jclass 引用,它代表了传入的 Java 对象 (obj) 的类。

4. (*env)->NewObject

NewObject 用于创建 Java 对象的新实例。

  • 原型

    1
    jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
  • 参数

    • env: 指向 JNI 环境的指针。
    • clazz: 类的 jclass 引用。
    • methodID: 类构造函数的 jmethodID
    • ...: 构造函数需要的参数。
  • 返回值

    • 返回一个 jobject,它是新创建的对象的引用。
    • 如果对象创建失败,返回 NULL

5.(*env)->GetFieldID

GetFieldID用于获取java对象的某个字段

  • 原型:
1
jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
  • 参数:

    • env: 指向 JNI 环境的指针。
    • clazz: Java 类的 jclass 引用。这个类是包含所需字段的类。
    • name: 字段的名称,是一个字符串。
    • sig: 字段的签名。这是一个描述字段类型的字符串。例如,"I" 代表 int 类型,"Ljava/lang/String;" 代表 String 类型等。
  • 返回值:

    • 返回一个 jfieldID 类型的值,它是字段的唯一标识符,用于后续的字段访问操作。
    • 如果没有找到指定的字段,将返回 NULL。这通常意味着类定义中没有该字段,或者字段名称/签名有误。

6. (*env)->GetField

GetField系列函数用于通过字段ID获取Java对象的字段值。这些函数根据字段的数据类型有不同的变体,如GetIntField用于获取int类型字段的值。

  • 原型:

    根据字段类型的不同,GetField函数有多个变体,例如:

    1
    jint GetIntField(JNIEnv *env, jobject obj, jfieldID fieldID);
  • 参数:

    • env: 指向 JNI 环境的指针。
    • obj: Java 对象的引用,即我们要获取其字段值的对象。
    • fieldID: 字段的ID,通常是通过GetFieldID函数获取的。
  • 返回值:

    • 返回字段的值。返回类型与函数的类型相匹配(如GetIntField返回jint)。
    • 如果对象为NULL或字段ID无效,函数行为未定义,可能导致程序崩溃。

7.(*env)->Set<Type>Field

Set<Type>Field用于设置 Java 对象的字段值。不同的函数用于不同类型的字段。

  • 原型(以 int 类型为例):

    1
    void SetIntField(JNIEnv *env, jobject obj, jfieldID fieldID, jint value);
  • 参数

    • env: 指向 JNI 现场的指针。
    • obj: Java 对象的引用,其字段将被设置。
    • fieldID: 字段的 jfieldID,通过 GetFieldID 获得。
    • value: 要设置的新值,其类型应与字段类型一致。

使用案例

模拟一个验证用户密码,然后修改余额,最后新建一个账户的简单场景,这样就涵盖了对java对象的所有基本操作。

其中修改余额modifyMoney与添加账户addAccount方法均由C++实现

创建java类和本地方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package my.bank;

import java.util.HashSet;
import java.util.Set;

public class BankSystem {
static {
System.loadLibrary("BankDll"); // 加载名为本地库
}
private static final Set<BankSystem> BankCounter = new HashSet<>();//将存活的BankSystem对象统计起来

public String username;
public String password;
public int money;
public BankSystem() {
this.username="admin";
this.password="qwerasdzxc123";
this.money=0;

}

public BankSystem(String username, String password, int money) {
this.username = username;
this.password = password;
this.money = money;

}

public static void showAccount(){
System.out.println("现有账户:");
for (BankSystem bankSystem: BankCounter
) {
System.out.println(bankSystem);
}
}
//修改余额
public native void modifyMoney(String username, String password, int newMoney);
//添加账户
public native static void addAccount(String username, String password, int money);

@Override
public String toString() {
return "BankSystem{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", money=" + money +
'}';
}

public static void main(String[] args) {
BankSystem bankSystem = new BankSystem();
BankCounter.add(bankSystem);

BankSystem.showAccount();

bankSystem.modifyMoney(bankSystem.username, bankSystem.password,999);

BankSystem.addAccount("lanbo","1111111111",88888);

BankSystem.showAccount();
}
}

本地代码C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <jni.h>
#include <cstring>
#include "my_bank_BankSystem.h"

static const char* c_username = "admin";
static const char* c_password = "qwerasdzxc123";


//modifyMoney 本地实现

JNIEXPORT void JNICALL Java_my_bank_BankSystem_modifyMoney
(JNIEnv *env, jobject obj, jstring , jstring , jint new_money ) {

//获取java BankSystem类
jclass jclazz = env->FindClass("my/bank/BankSystem");
//也可以利用obj参数: jclass jclazz = env->GetObjectClass(obj);


//获取所需字段ID
jfieldID moneyID = env->GetFieldID(jclazz,"money","I");
jfieldID usernameID = env->GetFieldID(jclazz, "username", "Ljava/lang/String;");//注意这里的分号;
jfieldID passwordID = env->GetFieldID(jclazz, "password", "Ljava/lang/String;");

//获取字段值
//这里可以直接用函数参数列表中的参数,不过为了尽可能全面的学习所以这里当做没有,通过反射获取
jint money = (jint)env->GetIntField(obj, moneyID);
jstring jusername = (jstring) env->GetObjectField(obj,usernameID);
jstring jpassword = (jstring)env->GetObjectField(obj,passwordID);


// 将 Java 字符串转换为 UTF-8 格式的 C 字符串
const char* username = env->GetStringUTFChars(jusername, NULL);
const char* password = env->GetStringUTFChars(jpassword, NULL);
//比较
if(std::strcmp(c_password,password)==0&&std::strcmp(c_username,username)==0){
//修改money值
env->SetIntField(obj, moneyID, new_money);
}
else {
printf("invalid username or password!");
}
env->ReleaseStringUTFChars(jusername, username);
env->ReleaseStringUTFChars(jpassword, password);
}

//addAccount本地实现

JNIEXPORT void JNICALL Java_my_bank_BankSystem_addAccount
(JNIEnv* env, jclass jclazz, jstring jusername, jstring jpassword, jint jmoney) {
//获取构造方法
jmethodID constructorID = env->GetMethodID(jclazz, "<init>", "(Ljava/lang/String;Ljava/lang/String;I)V");


//创建新BankSystem对象
jobject bankSystem = env->NewObject(jclazz, constructorID,jusername,jpassword,jmoney);

/*
获取BankCouter.add方法
*/

//获取BankSystem对象中的BankCounter静态成员
jfieldID fieldId = env->GetStaticFieldID(jclazz, "BankCounter", "Ljava/util/Set;");
jobject bankCounter = env->GetStaticObjectField(jclazz, fieldId);

//获取Set.add方法并调用
jclass setClass = env->FindClass("java/util/Set");
jmethodID addMethodID = env->GetMethodID(setClass, "add", "(Ljava/lang/Object;)Z");
jboolean isAdded = env->CallBooleanMethod(bankCounter, addMethodID, bankSystem);//bankSystem为参数
if (isAdded) {
const char* username = env->GetStringUTFChars(jusername, NULL);
printf("Add account of \' %s \' successfully", username);
}
else {
printf("something wrong...");
}
}

执行结果:

可能存在的理解误区

Java的Jni并不是将本地代码翻译成了java或者机器语言供JVM执行,而更多是建立了一种桥梁,使得 Java 代码能够与编写在其他语言(如 C 或 C++)的本地代码进行交互。形象一点地说,就是java把自己家的钥匙(jni接口)给了别人(C++),之后别人就能够通过这把钥匙来获取java自己家的东西(对应的数据,对象)了。

本地方法直接运行在操作系统层面,不经过 JVM 的字节码解释或 JIT 编译。