如何将本地库和JNI库捆绑在JAR文件中?

121

需要翻译的内容如下:

所涉及的库是Tokyo Cabinet

我想要将本地库、JNI库和所有Java API类放在一个JAR文件中,以避免重新分发时出现问题。

似乎GitHub上有人尝试这样做,但是:

  1. 它不包括实际的本地库,只有JNI库。
  2. 它似乎是特定于Leiningen的本地依赖项插件(它不能作为可再分发的程序使用)。

问题是,我能否将所有内容捆绑到一个JAR文件中并重新分发?如果可以,应该怎么做?

P.S.:是的,我意识到这可能会有可移植性方面的影响。

7个回答

72

可以创建一个包含所有依赖项(包括一个或多个平台的本地JNI库)的单个JAR文件。基本机制是使用System.load(File)来加载库,而不是通常搜索java.library.path系统属性的System.loadLibrary(String)。这种方法使安装变得更简单,因为用户不必在其系统上安装JNI库,但代价是可能不支持所有平台,因为特定平台的库可能未包含在单个JAR文件中。

具体步骤如下:

  • 在特定于平台的位置(例如在NATIVE / $ {os.arch} / $ {os.name}/libname.lib)将本地JNI库包含在JAR文件中
  • 在主类的静态初始化器中创建代码来:
    • 计算当前的os.arch和os.name
    • 使用Class.getResource(String)在预定义位置查找JAR文件中的库
    • 如果存在,则提取它到临时文件并使用System.load(File)加载。

我添加了一个功能来处理ZeroMQ的Java绑定(jzmq),代码可以在此处找到。jzmq代码使用混合解决方案,因此如果无法加载嵌入式库,则会回退到沿着java.library.path搜索JNI库的代码。


1
我喜欢它!它为集成节省了很多麻烦,如果失败了,您始终可以通过System.loadLibrary()恢复到“旧”方式。我想我会开始使用它。谢谢! :) - Matthieu
1
如果我的库dll依赖于另一个dll,我该怎么办?当加载前者dll时,我总是会得到“UnsatisfiedLinkError”错误,因为它隐式地想要加载后者dll,但无法找到它,因为它隐藏在jar中。先加载后者dll也没有帮助。 - fabb
其他依赖的 DLL 必须事先知道,并且它们的位置应该添加到 PATH 环境变量中。 - daparic
非常好!如果有人不清楚,只需将EmbeddedLibraryTools类放入您的项目中并相应地进行更改即可。 - Jon La Marr
@JonLaMarr 只有在您的代码是GPL许可的情况下才可以选择这条路,因为jzmq是GPL许可的。 - cobbal

48

https://www.adamheinrich.com/blog/2012/12/how-to-load-native-jni-library-from-jar/

这是一篇很棒的文章,解决了我的问题。

在我的情况下,我有以下代码来初始化库:

static {
    try {
        System.loadLibrary("crypt"); // used for tests. This library in classpath only
    } catch (UnsatisfiedLinkError e) {
        try {
            NativeUtils.loadLibraryFromJar("/natives/crypt.dll"); // during runtime. .DLL within .JAR
        } catch (IOException e1) {
            throw new RuntimeException(e1);
        }
    }
}

26
使用 NativeUtils.loadLibraryFromJar("/natives/"+System.mapLibraryName("crypt")); 可以更好。 - BlackJoker
1
你好,当我尝试使用NativeUtils类并尝试在libs/armeabi/libmyname.so中加载.so文件时,我遇到了异常java.lang.ExceptionInInitializerError,原因是:java.io.FileNotFoundException: File /native/libhellojni.so was not found inside JAR。请告诉我为什么会出现这种异常。谢谢。 - Ganesh

16

1)将本地库作为资源包含在您的JAR文件中。例如,使用Maven或Gradle和标准项目布局,将本地库放入main/resources目录中。

2)在与该库相关的Java类的静态初始化程序的某个位置,添加以下类似代码:

String libName = "myNativeLib.so"; // The name of the file in resources/ dir
URL url = MyClass.class.getResource("/" + libName);
File tmpDir = Files.createTempDirectory("my-native-lib").toFile();
tmpDir.deleteOnExit();
File nativeLibTmpFile = new File(tmpDir, libName);
nativeLibTmpFile.deleteOnExit();
try (InputStream in = url.openStream()) {
    Files.copy(in, nativeLibTmpFile.toPath());
}
System.load(nativeLibTmpFile.getAbsolutePath());

这个解决方案同样适用于你想要部署到像Wildfly这样的应用服务器上,而且最好的部分是它能够正确卸载应用程序! - Hash
这是避免“本地库已加载”异常的最佳解决方案。 - Krithika Vittal

16

看看One-JAR吧。它能将您的应用程序封装在一个单独的jar文件中,其中包含一个专门的类加载器,可处理“jar中的jar”等其他事项。

它通过按需将其解压缩到临时工作文件夹来处理本地 (JNI) 库

(免责声明:我从未使用过 One-JAR,至今还没有需要使用它,只是将其书签保存在那里以备不时之需。)


我不是Java程序员,不知道是否必须包装应用程序本身?这将如何与现有的类加载器结合使用?澄清一下,我将从Clojure中使用它,并希望能够将JAR作为库而不是应用程序加载。 - Alex B
啊,那可能不太合适。我想你只能将本地库文件分发到JAR文件之外,并提供将它们放置在应用程序库路径下的说明。 - Evan

