Как связать собственную библиотеку и библиотеку JNI внутри JAR?

104

Речь идет о библиотеке Tokyo Cabinet .

Я хочу иметь собственную библиотеку, библиотеку JNI и все классы API Java в одном файле JAR, чтобы избежать проблем с перераспределением.

Кажется, на GitHub есть такая попытка , но

  1. Он не включает собственно собственную библиотеку, только библиотеку JNI.
  2. Похоже, это специфично для плагина собственных зависимостей Leiningen (он не будет работать как распространяемый компонент).

Вопрос в том, могу ли я собрать все в один JAR и распространять его? Если да, то как?

PS: Да, я понимаю, что это может иметь последствия для переносимости.

Алекс Б
источник

Ответы:

54

Можно создать один файл JAR со всеми зависимостями, включая собственные библиотеки JNI для одной или нескольких платформ. Базовый механизм заключается в использовании System.load (File) для загрузки библиотеки вместо типичного System.loadLibrary (String), который ищет системное свойство java.library.path. Этот метод значительно упрощает установку, поскольку пользователю не нужно устанавливать библиотеку JNI в своей системе за счет, однако, того, что все платформы могут не поддерживаться, поскольку конкретная библиотека для платформы может не быть включена в один файл JAR. .

Процесс выглядит следующим образом:

  • включить собственные библиотеки JNI в файл JAR в месте, специфическом для платформы, например, в NATIVE / $ {os.arch} / $ {os.name} /libname.lib
  • создать код в статическом инициализаторе основного класса, чтобы
    • вычислить текущие os.arch и os.name
    • найдите библиотеку в файле JAR в предопределенном месте с помощью Class.getResource (String)
    • если он существует, извлеките его во временный файл и загрузите с помощью System.load (File).

Я добавил функциональность, чтобы сделать это для jzmq, привязок Java к ZeroMQ (бесстыдный плагин). Код можно найти здесь . В коде jzmq используется гибридное решение, поэтому, если встроенная библиотека не может быть загружена, код вернется к поиску библиотеки JNI по пути java.library.path.

kwo
источник
1
Я люблю это! Это избавляет от многих проблем при интеграции, и вы всегда можете вернуться к «старому» способу с помощью System.loadLibrary () в случае неудачи. Думаю, я начну этим пользоваться. Спасибо! :)
Matthieu
1
Что мне делать, если моя библиотека DLL зависит от другой библиотеки DLL? Я всегда получаю сообщение, UnsatisfiedLinkErrorчто первая dll неявно хочет загрузить последнюю, но не может найти ее, так как она скрыта в банке. Также загрузка последней DLL первой не помогает.
fabb
Остальные иждивенцы DLLдолжны быть известны заранее, и их расположение должно быть добавлено в PATHпеременную среды.
eigenfield
Прекрасно работает! Если кому-то это непонятно, добавьте класс EmbeddedLibraryTools в свой проект и соответствующим образом измените его.
Джон Ла Марр,
41

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 и пытаюсь разместить файл .so внутри libs / armeabi / libmyname.so, я получаю исключение, такое как java.lang.ExceptionInInitializerError, вызванное: java.io.FileNotFoundException: Файл /native/libhellojni.so не найден внутри JAR. Пожалуйста, дайте мне знать, почему я получаю исключение. Спасибо
Ганеш
16

Взгляните на One-JAR . Он заключит ваше приложение в один файл jar со специализированным загрузчиком классов, который, помимо прочего, обрабатывает "банки внутри jar".

Он обрабатывает собственные (JNI) библиотеки, при необходимости распаковывая их во временную рабочую папку.

(Отказ от ответственности: я никогда не использовал One-JAR, пока в нем не нуждался, просто пометил его закладкой на черный день.)

Эван
источник
Я не программист на Java, есть идеи, нужно ли оборачивать само приложение? Как это будет работать в сочетании с существующим загрузчиком классов? Чтобы уточнить, я собираюсь использовать его из Clojure и хочу иметь возможность загружать JAR как библиотеку, а не как приложение.
Alex B
Ааа, это, наверное, не подходит чем. Думаю, вы застрянете с распространением собственной библиотеки (библиотек) за пределами файла jar с инструкциями по размещению их в пути к библиотеке приложения.
Эван
9

1) Включите родную библиотеку в свой JAR как ресурс. E. g. с 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, И, что самое приятное, оно может правильно выгрузить приложение!
Хэш
Это лучшее решение, чтобы избежать загруженного исключения «уже загруженная собственная библиотека».
Критика Виттал,
5

JarClassLoader - это загрузчик классов для загрузки классов, собственных библиотек и ресурсов из одного монстра JAR и из JAR внутри монстра JAR.

Текумара
источник
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
Если вам нужно отключить какой-то конкретный ресурс, я рекомендую взглянуть на проект github.com/zeroturnaround/zt-zip
Neeme Praks 01
zt-zip выглядит как достойный API.
TofuBeer 02
0

Решение для Котлина:

  • build.gradle.dsl: скопируйте среду выполнения kotlin (kotlin-stdlib-1.4.0.jar) и собственную библиотеку (librust_kotlin.dylib) в 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 метод: скопируйте библиотеку во временный файл, чтобы загрузить ее по абсолютному пути

     with(createTempFile()) {
         deleteOnExit()
         val bytes = My::class.java.getResource("librust_kotlin.dylib")
             .readBytes()
    
         outputStream().write(bytes)
         System.load(path)
     }
Макс Фарсиков
источник