Làm cách nào để nhóm một thư viện gốc và một thư viện JNI bên trong một JAR?


101

Thư viện được đề cập là Nội các Tokyo .

Tôi muốn có thư viện gốc, thư viện JNI và tất cả các lớp API Java trong một tệp JAR để tránh đau đầu về phân phối lại.

Dường như có một nỗ lực về việc này tại GitHub , nhưng

  1. Nó không bao gồm thư viện gốc thực tế, chỉ có thư viện JNI.
  2. Nó dường như dành riêng cho plugin phụ thuộc gốc của Leiningen (nó sẽ không hoạt động như một plugin có thể phân phối lại).

Câu hỏi đặt ra là tôi có thể gộp mọi thứ trong một JAR và phân phối lại không? Nếu có, làm thế nào?

Tái bút: Vâng, tôi nhận thấy nó có thể có ý nghĩa về tính di động.

Câu trả lời:


54

Có thể tạo một tệp JAR duy nhất với tất cả các phụ thuộc bao gồm các thư viện JNI gốc cho một hoặc nhiều nền tảng. Cơ chế cơ bản là sử dụng System.load (Tệp) để tải thư viện thay vì System.loadLibrary (String) điển hình tìm kiếm thuộc tính hệ thống java.library.path. Phương pháp này làm cho việc cài đặt trở nên đơn giản hơn nhiều vì người dùng không phải cài đặt thư viện JNI trên hệ thống của mình, tuy nhiên, với chi phí là tất cả các nền tảng có thể không được hỗ trợ vì thư viện cụ thể cho một nền tảng có thể không được bao gồm trong tệp JAR duy nhất .

Quá trình này như sau:

  • bao gồm các thư viện JNI gốc trong tệp JAR tại một vị trí cụ thể cho nền tảng, ví dụ: tại NATIVE / $ {os.arch} / $ {os.name} /libname.lib
  • tạo mã trong trình khởi tạo tĩnh của lớp chính để
    • calc os.arch và os.name hiện tại
    • tìm kiếm thư viện trong tệp JAR tại vị trí được xác định trước bằng cách sử dụng Class.getResource (String)
    • nếu nó tồn tại, hãy giải nén nó vào một tệp tạm thời và tải nó bằng System.load (Tệp).

Tôi đã thêm chức năng để thực hiện việc này cho jzmq, các liên kết Java của ZeroMQ (trình cắm vô liêm sỉ). Mã có thể được tìm thấy ở đây . Mã jzmq sử dụng giải pháp kết hợp để nếu không thể tải thư viện nhúng, mã sẽ hoàn nguyên để tìm kiếm thư viện JNI dọc theo java.library.path.


1
Tôi thích nó! Nó giúp tiết kiệm rất nhiều rắc rối cho quá trình tích hợp và bạn luôn có thể hoàn nguyên về cách "cũ" với System.loadLibrary () trong trường hợp không thành công. Tôi nghĩ tôi sẽ bắt đầu sử dụng nó. Cảm ơn! :)
Matthieu

1
Tôi phải làm gì nếu dll thư viện của tôi phụ thuộc vào một dll khác? Tôi luôn nhận được UnsatisfiedLinkErrorkhi tải dll trước đây ngầm muốn tải dll sau, nhưng không thể tìm thấy nó vì nó được ẩn trong jar. Cũng tải dll sau trước không giúp được gì.
fabb

Những người phụ thuộc khác DLLphải được biết trước và vị trí của họ phải được thêm vào PATHbiến môi trường.
truthadjustr

Hoạt động tuyệt vời! Nếu ai đó không hiểu rõ điều này, hãy thả lớp EmbeddedLibraryTools vào dự án của bạn và thay đổi nó cho phù hợp.
Jon La Marr

41

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

là một bài báo tuyệt vời, giải quyết được vấn đề của tôi ..

Trong trường hợp của tôi, tôi có mã sau để khởi tạo thư viện:

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
sử dụng NativeUtils.loadLibraryFromJar ("/ natives /" + System.mapLibraryName ("crypt")); có thể tốt hơn
BlackJoker

1
Xin chào khi tôi đang cố gắng sử dụng lớp NativeUtils và cố gắng đăng nhập tệp .so bên trong libs / armeabi / libmyname.so, tôi nhận được ngoại lệ như java.lang.ExceptionInInitializerError, Nguyên nhân bởi: java.io.FileNotFoundException: Không tìm thấy tệp /native/libhellojni.so bên trong JAR. Vui lòng cho tôi biết lý do tại sao tôi nhận được ngoại lệ. Cảm ơn bạn
Ganesh

16

Hãy xem One-JAR . Nó sẽ gói ứng dụng của bạn trong một tệp jar duy nhất với một trình nạp lớp chuyên dụng xử lý "lọ trong lọ" trong số những thứ khác.

xử lý các thư viện gốc (JNI) bằng cách giải nén chúng vào một thư mục làm việc tạm thời theo yêu cầu.

(Tuyên bố từ chối trách nhiệm: Tôi chưa bao giờ sử dụng One-JAR, chưa cần dùng đến, chỉ cần đánh dấu trang cho một ngày mưa.)


Tôi không phải là một lập trình viên Java, bất kỳ ý tưởng nào nếu tôi phải tự gói ứng dụng? Làm thế nào điều này sẽ hoạt động khi kết hợp với một trình tải lớp hiện có? Để làm rõ, tôi sẽ sử dụng nó từ Clojure và muốn có thể tải JAR dưới dạng thư viện, thay vì dưới dạng ứng dụng.
Alex B,

Ahhh, nó có lẽ sẽ không phù hợp hơn. Hãy nghĩ rằng bạn sẽ gặp khó khăn với việc phân phối (các) thư viện gốc của mình bên ngoài tệp jar, với các hướng dẫn để đưa chúng vào đường dẫn thư viện của ứng dụng.
Evan

8

1) Đưa thư viện gốc vào JAR của bạn như một Tài nguyên. Ví dụ. với Maven hoặc Gradle, và bố cục dự án tiêu chuẩn, đưa thư viện gốc vào main/resourcesthư mục.

2) Ở đâu đó trong trình khởi tạo tĩnh của các lớp Java, liên quan đến thư viện này, hãy đặt mã như sau:

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

Giải pháp này cũng hoạt động trong trường hợp bạn muốn triển khai thứ gì đó lên một máy chủ ứng dụng như wildfly VÀ phần tốt nhất là nó có thể dỡ ứng dụng đúng cách!
Hash

Đây là giải pháp tốt nhất để tránh ngoại lệ tải 'thư viện gốc đã được tải'.
Krithika Vittal

5

JarClassLoader là trình tải lớp để tải các lớp, thư viện gốc và tài nguyên từ một JAR quái vật duy nhất và từ các JAR bên trong JAR quái vật.


1

Bạn có thể sẽ phải mở thư viện gốc vào hệ thống tệp cục bộ. Theo như tôi biết thì bit mã mà quá trình tải gốc sẽ nhìn vào hệ thống tệp.

Mã này sẽ giúp bạn bắt đầu (tôi đã không xem nó trong một thời gian và nó dành cho một mục đích khác nhưng nên thực hiện thủ thuật và tôi đang khá bận vào lúc này, nhưng nếu bạn có thắc mắc, hãy để lại bình luận và tôi sẽ trả lời ngay khi tôi có thể).

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
Nếu bạn cần phải unjar một số tài nguyên cụ thể, tôi khuyên bạn nên có một cái nhìn tại github.com/zeroturnaround/zt-zip dự án
Neeme Praks

zt-zip trông giống như một API tốt.
TofuBeer
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.