Como agrupar uma biblioteca nativa e uma biblioteca JNI dentro de um JAR?

101

A biblioteca em questão é o Gabinete de Tóquio .

Eu quero é ter a biblioteca nativa, biblioteca JNI e todas as classes de API Java em um arquivo JAR para evitar dores de cabeça de redistribuição.

Parece haver uma tentativa de fazer isso no GitHub , mas

  1. Não inclui a biblioteca nativa real, apenas a biblioteca JNI.
  2. Parece ser específico para o plugin de dependências nativas de Leiningen (não funcionará como redistribuível).

A questão é: posso agrupar tudo em um JAR e redistribuí-lo? Se sim, como?

PS: Sim, percebo que pode ter implicações na portabilidade.

Alex B
fonte

Respostas:

54

É possível criar um único arquivo JAR com todas as dependências, incluindo as bibliotecas JNI nativas para uma ou mais plataformas. O mecanismo básico é usar System.load (File) para carregar a biblioteca em vez do System.loadLibrary (String) típico, que pesquisa a propriedade de sistema java.library.path. Este método torna a instalação muito mais simples, pois o usuário não precisa instalar a biblioteca JNI em seu sistema, mas às custas de todas as plataformas podem não ser suportadas, pois a biblioteca específica para uma plataforma pode não estar incluída no arquivo JAR único .

O processo é como se segue:

  • inclua as bibliotecas JNI nativas no arquivo JAR em um local específico para a plataforma, por exemplo em NATIVE / $ {os.arch} / $ {os.name} /libname.lib
  • criar código em um inicializador estático da classe principal para
    • calcule o os.arch e os.name atuais
    • procure a biblioteca no arquivo JAR no local predefinido usando Class.getResource (String)
    • se existir, extraia-o para um arquivo temporário e carregue-o com System.load (File).

Eu adicionei funcionalidade para fazer isso para jzmq, as ligações Java de ZeroMQ (plug sem vergonha). O código pode ser encontrado aqui . O código jzmq usa uma solução híbrida para que, se uma biblioteca incorporada não puder ser carregada, o código reverterá para procurar a biblioteca JNI junto com java.library.path.

kwo
fonte
1
Eu amo isso! Isso evita muitos problemas de integração, e você sempre pode reverter para o modo "antigo" com System.loadLibrary () no caso de falha. Acho que vou começar a usar isso. Obrigado! :)
Matthieu
1
O que devo fazer se minha dll de biblioteca tiver uma dependência de outra dll? Sempre recebo um UnsatisfiedLinkErroras carregando o dll anterior implicitamente deseja carregar o último, mas não consigo encontrá-lo porque está oculto no jar. Também carregar o último dll primeiro não ajuda.
fabb
Os demais dependentes DLLdevem ser conhecidos previamente e suas localizações devem ser adicionadas na PATHvariável de ambiente.
truthadjustr
Funciona bem! Se não estiver claro para alguém que está descobrindo isso, coloque a classe EmbeddedLibraryTools em seu projeto e altere-a de acordo.
Jon La Marr
41

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

é um ótimo artigo, que resolve meu problema.

No meu caso, tenho o seguinte código para inicializar a 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
fonte
26
use NativeUtils.loadLibraryFromJar ("/ natives /" + System.mapLibraryName ("crypt")); poderia ser melhor
BlackJoker
1
Olá, quando estou tentando usar a classe NativeUtils e tentando hospedar o arquivo .so dentro de libs / armeabi / libmyname.so, estou recebendo a exceção como java.lang.ExceptionInInitializerError, causada por: java.io.FileNotFoundException: O arquivo /native/libhellojni.so não foi encontrado no JAR. Informe por que estou recebendo a exceção. Obrigado
Ganesh
16

Dê uma olhada no One-JAR . Ele embrulhará seu aplicativo em um único arquivo jar com um carregador de classes especializado que manipula "jars dentro de jars", entre outras coisas.

Ele lida com bibliotecas nativas (JNI) descompactando-as em uma pasta de trabalho temporária conforme necessário.

(Isenção de responsabilidade: eu nunca usei One-JAR, não precisei ainda, apenas o marquei para um dia chuvoso.)

Evan
fonte
Não sou programador Java, tem ideia se tenho que embrulhar o próprio aplicativo? Como isso funcionaria em combinação com um carregador de classe existente? Para esclarecer, vou usá-lo do Clojure e quero ser capaz de carregar um JAR como uma biblioteca, em vez de um aplicativo.
Alex B
Ahhh, provavelmente não seria adequado do que. Pense que você terá que distribuir sua (s) biblioteca (s) nativa (s) fora do arquivo jar, com instruções para colocá-las no caminho da biblioteca do aplicativo.
Evan
8

1) Inclua a biblioteca nativa em seu JAR como um recurso. Por exemplo. com Maven ou Gradle, e o layout de projeto padrão, coloque a biblioteca nativa no main/resourcesdiretório.

2) Em algum lugar em inicializadores estáticos de classes Java, relacionados a esta biblioteca, coloque o código da seguinte forma:

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
fonte
Essa solução também funciona caso você queira implantar algo em um servidor de aplicativos como o wildfly E a melhor parte é que ele é capaz de descarregar o aplicativo corretamente!
Hash
Esta é a melhor solução para evitar a exceção 'biblioteca nativa já' carregada.
Krithika Vittal
5

JarClassLoader é um carregador de classes para carregar classes, bibliotecas nativas e recursos de um único monstro JAR e de JARs dentro do monstro JAR.

tekumara
fonte
1

Você provavelmente terá que unjar a biblioteca nativa para o sistema de arquivos local. Pelo que eu sei, o trecho de código que faz o carregamento nativo examina o sistema de arquivos.

Este código deve ajudar você a começar (não o vejo há algum tempo e é para um propósito diferente, mas deve servir, e estou muito ocupado no momento, mas se você tiver dúvidas, deixe um comentário e responderei assim que puder).

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
fonte
1
Se você precisar descompactar algum recurso específico, recomendo dar uma olhada em github.com/zeroturnaround/zt-zip project
Neeme Praks 01 de
zt-zip parece uma API decente.
TofuBeer