1、简介 {#1简介}
众所周知,Java 和 Go 是两种著名的编程语言,各自在不同的领域表现出色。Java 以其可移植性和广泛的生态系统而闻名,而 Go 则以其简洁性、性能和高效的并发处理而著称。在某些情况下,将两种语言的优势结合起来,可以开发出更强大、更高效的应用程序。
本文将带你了解如何利用 Java Native Access(JNA)库来弥合两种语言之间的差距,从而在不编写任何 C 代码的情况下从 Java 中调用 Go 函数。
2、用 JNA 架起 Java 和 Go 之间的桥梁 {#2用-jna-架起-java-和-go-之间的桥梁}
传统上,从 Java 调用本地代码需要用 C 语言编写 Java 本地接口(JNI)代码,这会增加复杂性和开销。然而,随着 Java Native Access(JNA)库的出现,可以直接从 Java 调用本地共享库,而无需深入研究 C 代码。这种方法简化了集成过程,允许开发人员在 Java 应用程序中无缝利用 Go 的功能。
要理解这种集成是如何工作的,首先,要了解 Java Native Access(JNA)库在连接 Java 和 Go 方面的作用。具体来说,JNA 提供了一种从 Java 代码调用本地共享库函数的直接方法。通过将 Go 代码编译到共享库中并导出必要的函数,Java 可以与 Go 函数进行交互,就像它们是自己生态系统的一部分一样。
本质上,这一过程包括编写 Go 函数,将其编译成共享库,然后使用 JNA 创建映射到这些函数的相应 Java 接口。
2、项目设置 {#2项目设置}
首先,设置项目环境。这包括配置集成所需的构建工具和依赖项。在本例中,我们需要以下组件:
- Java Development Kit(JDK):用于编译和运行 Java 代码。
- Go 语言环境:用于编写和编译 Go 代码。
- Java Native Access(JNA)库:作为 Maven 项目的一个依赖项包含在内。
- 构建工具:Maven 适用于 Java,Go 编译器适用于 Go 代码。
添加 JNA 库到 maven 依赖:
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.15.0</version>
</dependency>
3、用 Java 调用 Go 函数 {#3用-java-调用-go-函数}
我们以一个简单的程序为例,来演示如何将 Go 函数集成到 Java 应用程序中。
其中 Java 将调用 Go 函数向控制台打印一条消息。实现过程包括几个关键步骤:编写 Go 函数,将其编译到共享库中,然后编写使用 Java Native Access(JNA)库调用 Go 函数的 Java 代码。
3.1、构建 Go 共享库 {#31构建-go-共享库}
首先,我们需要正确定义 Go 代码。
要从共享库中访问 Go 函数,我们必须用 C 链接导出该函数。这可以通过在函数定义前加入 import "C"
语句和 //export
指令来实现。此外,在用 Go 构建共享库时,还需要一个空的 main
函数。
创建一个名为 hello.go 的文件:
package main
/*
#include <stdlib.h>
*/
import "C"
import "fmt"
//export SayHello
func SayHello() {
fmt.Println("Hello Baeldung from Go!")
}
func main() {}
如上:
import "C"
语句启用了 CGO,允许使用 C 功能和导出函数。//export SayHello
指令告诉 Go 编译器导出带有 C 语言链接的 SayHello 函数。- 在将 Go 代码编译成共享库时,必须使用空的 main 函数。
编写 Go 函数后,下一步是将 Go 代码编译成共享库。这需要使用 Go build 命令,并加上 -buildmode=c-shared
选项,这样就能生成一个共享库文件(例如,Linux 上的 libhello.so 或 Windows 上的 libhello.dll)。
根据我们使用的操作系统,我们需要执行不同的命令来编译共享库。
对于基于 Linux 的操作系统,我们可以使用以下命令:
go build -o libhello.so -buildmode=c-shared hello.go
Windows:
go build -o libhello.dll -buildmode=c-shared hello.go
macOS:
go build -o libhello.dylib -buildmode=c-shared hello.go
构建完毕后,我们应该能在当前目录下找到共享库。
为方便起见,所有这些脚本都与 README.md 文件一起包含在 源码 中。
3.2、创建 JNA 接口 {#32创建-jna-接口}
下一步是进行 Java 方面的集成。在 Java 应用中,我们利用 Java Native Access (JNA)库加载共享库并调用 Go 函数。
首先,定义一个继承 com.sun.jna.Library
的 Java 接口,声明与导出 Go 函数相对应的方法:
import com.sun.jna.Library;
public interface GoLibrary extends Library {
void SayHello();
}
在该接口中,GoLibrary 继承了 JNA 的标记接口 Library
,SayHello
方法的签名与共享库中导出的 Go 函数相匹配。
接下来,编写 Java 应用,使用该接口加载共享库并调用 Go 函数。
import com.sun.jna.Native;
public class HelloGo {
public static void main(String[] args) {
GoLibrary goLibrary = Native.load("hello", GoLibrary.class);
goLibrary.SayHello();
}
}
如上,Native.load
会加载共享库 libhello.so (省略 lib 前缀和 .so 扩展名),创建 GoLibrary
实例以访问导出函数,并调用 SayHello
方法,调用 Go 函数并将消息打印到控制台。
运行 Java 应用时,必须确保在库路径中可以访问共享库。这可以通过将共享库放在与 Java 应用程序相同的目录下或设置适当的环境变量来实现:
对于 Linux 的系统,我们通过调用 export 命令来定义环境变量:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
对于 Windows,我们需要将包含 libhello.dll 文件的目录添加到 PATH 环境变量中。这可以在命令提示符中使用以下命令完成(或通过系统环境设置为永久配置):
set PATH=%PATH%;C:\path\to\directory
对于 macOS,使用的命令与 Linux 类似:
export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:.
最后,如果一切设置正确,我们就可以运行应用,并获得以下输出:
Hello Baeldung from Go!
4、传递参数和返回值 {#4传递参数和返回值}
在上述简单示例的基础上,通过向 Go 函数传递参数并向 Java 应用返回值来演示如何在 Java 和 Go 之间交换数据。
首先,修改 Go 代码,加入一个将两个整数相加并返回结果的函数。
在 Go 代码中,我们定义 AddNumbers
函数接受两个 C.int
类型的整数,并返回一个 C.int
.
//export AddNumbers
func AddNumbers(a, b C.int) C.int {
return a + b
}
更新 Go 代码后,我们需要重新编译共享库以包含新函数。
接下来,更新 Java 接口,添加 AddNumbers
函数。
我们定义了一个与 Go 函数相对应的接口,指定了带有适当参数和返回类型的方法签名。
public interface GoLibrary extends Library {
void SayHello();
int AddNumbers(int a, int b);
}
因此,我们可以调用 AddNumbers
函数并传递 int
参数:
public static void main(String[] args) {
GoLibrary goLibrary = Native.load("hello", GoLibrary.class);
int result = goLibrary.AddNumbers(2, 3);
System.out.printf("Result is %d%n", result);
}
运行应用后,应该能在输出中看到计算结果:
Result is 5
5、处理复杂数据类型 {#5处理复杂数据类型}
除了简单的数据类型外,通常还需要处理字符串等更复杂的数据类型。要将字符串从 Java 传递到 Go 并接收回字符串,我们需要在 Go 代码中处理指向 C 字符串的指针,并在 Java 中适当地映射它们。
首先,实现一个接受字符串并返回问候信息的 Go 函数。
在 Go 代码中,我们定义了 Greet
函数,它接受一个 *C.char
并返回一个 *C.char
。我们使用 C.GoString
将 C 字符串转换为 Go 字符串,并使用 C.CString
将 Go 字符串转换回 C 字符串。
//export Greet
func Greet(name *C.char) *C.char {
greeting := fmt.Sprintf("Hello, %s!", C.GoString(name))
return C.CString(greeting)
}
添加新函数后,需要重新编译共享库。
接下来,需要更新 Java 接口以包含 Greet
函数。
由于 Go 函数返回一个 C
字符串,我们要使用 JNA 提供的 Pointer
类来处理返回值。
public static void main(String[] args) {
GoLibrary goLibrary = Native.load("hello", GoLibrary.class);
Pointer ptr = goLibrary.Greet("Alice");
String greeting = ptr.getString(0);
System.out.println(greeting);
Native.free(Pointer.nativeValue(ptr));
}
在这段代码中,调用 Greet 方法时使用的参数是 "Alice"
,返回的 Pointer
会获取字符串,Native.free
会释放 Go 函数分配的内存。
运行该程序,输出如下:
Hello, Alice!
6、总结 {#6总结}
本文介绍了如何使用 Java Native Access(JNA)库轻松地将 Go 函数集成到 Java 应用程序中,而无需编写任何 C 代码。这种方法将 Go 的性能和并发性与 Java 结合起来,简化了集成过程,加快了开发速度。
关键因素包括确保 Java 和 Go 数据类型之间的兼容性,正确管理内存以避免泄漏,并建立健壮的错误处理机制。通过将 Java 的生态系统与 Go 的性能优化相结合,开发人员可以创建高效而强大的应用程序。
与 JNI 相比,JNA 具有多项优势,如无需使用 C 代码、支持跨平台开发,以及简化本地集成流程以加快实现速度。
总之,使用 JNA 将 Go 函数集成到 Java 中是一种有效而直接的方法,既能提高性能,又能简化开发。
完整的代码可以在 这里 找到。
Ref:https://www.baeldung.com/java-golang-invoke-function