¿Cómo agrupar una biblioteca nativa y una biblioteca JNI dentro de un JAR?

101

La biblioteca en cuestión es el Tokyo Cabinet .

Lo que quiero es tener la biblioteca nativa, la biblioteca JNI y todas las clases de API de Java en un archivo JAR para evitar dolores de cabeza por redistribución.

Parece haber un intento de esto en GitHub , pero

  1. No incluye la biblioteca nativa real, solo la biblioteca JNI.
  2. Parece ser específico del complemento de dependencias nativas de Leiningen (no funcionará como redistribuible).

La pregunta es, ¿puedo agrupar todo en un JAR y redistribuirlo? Si es así, ¿cómo?

PD: Sí, me doy cuenta de que puede tener implicaciones en la portabilidad.

Alex B
fuente

Respuestas:

54

Es posible crear un solo archivo JAR con todas las dependencias, incluidas las bibliotecas JNI nativas para una o más plataformas. El mecanismo básico es usar System.load (Archivo) para cargar la biblioteca en lugar del típico System.loadLibrary (String) que busca la propiedad del sistema java.library.path. Este método hace que la instalación sea mucho más sencilla, ya que el usuario no tiene que instalar la biblioteca JNI en su sistema, a expensas, sin embargo, de que es posible que no todas las plataformas sean compatibles, ya que la biblioteca específica para una plataforma podría no estar incluida en el archivo JAR único. .

El proceso es el siguiente:

  • incluir las bibliotecas JNI nativas en el archivo JAR en una ubicación específica de la plataforma, por ejemplo en NATIVE / $ {os.arch} / $ {os.name} /libname.lib
  • crear código en un inicializador estático de la clase principal para
    • calcula el os.arch y os.name actuales
    • busque la biblioteca en el archivo JAR en la ubicación predefinida usando Class.getResource (String)
    • si existe, extráigalo a un archivo temporal y cárguelo con System.load (Archivo).

Agregué funcionalidad para hacer esto para jzmq, los enlaces Java de ZeroMQ (enchufe desvergonzado). El código se puede encontrar aquí . El código jzmq utiliza una solución híbrida de modo que si no se puede cargar una biblioteca incrustada, el código volverá a buscar la biblioteca JNI a lo largo de java.library.path.

kwo
fuente
1
¡Me encanta! Ahorra muchos problemas para la integración, y siempre puede volver a la forma "antigua" con System.loadLibrary () en caso de que falle. Creo que empezaré a usar eso. ¡Gracias! :)
Matthieu
1
¿Qué hago si la dll de mi biblioteca depende de otra dll? Siempre obtengo un UnsatisfiedLinkErrormensaje al cargar el dll anterior que implícitamente quiere cargar el último, pero no puedo encontrarlo porque está oculto en el frasco. Además, cargar la última dll primero no ayuda.
fabb
Los otros dependientes DLLdeben conocerse de antemano y sus ubicaciones deben agregarse en la PATHvariable de entorno.
truthadjustr
¡Funciona genial! Si no está claro para alguien que se encuentre con esto, suelte la clase EmbeddedLibraryTools en su proyecto y cámbiela en consecuencia.
Jon La Marr
41

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

es un gran artículo, que resuelve mi problema ..

En mi caso, tengo el siguiente código para inicializar la biblioteca:

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
fuente
26
use NativeUtils.loadLibraryFromJar ("/ natives /" + System.mapLibraryName ("cripta")); podría ser mejor
BlackJoker
1
Hola, cuando intento usar la clase NativeUtils y trato de colocar el archivo .so dentro de libs / armeabi / libmyname.so, obtengo una excepción como java.lang.ExceptionInInitializerError, Causado por: java.io.FileNotFoundException: El archivo /native/libhellojni.so no se encontró dentro de JAR. Hágame saber por qué recibo la excepción. Gracias
Ganesh
16

Eche un vistazo a One-JAR . Envolverá su aplicación en un solo archivo jar con un cargador de clases especializado que maneja "jarras dentro de jarras", entre otras cosas.

Se encarga de bibliotecas nativas (JNI) por desempaquetarlos a una carpeta de trabajo temporal según sea necesario.

(Descargo de responsabilidad: nunca he usado One-JAR, aún no lo he necesitado, solo lo he marcado como favorito para un día lluvioso).

Evan
fuente
No soy un programador de Java, ¿alguna idea si tengo que ajustar la aplicación? ¿Cómo funcionaría esto en combinación con un cargador de clases existente? Para aclarar, lo usaré desde Clojure y quiero poder cargar un JAR como una biblioteca, en lugar de como una aplicación.
Alex B
Ahhh, probablemente no sería adecuado. Piense que no podrá distribuir sus bibliotecas nativas fuera del archivo jar, con instrucciones para colocarlas en la ruta de la biblioteca de la aplicación.
Evan
8

1) Incluya la biblioteca nativa en su JAR como recurso. P.ej. con Maven o Gradle, y el diseño de proyecto estándar, coloque la biblioteca nativa en el main/resourcesdirectorio.

2) En algún lugar de los inicializadores estáticos de las clases de Java, relacionados con esta biblioteca, coloque el código como el siguiente:

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
fuente
Esta solución también funciona en caso de que desee implementar algo en un servidor de aplicaciones como wildfly Y la mejor parte es que puede descargar la aplicación correctamente.
Hash
Esta es la mejor solución para evitar la excepción de 'biblioteca nativa ya cargada'.
Krithika Vittal
5

JarClassLoader es un cargador de clases para cargar clases, bibliotecas nativas y recursos desde un solo JAR monstruo y desde JARs dentro del JAR monstruo.

tekumara
fuente
1

Probablemente tendrá que descomprimir la biblioteca nativa en el sistema de archivos local. Hasta donde yo sé, el fragmento de código que realiza la carga nativa examina el sistema de archivos.

Este código debería ayudarlo a comenzar (no lo he visto en un tiempo, y es para un propósito diferente, pero debería funcionar, y estoy bastante ocupado en este momento, pero si tiene preguntas, simplemente deje un comentario y responderé tan pronto como pueda).

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();
            }
        }
    }
}
Tofu cerveza
fuente
1
Si necesita abrir algún recurso específico, le recomiendo echar un vistazo al proyecto github.com/zeroturnaround/zt-zip
Neeme Praks
zt-zip parece una API decente.
TofuBeer