Bagaimana cara menggabungkan perpustakaan asli dan perpustakaan JNI di dalam JAR?

101

Perpustakaan yang dimaksud adalah Kabinet Tokyo .

Saya ingin memiliki perpustakaan asli, perpustakaan JNI, dan semua kelas Java API dalam satu file JAR untuk menghindari sakit kepala redistribusi.

Tampaknya ada upaya untuk melakukan ini di GitHub , tetapi

  1. Ini tidak termasuk perpustakaan asli yang sebenarnya, hanya perpustakaan JNI.
  2. Tampaknya ini khusus untuk plugin dependensi asli Leiningen (tidak akan berfungsi sebagai dapat didistribusikan ulang).

Pertanyaannya adalah, dapatkah saya menggabungkan semuanya dalam satu JAR dan mendistribusikannya kembali? Jika ya, bagaimana caranya?

PS: Ya, saya sadari mungkin ada implikasi portabilitasnya.

Alex B
sumber

Jawaban:

54

Anda dapat membuat satu file JAR dengan semua dependensi termasuk library JNI asli untuk satu atau beberapa platform. Mekanisme dasarnya adalah menggunakan System.load (File) untuk memuat pustaka, bukan System.loadLibrary (String) tipikal yang mencari properti sistem java.library.path. Metode ini membuat penginstalan jauh lebih sederhana karena pengguna tidak harus menginstal pustaka JNI di sistemnya, dengan biaya, bagaimanapun, semua platform mungkin tidak didukung karena pustaka khusus untuk platform mungkin tidak disertakan dalam satu file JAR .

Prosesnya adalah sebagai berikut:

  • sertakan pustaka JNI asli dalam file JAR di lokasi yang spesifik untuk platform, misalnya di NATIVE / $ {os.arch} / $ {os.name} /libname.lib
  • membuat kode dalam penginisialisasi statis kelas utama untuk
    • hitung os.arch dan os.name saat ini
    • cari perpustakaan di file JAR di lokasi yang telah ditentukan menggunakan Class.getResource (String)
    • jika ada, ekstrak ke file temp dan muat dengan System.load (File).

Saya menambahkan fungsionalitas untuk melakukan ini untuk jzmq, Java binding dari ZeroMQ (steker tak tahu malu). Kode dapat ditemukan di sini . Kode jzmq menggunakan solusi hybrid sehingga jika pustaka yang disematkan tidak dapat dimuat, kode akan kembali mencari pustaka JNI di sepanjang java.library.path.

kwo
sumber
1
Aku menyukainya! Ini menghemat banyak masalah untuk integrasi, dan Anda selalu dapat kembali ke cara "lama" dengan System.loadLibrary () jika gagal. Saya pikir saya akan mulai menggunakan itu. Terima kasih! :)
Matthieu
1
Apa yang harus saya lakukan jika dll perpustakaan saya memiliki ketergantungan ke dll lain? Saya selalu mendapatkan UnsatisfiedLinkErroras memuat dll mantan secara implisit ingin memuat yang terakhir, tetapi tidak dapat menemukannya karena tersembunyi di toples. Juga memuat dll terakhir pertama tidak membantu.
fabb
Para tanggungan lainnya DLLharus diketahui terlebih dahulu dan lokasinya harus ditambahkan dalam PATHvariabel lingkungan.
truthadjustr
Bekerja dengan baik! Jika tidak jelas bagi seseorang yang menemukan ini, lepaskan kelas EmbeddedLibraryTools ke proyek Anda dan ubah sesuai.
Jon La Marr
41

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

adalah artikel bagus, yang memecahkan masalah saya ..

Dalam kasus saya, saya punya kode berikut untuk menginisialisasi perpustakaan:

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
sumber
26
gunakan NativeUtils.loadLibraryFromJar ("/ natives /" + System.mapLibraryName ("crypt")); bisa lebih baik
BlackJoker
1
Hai ketika saya mencoba menggunakan kelas NativeUtils dan mencoba memasukkan file .so ke dalam libs / armeabi / libmyname.so, saya mendapatkan pengecualian seperti java.lang.ExceptionInInitializerError, Disebabkan oleh: java.io.FileNotFoundException: File /native/libhellojni.so tidak ditemukan di dalam JAR. Tolong beri tahu saya mengapa saya mendapatkan pengecualian. Terima kasih
Ganesh
16

Lihat One-JAR . Ini akan membungkus aplikasi Anda dalam satu file jar dengan loader kelas khusus yang antara lain menangani "guci di dalam guci".

Ini menangani pustaka asli (JNI) dengan membongkar mereka ke folder kerja sementara sesuai kebutuhan.

(Penafian: Saya tidak pernah menggunakan One-JAR, belum perlu, baru saja menandai untuk hari hujan.)

Evan
sumber
Saya bukan seorang programmer Java, ada ide jika saya harus membungkus aplikasi itu sendiri? Bagaimana ini akan bekerja dalam kombinasi dengan classloader yang sudah ada? Untuk memperjelas, saya akan menggunakannya dari Clojure dan ingin memuat JAR sebagai pustaka, bukan sebagai aplikasi.
Alex B
Ahhh, itu mungkin tidak akan cocok daripada. Pikirkan Anda akan terjebak dengan mendistribusikan perpustakaan asli Anda di luar file jar, dengan instruksi untuk meletakkannya di jalur perpustakaan aplikasi.
Evan
8

1) Sertakan perpustakaan asli ke JAR Anda sebagai Sumber Daya. E. g. dengan Maven atau Gradle, dan tata letak proyek standar, tempatkan library nativemain/resources direktori.

2) Di suatu tempat di penginisialisasi statis kelas Java, terkait dengan pustaka ini, letakkan kode seperti berikut:

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.dll
sumber
Solusi ini juga berfungsi jika Anda ingin menyebarkan sesuatu ke server aplikasi seperti wildfly DAN bagian terbaiknya adalah dapat membongkar aplikasi dengan benar!
Hash
Ini adalah solusi terbaik untuk menghindari pengecualian yang dimuat 'pustaka asli sudah'.
Krithika Vittal
5

JarClassLoader adalah pemuat kelas untuk memuat kelas, pustaka asli, dan sumber daya dari JAR monster tunggal dan dari JAR di dalam JAR monster.

tekumara
sumber
1

Anda mungkin harus membatalkan pustaka asli ke sistem file lokal. Sejauh yang saya tahu sedikit kode yang memuat tampilan asli di sistem file.

Kode ini akan membantu Anda memulai (saya sudah lama tidak melihatnya, dan ini untuk tujuan yang berbeda tetapi harus melakukan trik, dan saya cukup sibuk saat ini, tetapi jika Anda memiliki pertanyaan, tinggalkan komentar dan saya akan menjawab secepat saya bisa).

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
sumber
1
Jika Anda perlu menghapus beberapa sumber daya tertentu, saya sarankan untuk melihat proyek github.com/zeroturnaround/zt-zip
Neeme Praks
zt-zip terlihat seperti API yang layak.
TofuBeer