5

JarClassLoader 是一个类加载器,可以从单个大型 JAR 文件和嵌套在其中的 JAR 文件中加载类、本地库和资源。


2

Kotlin解决方案:

  • build.gradle.dsl: copy kotlin runtime (kotlin-stdlib-1.4.0.jar) and native library (librust_kotlin.dylib) to JAR

    tasks.withType<Jar> {
        manifest {
            attributes("Main-Class" to "MainKt")
        }
    
        val libs = setOf("kotlin-stdlib-1.4.0.jar")
    
        from(configurations.runtimeClasspath.get()
            .filter { it.name in libs }
            .map { zipTree(it) })
    
        from("librust_kotlin.dylib")
    }
    
  • main method: copy library to a temporary file to load it using absolute path

     with(createTempFile()) {
         deleteOnExit()
         val bytes = My::class.java.getResource("librust_kotlin.dylib")
             .readBytes()
    
         outputStream().write(bytes)
         System.load(path)
     }
    

1

你可能需要将本地库解压到本地文件系统中。据我所知,执行本地加载的代码段会查看文件系统。

这段代码应该能帮助你入门(我已经有一段时间没有看过它了,而且它是为不同的目的编写的,但应该能够完成任务,而且我现在很忙,但如果你有问题,请留言,我会尽快回答)。

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;


public class FileUtils
{
    public static String getFileName(final Class<?>  owner,
                                     final String    name)
        throws URISyntaxException,
               ZipException,
               IOException
    {
        String    fileName;
        final URI uri;

        try
        {
            final String external;
            final String decoded;
            final int    pos;

            uri      = getResourceAsURI(owner.getPackage().getName().replaceAll("\\.", "/") + "/" + name, owner);
            external = uri.toURL().toExternalForm();
            decoded  = external; // URLDecoder.decode(external, "UTF-8");
            pos      = decoded.indexOf(":/");
            fileName = decoded.substring(pos + 1);
        }
        catch(final FileNotFoundException ex)
        {
            fileName = null;
        }

        if(fileName == null || !(new File(fileName).exists()))
        {
            fileName = getFileNameX(owner, name);
        }

        return (fileName);
    }

    private static String getFileNameX(final Class<?> clazz, final String name)
        throws UnsupportedEncodingException
    {
        final URL    url;
        final String fileName;

        url = clazz.getResource(name);

        if(url == null)
        {
            fileName = name;
        }
        else
        {
            final String decoded;
            final int    pos;

            decoded  = URLDecoder.decode(url.toExternalForm(), "UTF-8");
            pos      = decoded.indexOf(":/");
            fileName = decoded.substring(pos + 1);
        }

        return (fileName);
    }

    private static URI getResourceAsURI(final String    resourceName,
                                       final Class<?> clazz)
        throws URISyntaxException,
               ZipException,
               IOException
    {
        final URI uri;
        final URI resourceURI;

        uri         = getJarURI(clazz);
        resourceURI = getFile(uri, resourceName);

        return (resourceURI);
    }

    private static URI getJarURI(final Class<?> clazz)
        throws URISyntaxException
    {
        final ProtectionDomain domain;
        final CodeSource       source;
        final URL              url;
        final URI              uri;

        domain = clazz.getProtectionDomain();
        source = domain.getCodeSource();
        url    = source.getLocation();
        uri    = url.toURI();

        return (uri);
    }

    private static URI getFile(final URI    where,
                               final String fileName)
        throws ZipException,
               IOException
    {
        final File location;
        final URI  fileURI;

        location = new File(where);

        // not in a JAR, just return the path on disk
        if(location.isDirectory())
        {
            fileURI = URI.create(where.toString() + fileName);
        }
        else
        {
            final ZipFile zipFile;

            zipFile = new ZipFile(location);

            try
            {
                fileURI = extract(zipFile, fileName);
            }
            finally
            {
                zipFile.close();
            }
        }

        return (fileURI);
    }

    private static URI extract(final ZipFile zipFile,
                               final String  fileName)
        throws IOException
    {
        final File         tempFile;
        final ZipEntry     entry;
        final InputStream  zipStream;
        OutputStream       fileStream;

        tempFile = File.createTempFile(fileName.replace("/", ""), Long.toString(System.currentTimeMillis()));
        tempFile.deleteOnExit();
        entry    = zipFile.getEntry(fileName);

        if(entry == null)
        {
            throw new FileNotFoundException("cannot find file: " + fileName + " in archive: " + zipFile.getName());
        }

        zipStream  = zipFile.getInputStream(entry);
        fileStream = null;

        try
        {
            final byte[] buf;
            int          i;

            fileStream = new FileOutputStream(tempFile);
            buf        = new byte[1024];
            i          = 0;

            while((i = zipStream.read(buf)) != -1)
            {
                fileStream.write(buf, 0, i);
            }
        }
        finally
        {
            close(zipStream);
            close(fileStream);
        }

        return (tempFile.toURI());
    }

    private static void close(final Closeable stream)
    {
        if(stream != null)
        {
            try
            {
                stream.close();
            }
            catch(final IOException ex)
            {
                ex.printStackTrace();
            }
        }
    }
}

1
如果您需要解压缩某些特定资源,我建议查看 https://github.com/zeroturnaround/zt-zip 项目。 - Neeme Praks
zt-zip看起来是一个不错的API。 - TofuBeer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接