Wie bündle ich eine native Bibliothek und eine JNI-Bibliothek in einer JAR?

101

Die fragliche Bibliothek ist Tokyo Cabinet .

Ich möchte, dass die native Bibliothek, die JNI-Bibliothek und alle Java-API-Klassen in einer JAR-Datei enthalten sind, um Umverteilungsprobleme zu vermeiden.

Bei GitHub scheint es einen Versuch zu geben , aber

  1. Es enthält nicht die eigentliche native Bibliothek, sondern nur die JNI-Bibliothek.
  2. Es scheint spezifisch für Leiningens natives Abhängigkeits-Plugin zu sein (es funktioniert nicht als weiterverteilbar).

Die Frage ist, kann ich alles in einem JAR bündeln und neu verteilen? Wenn ja, wie?

PS: Ja, mir ist klar, dass dies Auswirkungen auf die Portabilität haben kann.

Alex B.
quelle

Antworten:

54

Es ist möglich, eine einzelne JAR-Datei mit allen Abhängigkeiten einschließlich der nativen JNI-Bibliotheken für eine oder mehrere Plattformen zu erstellen. Der grundlegende Mechanismus besteht darin, System.load (Datei) zum Laden der Bibliothek anstelle der typischen System.loadLibrary (String) zu verwenden, die die Systemeigenschaft java.library.path durchsucht. Diese Methode vereinfacht die Installation erheblich, da der Benutzer die JNI-Bibliothek nicht auf seinem System installieren muss, jedoch auf Kosten der Tatsache, dass möglicherweise nicht alle Plattformen unterstützt werden, da die spezifische Bibliothek für eine Plattform möglicherweise nicht in der einzelnen JAR-Datei enthalten ist .

Der Prozess ist wie folgt:

  • Fügen Sie die nativen JNI-Bibliotheken an einem plattformspezifischen Speicherort in die JAR-Datei ein, z. B. unter NATIVE / $ {os.arch} / $ {os.name} /libname.lib
  • Erstellen Sie Code in einem statischen Initialisierer der Hauptklasse zu
    • Berechnen Sie den aktuellen os.arch und os.name
    • Suchen Sie mit Class.getResource (String) in der JAR-Datei am vordefinierten Speicherort nach der Bibliothek.
    • Wenn es existiert, extrahieren Sie es in eine temporäre Datei und laden Sie es mit System.load (Datei).

Ich habe Funktionen hinzugefügt, um dies für jzmq, die Java-Bindungen von ZeroMQ (schamloser Plug), zu tun. Den Code finden Sie hier . Der jzmq-Code verwendet eine Hybridlösung. Wenn eine eingebettete Bibliothek nicht geladen werden kann, sucht der Code wieder nach der JNI-Bibliothek im Pfad java.library.path.

kwo
quelle
1
Ich liebe es! Dies erspart der Integration viel Ärger und Sie können mit System.loadLibrary () jederzeit auf den "alten" Weg zurückkehren, falls dies fehlschlägt. Ich denke, ich werde damit anfangen. Vielen Dank! :)
Matthieu
1
Was mache ich, wenn meine Bibliotheks-DLL von einer anderen DLL abhängig ist? Ich bekomme immer eine UnsatisfiedLinkErrorals Laden der ersteren DLL will implizit die letztere laden, kann sie aber nicht finden, da sie im Glas versteckt ist. Auch das erstmalige Laden der letzteren DLL hilft nicht.
fabb
Die anderen abhängigen Personen DLLmüssen im Voraus bekannt sein und ihre Standorte sollten in der PATHUmgebungsvariablen hinzugefügt werden.
Truthadjustr
Funktioniert super! Wenn es jemandem nicht klar ist, der darauf stößt, fügen Sie die EmbeddedLibraryTools-Klasse in Ihr Projekt ein und ändern Sie sie entsprechend.
Jon La Marr
41

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

ist ein großartiger Artikel, der mein Problem löst.

