关键字native

native

1. 含义

在 JDK 源代码中,有一些方法是被关键字 native 修饰的,就比如 Object 类:

1
2
3
// java.lang.Object

public native int hashCode();

native 的作用是什么呢?

native 修饰的方法表示这个方法是原生函数,其具体实现由非 Java 语言实现,比如 C 语言,其代码定义不在 Java 类中,而是在外部文件中(一般是动态链接库 .dll 文件)。

  • native 修饰的方法称为本地方法,当在 Java 代码中调用该方法时,JVM 会到外部定义中寻找它的实现代码

在 Java 平台中,与本地代码进行互操作的 API,称为 Java Native Interface (Java 本地接口)。

2. native 方法实现

2.1 方法声明

要实现 native 方法,首先要声明一个 native 方法。

比如下面的这个类 HelloNative,它声明了一个本地方法 sayHello:

1
2
3
4
5
6
7
package com.demo;
public class HelloNative {

// 外部定义方法
public native void sayHello();

}

运行时,JVM 会寻找本地方法 sayHello 的外部定义实现(一般是 C/C++ 实现),如果 JVM 没有找到并加载进去,执行 sayHello 方法时就会出现和以下类似的错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
package com;

import com.demo.HelloNative;

public class Main {

public static void main(String[] args) {
HelloNative helloNative = new HelloNative();
helloNative.sayHello();
}

}

错误信息日志:

1
2
3
Exception in thread "main" java.lang.UnsatisfiedLinkError: com.demo.HelloNative.sayHello()V
at com.demo.HelloNative.sayHello(Native Method)
at com.Main.main(Main.java:9)

在这里,sayHello 方法并没有外部实现,找不到肯定会报错。

可以自己手写一个外部实现代码,并让 JVM 加载它,然后就可以正常调用 sayHello 了。

2.2 外部定义实现

Java 对于 JNI 接口方法定义格式有一定的要求,所以不是随便写就行的。

  • 按照一般的 C/C++ 的文件格式可能无法正常加载到 JVM 中
  • JDK 中提供了一个 javah 命令,可以用来生成符合 JNI 样式的标头文件

在类 HelloNative 所在包的“根目录”执行以下命令:

1
2
# 生成 JNI 标头文件
javah -d com/demo com.demo.HelloNative

这样会在源文件 HelloNative.java 所在目录生成 JNI 标头文件 com_demo_HelloNative.h

  • javah 命令默认生成的标头文件名是“包名\_类名

生成的 com_demo_HelloNative.h 头文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_demo_HelloNative */

#ifndef _Included_com_demo_HelloNative
#define _Included_com_demo_HelloNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_demo_HelloNative
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_demo_HelloNative_sayHello
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

从头文件可以看到,javah 命令生成的标头文件是使用 C 语言声明定义的(extern "C"):

  • JNI 接口方法名称是“Java\_包名\_类名\_方法名”,该方法和 Java 中的 native 方法是一一对应的

有了头文件的接口方法声明,就可以开始实现具体的代码了。

创建一个文件 com_demo_HelloNative.c,并引入上面生成的头文件 com_demo_HelloNative.h

1
2
3
4
5
6
7
#include "com_demo_HelloNative.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_com_demo_HelloNative_sayHello(JNIEnv *env, jobject object)
{
printf("Hello,world!");
}

实现 native 方法后,还不能直接在 Java 中用,接下来还需要:

  • 将头文件(.h)和实现文件(.c)结合生成动态链接库文件(DLL),然后才能在 Java
    中使用

不同编译软件的生成命令可能不太一样,比如利用 MinGW GCC 的命令生成:

1
2
3
# -m64 是指编译成64位的库
# 需要注意给路径添加双引号,因为路径上有可能存在空格
gcc -m64 -Wl,--add-stdcall-alias -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32" -shared -o HelloNative.dll .\com_demo_HelloNative.c

参数 -I 是用于指定头文件(.h,例如 jni.h)的寻找路径。

编译完成后,就可以生成一个实现了 sayHello 接口的 HelloNative.dll 动态链接库文件了。

2.3 加载外部定义

生成 dll 后,还要把它加载到 JVM 中后才能使用,所以需要 JVM 能找到 dll 文件:

  • dll 文件需要放到 JVM 能够找到的位置,它才能被正确加载
  • JVM 默认的加载路径是程序当前目录,或者 System.getProperty("java.library.path")

所以只要把 dll 文件放到这些路径中的其中一个,JVM 就能够正常加载到 dll 文件。

加载好 dll 文件后,就可以调用 native 方法了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com;

import com.demo.HelloNative;

public class Main {

static {
// "HelloNative" 是 dll 文件名称
// 默认查找路径是程序当前目录
// 或者 System.getProperty("java.library.path")
System.loadLibrary("HelloNative");
}

public static void main(String[] args) {
HelloNative helloNative = new HelloNative();
helloNative.sayHello();
}

}

程序的运行结果:

1
Hello,world!

3. 流程图

1
2
3
4
5
6
7
8
9
10
graph LR
源文件(.java源文件) --> javac
javac --> 类文件(.class类文件)
类文件(.class类文件) --> JVM
JVM -- 加载 --> 共享库
源文件(.java源文件) -. native方法 .-> javah
javah --> 头文件(.h头文件)
头文件(.h头文件) --> 编译链接
实现文件(.c文件) --> 编译链接
编译链接 --> 共享库

4. 总结

  1. native 方法不是由 java 语言实现的,而是由 C/C++ 语言实现的
  2. native 的外部定义实现一般是编译成 dll 文件后,再被 JVM 加载
  3. native 方法的主要作用是为了跨平台、速度快、隐藏敏感代码
  4. native 方法可以提供 java 访问操作系统底层的需求
作者

jiaduo

发布于

2021-09-05

更新于

2023-04-02

许可协议