Jak połączyć bibliotekę natywną i bibliotekę JNI w JAR?


101

Biblioteka, o której mowa, to Tokyo Cabinet .

Chcę mieć natywną bibliotekę, bibliotekę JNI i wszystkie klasy Java API w jednym pliku JAR, aby uniknąć problemów związanych z redystrybucją.

Wydaje się, że jest to próba na GitHub , ale

  1. Nie obejmuje rzeczywistej biblioteki natywnej, tylko bibliotekę JNI.
  2. Wydaje się, że jest specyficzny dla natywnej wtyczki zależności Leiningen (nie będzie działać jako redystrybucja).

Pytanie brzmi, czy mogę spakować wszystko w jednym pliku JAR i rozpowszechniać go? Jeśli tak, w jaki sposób?

PS: Tak, zdaję sobie sprawę, że może to mieć wpływ na przenośność.

Odpowiedzi:


54

Możliwe jest utworzenie pojedynczego pliku JAR ze wszystkimi zależnościami, w tym z natywnymi bibliotekami JNI dla jednej lub większej liczby platform. Podstawowym mechanizmem jest użycie System.load (File) do załadowania biblioteki zamiast typowego System.loadLibrary (String), który przeszukuje właściwość systemową java.library.path. Ta metoda znacznie upraszcza instalację, ponieważ użytkownik nie musi instalować biblioteki JNI w swoim systemie, kosztem jednak, że wszystkie platformy mogą nie być obsługiwane, ponieważ konkretna biblioteka dla platformy może nie być zawarta w jednym pliku JAR .

Proces przebiega następująco:

  • dołącz natywne biblioteki JNI do pliku JAR w lokalizacji specyficznej dla platformy, na przykład w NATIVE / $ {os.arch} / $ {os.name} /libname.lib
  • utworzyć kod w statycznym inicjatorze klasy głównej do
    • oblicz aktualny os.arch i os.name
    • poszukaj biblioteki w pliku JAR w predefiniowanej lokalizacji za pomocą Class.getResource (String)
    • jeśli istnieje, wyodrębnij go do pliku tymczasowego i załaduj za pomocą System.load (plik).

Dodałem funkcjonalność, aby to zrobić dla jzmq, powiązań Java ZeroMQ (bezwstydna wtyczka). Kod można znaleźć tutaj . Kod jzmq wykorzystuje rozwiązanie hybrydowe, więc jeśli nie można załadować wbudowanej biblioteki, kod powróci do wyszukiwania biblioteki JNI w java.library.path.


1
Kocham to! Oszczędza to wiele kłopotów przy integracji i zawsze możesz powrócić do „starego” sposobu za pomocą System.loadLibrary () na wypadek niepowodzenia. Myślę, że zacznę tego używać. Dzięki! :)
Matthieu

1
Co mam zrobić, jeśli moja biblioteka dll jest zależna od innej biblioteki dll? Zawsze otrzymuję, UnsatisfiedLinkErrorponieważ ładowanie poprzedniej biblioteki dll niejawnie chce załadować drugą, ale nie mogę jej znaleźć, ponieważ jest ukryta w słoiku. Również ładowanie tej ostatniej biblioteki dll w pierwszej kolejności nie pomaga.
fabb

Inne elementy zależne DLLmuszą być znane z wyprzedzeniem, a ich lokalizacje należy dodać do PATHzmiennej środowiskowej.
trueadjustr

Działa świetnie! Jeśli nie jest to jasne dla kogoś, kto się z tym spotka, upuść klasę EmbeddedLibraryTools do swojego projektu i odpowiednio ją zmień.
Jon La Marr

41

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

to świetny artykuł, który rozwiązuje mój problem.

W moim przypadku mam następujący kod do inicjalizacji biblioteki:

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
use NativeUtils.loadLibraryFromJar ("/ natives /" + System.mapLibraryName ("crypt")); mogłoby być lepiej
BlackJoker

1
Cześć, kiedy próbuję użyć klasy NativeUtils i próbuję umieścić plik .so w libs / armeabi / libmyname.so, otrzymuję wyjątek, taki jak java.lang.ExceptionInInitializerError, spowodowany przez: java.io.FileNotFoundException: Plik /native/libhellojni.so nie został znaleziony w JAR. Proszę, daj mi znać, dlaczego otrzymuję wyjątek. Dziękuję
Ganesh

16

Spójrz na One-JAR . Spowoduje to umieszczenie aplikacji w jednym pliku jar z wyspecjalizowanym programem ładującym klasy, który obsługuje między innymi „pliki jar wewnątrz jars”.

To obsługuje natywne (JNI) bibliotek przez rozpakowanie je do tymczasowego folderu roboczego zgodnie z wymaganiami.

(Zastrzeżenie: nigdy nie używałem One-JAR, jeszcze nie musiałem, po prostu dodałem go do zakładek na deszczowy dzień.)


Nie jestem programistą Java, masz jakiś pomysł, czy muszę opakować samą aplikację? Jak to działałoby w połączeniu z istniejącym programem ładującym klasy? Aby wyjaśnić, zamierzam użyć go z Clojure i chcę mieć możliwość załadowania pliku JAR jako biblioteki, a nie jako aplikacji.
Alex B

Ahhh, prawdopodobnie nie byłby odpowiedni niż. Myślę, że utkniesz z dystrybucją natywnych bibliotek poza plikiem jar, z instrukcjami umieszczenia ich w ścieżce biblioteki aplikacji.
Evan

9

1) Dołącz bibliotekę natywną do pliku JAR jako zasób. Np. z Maven lub Gradle i standardowym układem projektu, umieść bibliotekę natywną w main/resourceskatalogu.

2) Gdzieś w statycznych inicjalizatorach klas Javy, związanych z tą biblioteką, umieść kod w następujący sposób:

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());

To rozwiązanie działa również w przypadku, gdy chcesz wdrożyć coś na serwerze aplikacji, takim jak Wildfly, A najlepsze jest to, że jest w stanie poprawnie rozładować aplikację!
Hash

Jest to najlepsze rozwiązanie, aby uniknąć wyjątku załadowanej biblioteki natywnej.
Krithika Vittal

freak ........... to działa
Juhan


1

Prawdopodobnie będziesz musiał rozpakować natywną bibliotekę do lokalnego systemu plików. O ile wiem, fragment kodu, który wykonuje natywne ładowanie, patrzy na system plików.

Ten kod powinien pomóc Ci zacząć (nie patrzyłem na niego od jakiegoś czasu i służy do innego celu, ale powinien załatwić sprawę, a ja jestem w tej chwili dość zajęty, ale jeśli masz pytania, zostaw komentarz i odpowiem najszybciej jak będę mógł).

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
Jeśli chcesz odblokować jakiś konkretny zasób, polecam zajrzeć na github.com/zeroturnaround/zt-zip project
Neeme Praks

zt-zip wygląda na przyzwoity interfejs API.
TofuBeer
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.