使用Maven构建发布JNI项目

Maven是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!