使用Maven构建发布JNI项目
01 Jun 2016Maven是Java世界必备的开发工具。它完美的解决了Java项目定义、依赖、构建和发布等诸多环节的问题。
当项目中使用JNI的时候,我们不仅要编译Java代码,还要编译Native代码。Java类文件要和Native代码生成的库文件(例如Linux下是so文件)一起才能 工作。这就需要我们考虑在Maven下怎样编译native代码、测试的时候怎样引入库文件、怎样发布库文件和类文件等问题。
幸运的是,我们在Maven的框架下仍然能够优雅的解决这个问题。这篇文章就总结了如何使用Maven构建、测试和发布一个最简单的JNI项目。这里我们设定开发 环境Linux。
###一个简单的JNI项目 在这个项目,我们通过JNI调用一段native代码打印“Goodbye World!”
####Greeting.java:
class Greeting {
static {
System.loadLibrary("greeting");
}
public static void main(String[] args) {
Greeting.greeting();
}
public static native void greeting();
}
代码很简单。JVM在装载Class的时候,会去load一个greeting的动态链接库,也就是一个叫libgreeting.so的文件。在main里面会调用greeting函数, 而该函数被声明成native的,也就是会调用native的实现。这里我们不用package。
编译一下
javac Greeting.java
我们看到生成Greeting.class的文件。
生成头文件
javah Greeting
这就生成了我们应该实现的C文件的接口。
####C代码 Greeting.c:
#include <jni.h>
#include <stdio.h>
JNIEXPORT void JNICALL Java_Greeting_greeting(JNIEnv * jenv, jclass jcls) {
printf("Goodbye World!\n");
}
很简单,在实现里调用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文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.yiheng</groupId>
<artifactId>greeting</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>native</module>
<module>jni</module>
</modules>
</project>
创建native项目的pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.yiheng</groupId>
<artifactId>greeting</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>greeting_native</artifactId>
<packaging>so</packaging>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>1.0-alpha-8</version>
<extensions>true</extensions>
<configuration>
<compilerProvider>generic-classic</compilerProvider>
<compilerExecutable>gcc</compilerExecutable>
<linkerExecutable>gcc</linkerExecutable>
<sources>
<source>
<directory>${basedir}/src/main/c/jni</directory>
<fileNames>
<fileName>Greeting.c</fileName>
</fileNames>
</source>
</sources>
<compilerStartOptions>
<compilerStartOption>-I ${JAVA_HOME}/include/</compilerStartOption>
<compilerStartOption>-I ${JAVA_HOME}/include/linux/</compilerStartOption>
</compilerStartOptions>
<compilerEndOptions>
<compilerEndOption>-shared</compilerEndOption>
<compilerEndOption>-fPIC</compilerEndOption>
</compilerEndOptions>
<linkerStartOptions>
<linkerStartOption>-I ${JAVA_HOME}/include/</linkerStartOption>
<linkerStartOption>-I ${JAVA_HOME}/include/linux/</linkerStartOption>
</linkerStartOptions>
<linkerEndOptions>
<linkerEndOption>-shared</linkerEndOption>
<linkerEndOption>-fPIC</linkerEndOption>
</linkerEndOptions>
<linkerFinalName>libgreeting</linkerFinalName>
</configuration>
</plugin>
</plugins>
</build>
</project>
这里我们使用了一个叫做native-maven-plugin的插件编译我们的native代码,本质上相当于执行了一条gcc命令。
然后是jni项目的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.yiheng</groupId>
<artifactId>greeting</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>greeting_jni</artifactId>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在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:
import org.junit.Test;
public class GreetingTest {
@Test
public void testGreeting() {
Greeting.greeting();
}
}
修改jni文件夹里的pom.xml文件,添加junit依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
在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>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
<configuration>
<systemPropertyVariables>
<java.library.path>${project.build.directory}/classes</java.library.path>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy</id>
<phase>compile</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>io.github.yiheng</groupId>
<artifactId>greeting_native</artifactId>
<version>0.0.1-SNAPSHOT</version>
<type>so</type>
<overWrite>false</overWrite>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<destFileName>libgreeting.so</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
第一个plugin是设置路径,第二个plugin是编译时拷so文件。为了让jni编译时能找到so文件,我们需要把native pom.xml中
<linkerFinalName>libgreeting</linkerFinalName>
这一行删掉。在拷贝的时候我们改变了文件名。
把so文件拷到target/classes的一个好处是,打包时也会把so文件包含到jar包里,这样就方便部署你的代码了。
在jni项目pom.xml的dependencies部分添加
<dependency>
<groupId>com.github.fommil</groupId>
<artifactId>jniloader</artifactId>
<version>1.1</version>
</dependency>
我们使用jniloader来load我们的so文件。
static {
com.github.fommil.jni.JniLoader.load("libgreeting.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部分,添加
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
然后我们在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!