วิธีรวมไลบรารีเนทีฟและไลบรารี JNI ไว้ใน JAR


104

ห้องสมุดในคำถามคือคณะรัฐมนตรีโตเกียว

ฉันต้องการมีไลบรารีเนทีฟไลบรารี JNI และคลาส Java API ทั้งหมดในไฟล์ 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
  • สร้างโค้ดใน initializier แบบคงที่ของคลาสหลักเป็น
    • คำนวณ os.arch และ os.name ปัจจุบัน
    • ค้นหาไลบรารีในไฟล์ JAR ที่ตำแหน่งที่กำหนดไว้ล่วงหน้าโดยใช้ Class.getResource (String)
    • หากมีอยู่ให้แตกไฟล์ลงในไฟล์ temp และโหลดด้วย System.load (File)

ฉันเพิ่มฟังก์ชันเพื่อทำสิ่งนี้สำหรับ jzmq การรวม Java ของ ZeroMQ (ปลั๊กไร้ยางอาย) รหัสที่สามารถพบได้ที่นี่ โค้ด jzmq ใช้โซลูชันแบบไฮบริดดังนั้นหากไม่สามารถโหลดไลบรารีแบบฝังได้โค้ดจะเปลี่ยนกลับเป็นการค้นหาไลบรารี JNI ตาม java.library.path


1
ฉันรักมัน! ช่วยประหยัดปัญหาในการผสานรวมได้มากและคุณสามารถเปลี่ยนกลับไปใช้วิธี "เก่า" ได้ตลอดเวลาด้วย System.loadLibrary () ในกรณีที่ล้มเหลว ฉันคิดว่าฉันจะเริ่มใช้มัน ขอบคุณ! :)
Matthieu

1
ฉันจะทำอย่างไรถ้า dll ไลบรารีของฉันมีการพึ่งพากับ dll อื่น ฉันมักจะได้รับการUnsatisfiedLinkErrorโหลด dll ในอดีตโดยปริยายต้องการโหลดหลังเสมอ แต่ไม่สามารถค้นหาได้เนื่องจากซ่อนอยู่ในโถ การโหลด dll หลังก่อนยังไม่ช่วย
fabb

DLLต้องทราบผู้อยู่ในอุปการะอื่น ๆล่วงหน้าและควรเพิ่มตำแหน่งของพวกเขาในPATHตัวแปรสภาพแวดล้อม
eigenfield

ใช้งานได้ดี! หากไม่ชัดเจนสำหรับใครบางคนที่พบปัญหานี้ให้ปล่อยคลาส EmbeddedLibraryTools ในโครงการของคุณและเปลี่ยนตามนั้น
Jon La Marr

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

ลองดูที่หนึ่ง JAR มันจะรวมแอปพลิเคชันของคุณไว้ในไฟล์ jar เดียวด้วยตัวโหลดคลาสเฉพาะที่จัดการ "jar within jar" เหนือสิ่งอื่นใด

มันจับพื้นเมือง (JNI) ห้องสมุดโดยการเปิดออกไปยังโฟลเดอร์การทำงานชั่วคราวตามที่ต้องการ

(ข้อจำกัดความรับผิดชอบ: ฉันไม่เคยใช้ One-JAR แต่ยังไม่จำเป็นต้องทำเช่นนั้นเพียงแค่บุ๊กมาร์กสำหรับวันที่ฝนตก)


ฉันไม่ใช่โปรแกรมเมอร์ Java มีความคิดอย่างไรถ้าฉันต้องปิดแอปพลิเคชัน วิธีนี้จะทำงานร่วมกับ classloader ที่มีอยู่ได้อย่างไร เพื่อความชัดเจนฉันจะใช้มันจาก Clojure และต้องการให้สามารถโหลด JAR เป็นไลบรารีแทนที่จะเป็นแอปพลิเคชัน
Alex B

อ่ามันคงไม่เหมาะไปกว่า คิดว่าคุณจะติดอยู่กับการแจกจ่ายไลบรารีดั้งเดิมของคุณนอกไฟล์ jar พร้อมคำแนะนำในการวางไว้ในเส้นทางไลบรารีของแอปพลิเคชัน
Evan

9

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

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 project
Neeme Praks

zt-zip ดูเหมือน API ที่ดี
TofuBeer

0

ทางออกสำหรับ Kotlin:

  • 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)
     }
    
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.