使用Maven构建发布JNI项目
01 Jun 2016
Maven是Java世界必备的开发工具。它完美的解决了Java项目定义、依赖、构建和发布等诸多环节的问题。
当项目中使用JNI的时候,我们不仅要编译Java代码,还要编译Native代码。Java类文件要和Native代码生成的库文件(例如Linux下是so文件)一起才能
工作。这就需要我们考虑在Maven下怎样编译native代码、测试的时候怎样引入库文件、怎样发布库文件和类文件等问题。
幸运的是,我们在Maven的框架下仍然能够优雅的解决这个问题。这篇文章就总结了如何使用Maven构建、测试和发布一个最简单的JNI项目。这里我们设定开发
环境Linux。
###一个简单的JNI项目
在这个项目,我们通过JNI调用一段native代码打印“Goodbye World!”
####Greeting.java:
代码很简单。JVM在装载Class的时候,会去load一个greeting的动态链接库,也就是一个叫libgreeting.so的文件。在main里面会调用greeting函数,
而该函数被声明成native的,也就是会调用native的实现。这里我们不用package。
编译一下
javac Greeting.java
我们看到生成Greeting.class的文件。
生成头文件
javah Greeting
这就生成了我们应该实现的C文件的接口。
####C代码 Greeting.c:
很简单,在实现里调用stdio的printf打印信息。
编译C代码
gcc Greeting.c -o libgreeting.so -I $JAVA_HOME/include/ -I $JAVA_HOME/include/linux/ -shared -fPIC
生成文件名是我们前面提到的libgreeting.so。这里$JAVA_HOME是你的jdk安装路径。我们生成的是动态链接文件,所以编译时要添加-shared和-fPIC。
运行程序
java Greeting
可以看到打印出
Goodbye World!
###在Maven中构建
基本思路是,我们创建一个Maven项目Greeting,这个项目包含两个子项目native和jni,其中native生成so文件,jni生成class文件以及打成jar包。
创建文件夹
mkdir Greeting
mkdir -p Greeting/native/src/main/c/jni
mv Greeting.c Greeting/native/src/main/c/jni
mkdir -p Greeting/jni/src/main/java
mv Greeting.java Greeting/jni/src/main/java
创建Greeting项目的pom.xml文件
创建native项目的pom.xml文件
这里我们使用了一个叫做native-maven-plugin的插件编译我们的native代码,本质上相当于执行了一条gcc命令。
然后是jni项目的pom.xml
在Greeting目录下面,执行
mvn compile
我们可以看到在编译成功,并在native/target下生成了so文件,在jni/target/classes下生成了class文件。
把它们拷到一起
cp jni/target/classes/Greeting.class ./
cp native/target/libgreeting.so ./
java Greeting
打印出Goodby World!消息。
###添加测试
在开发中我们都会添加回归测试用例。我们可以先往greeting_jni项目中添加一个unit test。
创建目录,在Greeting目录下
mkdir -p jni/src/test/java
测试代码,GreetingTest.java:
修改jni文件夹里的pom.xml文件,添加junit依赖
在Greeting目录下,运行测试命令
mvn test
我们可以看到抛了一个错
Tests in error:
testGreeting(GreetingTest): no greeting in java.library.path
这个意思就是在给定路径下找不到libgreeting.so文件。JVM在load动态库文件时,会从制定的几个位置去找,例如当前路径,以及在/etc/ld.conf.so和/
etc/ld.conf.so.d中定义的路径。
这里我们使用jniloader来load我们的so文件,使用它的另一个好处是它可以从打包好的jar包中load动态库。这样我们可以把so文件打包进jar文件,方便使用。
只要在jni项目的pom.xml中build部分添加
第一个plugin是设置路径,第二个plugin是编译时拷so文件。为了让jni编译时能找到so文件,我们需要把native pom.xml中
这一行删掉。在拷贝的时候我们改变了文件名。
把so文件拷到target/classes的一个好处是,打包时也会把so文件包含到jar包里,这样就方便部署你的代码了。
在jni项目pom.xml的dependencies部分添加
我们使用jniloader来load我们的so文件。
在test时,jniloader检查java.library.path里的路径有没有指定的so文件。由于我们前面在plugin里设置了路径target/classes,并把so文件拷了过去,
我们编译生成的so文件在test会被load起来。
重新编译加测试
mvn clean test
我们看到输出中显示so文件被加载,并且看到消息被打印出来了。
INFO: successfully loaded /home/ian/demo/Greeting/jni/target/classes/libgreeting.so
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.04 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Goodbye World!
###打包
在打包时,不仅要打包我们的文件,还要把jniloader类都打包进来。我们可以使用maven-assembly-plugin插件。
在jni路径下的pom.xml文件的build部分,添加
然后我们在Greeting路径下执行打包命令。
mvn package
用jar命令看一下生成的jar包里有什么东西。
jar tf jni/target/greeting_jni-0.0.1-SNAPSHOT-jar-with-dependencies.jar
可以看到可爱的libgreeting.so文件和Greeting.class文件。
前面说过jniloader可以从jar包里load so文件,我们直接执行jar文件
java -cp jni/target/greeting_jni-0.0.1-SNAPSHOT-jar-with-dependencies.jar Greeting
我们看到输出
INFO: successfully loaded /tmp/jniloader904592266431635427libgreeting.so
Goodbye World!
上面的代码可以从这里下载。Have fun!