In meinem Fall habe ich den folgenden Code zum Initialisieren der Bibliothek:

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);
        }
    }
}
Davs
quelle
26
benutze NativeUtils.loadLibraryFromJar ("/ natives /" + System.mapLibraryName ("crypt")); könnte besser sein
BlackJoker
1
Hallo, wenn ich versuche, die NativeUtils-Klasse zu verwenden und die .so-Datei in libs / armeabi / libmyname.so abzulegen, erhalte ich die Ausnahme wie java.lang.ExceptionInInitializerError, verursacht durch: java.io.FileNotFoundException: Die Datei /native/libhellojni.so wurde in JAR nicht gefunden. Bitte lassen Sie mich wissen, warum ich die Ausnahme bekomme. Vielen Dank
Ganesh
16

Schauen Sie sich One-JAR an . Es wird Ihre Anwendung in einer einzigen JAR-Datei mit einem speziellen Klassenladeprogramm zusammenfassen, das unter anderem "Jars in Jars" verarbeitet.

Es verarbeitet native (JNI) Bibliotheken, indem es sie nach Bedarf in einen temporären Arbeitsordner entpackt.

(Haftungsausschluss: Ich habe One-JAR noch nie verwendet, musste es noch nicht, habe es nur für einen regnerischen Tag mit einem Lesezeichen versehen.)

Evan
quelle
Ich bin kein Java-Programmierer. Haben Sie eine Idee, ob ich die Anwendung selbst verpacken muss? Wie würde dies in Kombination mit einem vorhandenen Klassenladeprogramm funktionieren? Zur Verdeutlichung werde ich es von Clojure aus verwenden und möchte eine JAR als Bibliothek und nicht als Anwendung laden können.
Alex B
Ahhh, es wäre wahrscheinlich nicht geeignet als. Denken Sie, Sie müssen Ihre native Bibliothek (en) nicht mehr außerhalb der JAR-Datei verteilen, sondern erhalten Anweisungen, um sie in den Bibliothekspfad der Anwendung einzufügen.
Evan
8

1) Fügen Sie die native Bibliothek als Ressource in Ihre JAR ein. Z.B. Fügen Sie mit Maven oder Gradle und dem Standardprojektlayout die native Bibliothek einmain/resources Verzeichnis.

2) Geben Sie den Code in statischen Initialisierern von Java-Klassen, die sich auf diese Bibliothek beziehen, wie folgt ein:

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());
leventov
quelle
Diese Lösung funktioniert auch, wenn Sie etwas auf einem Anwendungsserver wie wildfly bereitstellen möchten UND das Beste daran ist, dass die Anwendung ordnungsgemäß entladen werden kann!
Hash
Dies ist die beste Lösung, um eine Ausnahme zu vermeiden, bei der die native Bibliothek bereits geladen ist.
Krithika Vittal
5

JarClassLoader ist ein Klassenladeprogramm zum Laden von Klassen, nativen Bibliotheken und Ressourcen aus einer einzelnen Monster-JAR und aus JARs innerhalb der Monster-JAR.

Tekumara
quelle
1

Wahrscheinlich müssen Sie die native Bibliothek vom lokalen Dateisystem trennen. Soweit ich weiß, befasst sich der Code, der das native Laden ausführt, mit dem Dateisystem.

Dieser Code soll Ihnen den Einstieg erleichtern (ich habe ihn eine Weile nicht mehr angeschaut und er dient einem anderen Zweck, sollte aber den Trick machen, und ich bin im Moment ziemlich beschäftigt, aber wenn Sie Fragen haben, hinterlassen Sie einfach einen Kommentar und ich werde so schnell wie möglich antworten.

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();
            }
        }
    }
}
TofuBeer
quelle
1
Wenn Sie eine bestimmte Ressource entfernen müssen, empfehlen wir Ihnen, einen Blick auf das Projekt github.com/zeroturnaround/zt-zip zu werfen
Neeme Praks
zt-zip sieht aus wie eine anständige API.
TofuBeer