Como carregar arquivos JAR dinamicamente no Runtime?

308

Por que é tão difícil fazer isso em Java? Se você deseja ter qualquer tipo de sistema de módulos, precisa poder carregar arquivos JAR dinamicamente. Disseram-me que há uma maneira de fazê-lo escrevendo o seu próprio ClassLoader, mas isso dá muito trabalho para algo que deve (pelo menos em minha mente) ser tão fácil quanto chamar um método com um arquivo JAR como argumento.

Alguma sugestão para código simples que faz isso?

Allain Lalonde
fonte
4
Quero fazer o mesmo, mas execute o jar carregado em um ambiente mais protegido (por razões de segurança, obviamente). Por exemplo, quero bloquear todo o acesso à rede e ao sistema de arquivos.
Jus12

Respostas:

254

A razão pela qual é difícil é a segurança. Os classloaders devem ser imutáveis; você não deve poder adicionar classes a ela em tempo de execução. Estou realmente muito surpreso que funcione com o carregador de classe do sistema. Aqui está como você faz seu próprio carregador de classes filho:

URLClassLoader child = new URLClassLoader(
        new URL[] {myJar.toURI().toURL()},
        this.getClass().getClassLoader()
);
Class classToLoad = Class.forName("com.MyClass", true, child);
Method method = classToLoad.getDeclaredMethod("myMethod");
Object instance = classToLoad.newInstance();
Object result = method.invoke(instance);

Doloroso, mas aí está.

jodonnell
fonte
16
O único problema dessa abordagem é que você precisa saber quais classes estão em quais jarros. Em vez de apenas carregar um diretório de jars e instanciar classes. Estou entendendo errado?
Allain Lalonde
10
Esse método funciona muito bem quando executado no meu IDE, mas quando construo meu JAR, recebo uma ClassNotFoundException ao chamar Class.forName ().
29412 darrickc
29
Usando essa abordagem, você precisa garantir que não chamará esse método de carregamento mais de uma vez para cada classe. Como você está criando um novo carregador de classes para cada operação de carregamento, não é possível saber se a classe já foi carregada anteriormente. Isso pode ter consequências ruins. Por exemplo, singletons não funcionam porque a classe foi carregada várias vezes e, portanto, os campos estáticos existem várias vezes.
Eduard Wirch
8
Trabalho. Mesmo com dependências para outras classes dentro do jar. A primeira linha estava incompleta. Eu costumava URLClassLoader child = new URLClassLoader (new URL[] {new URL("file://./my.jar")}, Main.class.getClassLoader());assumir que o arquivo jar é chamado my.jare está localizado no mesmo diretório.
mandíbula
4
Não se esqueça do URL url = file.toURI (). ToURL ();
19417 johnstosh
139

A solução a seguir é imprudente, pois usa a reflexão para ignorar o encapsulamento, mas funciona perfeitamente:

File file = ...
URL url = file.toURI().toURL();

URLClassLoader classLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(classLoader, url);
Allain Lalonde
fonte
40
Toda a atividade nessa resposta me faz pensar em quanto hacks estamos executando na produção em diferentes sistemas. Não tenho certeza se quero saber a resposta #
Andrei Savu 26/03
6
Não funciona tão bem se o carregador de classe do sistema passa a ser algo diferente de um URLClassLoader ...
Gus
6
O Java 9+ adverte que URLClassLoader.class.getDeclaredMethod("addURL", URL.class)é um uso ilegal de reflexão e falhará no futuro.
precisa saber é o seguinte
1
Alguma idéia de como atualizar esse código para funcionar com o Java 9+?
FiReTiTi 28/03/19
1
@FiReTiTi Yes !!
Mordechai
51

Você deve dar uma olhada no OSGi , por exemplo, implementado na plataforma Eclipse . Faz exatamente isso. É possível instalar, desinstalar, iniciar e parar os chamados pacotes configuráveis, que são efetivamente arquivos JAR. Mas faz um pouco mais, pois oferece, por exemplo, serviços que podem ser descobertos dinamicamente em arquivos JAR em tempo de execução.

Ou consulte a especificação para o Java Module System .

Martin Klinke
fonte
41

E a estrutura do carregador de classes JCL ? Eu tenho que admitir, eu não usei, mas parece promissor.

Exemplo de uso:

JarClassLoader jcl = new JarClassLoader();
jcl.add("myjar.jar"); // Load jar file  
jcl.add(new URL("http://myserver.com/myjar.jar")); // Load jar from a URL
jcl.add(new FileInputStream("myotherjar.jar")); // Load jar file from stream
jcl.add("myclassfolder/"); // Load class folder  
jcl.add("myjarlib/"); // Recursively load all jar files in the folder/sub-folder(s)

JclObjectFactory factory = JclObjectFactory.getInstance();
// Create object of loaded class  
Object obj = factory.create(jcl, "mypackage.MyClass");
Chris
fonte
9
Também está com erros e faltam algumas implementações importantes, ou seja, findResources (...). Esteja pronto para passar noites maravilhosas investigar por que certas coisas não funcionam =)
Sergey Karpushin
Ainda estou me perguntando as reivindicações de @ SergeyKarpushin ainda estão presentes desde que o projeto foi atualizado ao longo do tempo para a segunda versão principal. Gostaria de ouvir a experiência.
Erdin Eray
2
@ErdinEray, é uma pergunta muito boa que eu também me pergunto, já que fomos "forçados" a mudar para o OpenJDK. Ainda trabalho em projetos java e não tenho nenhuma evidência de que o Open JDK falhará com você hoje em dia (eu tive um problema naquela época). Acho que retiro minha reivindicação até encontrar outra coisa.
Sergey Karpushin 12/02/19
20

Aqui está uma versão que não foi preterida. Modifiquei o original para remover a funcionalidade descontinuada.

/**************************************************************************************************
 * Copyright (c) 2004, Federal University of So Carlos                                           *
 *                                                                                                *
 * All rights reserved.                                                                           *
 *                                                                                                *
 * Redistribution and use in source and binary forms, with or without modification, are permitted *
 * provided that the following conditions are met:                                                *
 *                                                                                                *
 *     * Redistributions of source code must retain the above copyright notice, this list of      *
 *       conditions and the following disclaimer.                                                 *
 *     * Redistributions in binary form must reproduce the above copyright notice, this list of   *
 *     * conditions and the following disclaimer in the documentation and/or other materials      *
 *     * provided with the distribution.                                                          *
 *     * Neither the name of the Federal University of So Carlos nor the names of its            *
 *     * contributors may be used to endorse or promote products derived from this software       *
 *     * without specific prior written permission.                                               *
 *                                                                                                *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS                            *
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT                              *
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR                          *
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR                  *
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,                          *
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,                            *
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR                             *
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF                         *
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING                           *
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS                             *
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.                                   *
 **************************************************************************************************/
/*
 * Created on Oct 6, 2004
 */
package tools;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * Useful class for dynamically changing the classpath, adding classes during runtime. 
 */
public class ClasspathHacker {
    /**
     * Parameters of the method to add an URL to the System classes. 
     */
    private static final Class<?>[] parameters = new Class[]{URL.class};

    /**
     * Adds a file to the classpath.
     * @param s a String pointing to the file
     * @throws IOException
     */
    public static void addFile(String s) throws IOException {
        File f = new File(s);
        addFile(f);
    }

    /**
     * Adds a file to the classpath
     * @param f the file to be added
     * @throws IOException
     */
    public static void addFile(File f) throws IOException {
        addURL(f.toURI().toURL());
    }

    /**
     * Adds the content pointed by the URL to the classpath.
     * @param u the URL pointing to the content to be added
     * @throws IOException
     */
    public static void addURL(URL u) throws IOException {
        URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
        Class<?> sysclass = URLClassLoader.class;
        try {
            Method method = sysclass.getDeclaredMethod("addURL",parameters);
            method.setAccessible(true);
            method.invoke(sysloader,new Object[]{ u }); 
        } catch (Throwable t) {
            t.printStackTrace();
            throw new IOException("Error, could not add URL to system classloader");
        }        
    }

    public static void main(String args[]) throws IOException, SecurityException, ClassNotFoundException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException{
        addFile("C:\\dynamicloading.jar");
        Constructor<?> cs = ClassLoader.getSystemClassLoader().loadClass("test.DymamicLoadingTest").getConstructor(String.class);
        DymamicLoadingTest instance = (DymamicLoadingTest)cs.newInstance();
        instance.test();
    }
}
Jonathan Nadeau
fonte
19
Eu odeio esbarrar em um encadeamento antigo, mas gostaria de salientar que todo o conteúdo no stackoverflow é licenciado por CC. Sua declaração de direitos autorais é efetivamente ineficaz. stackoverflow.com/faq#editing
Huckle
43
Hum. Tecnicamente, o conteúdo original é licenciado por CC, mas se você postar conteúdo protegido por direitos autorais aqui, isso não removerá o fato de que o conteúdo é protegido por direitos autorais. Se eu postar uma foto do Mickey Mouse, ela não será licenciada por CC. Então, estou adicionando a declaração de direitos autorais de volta.
Jason S
19

Enquanto a maioria das soluções listadas aqui são hacks (pré-JDK 9) difíceis de configurar (agentes) ou simplesmente não funcionam mais (pós-JDK 9), acho realmente chocante que ninguém tenha mencionado um método claramente documentado .

Você pode criar um carregador de classes de sistema personalizado e, em seguida, pode fazer o que quiser. Nenhuma reflexão é necessária e todas as classes compartilham o mesmo carregador de classes.

Ao iniciar a JVM, adicione este sinalizador:

java -Djava.system.class.loader=com.example.MyCustomClassLoader

O carregador de classes deve ter um construtor aceitando um carregador de classes, que deve ser definido como seu pai. O construtor será chamado na inicialização da JVM e o carregador de classes real do sistema será passado, a classe principal será carregada pelo carregador customizado.

Para adicionar jarros, basta ligar ClassLoader.getSystemClassLoader()e transmitir à sua turma.

Confira esta implementação para obter um carregador de classe cuidadosamente criado. Observe que você pode alterar o add()método para público.

Mordechai
fonte
Obrigado - isso é realmente útil! Todas as outras referências na Web usam métodos para o JDK 8 ou anterior - o que tem vários problemas.
Vishal Biyani
15

Com o Java 9 , as respostas com URLClassLoaderagora dão um erro como:

java.lang.ClassCastException: java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to java.base/java.net.URLClassLoader

Isso ocorre porque os carregadores de classes usados ​​foram alterados. Em vez disso, para adicionar ao carregador de classes do sistema, você pode usar a API de Instrumentação por meio de um agente.

Crie uma classe de agente:

package ClassPathAgent;

import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;

public class ClassPathAgent {
    public static void agentmain(String args, Instrumentation instrumentation) throws IOException {
        instrumentation.appendToSystemClassLoaderSearch(new JarFile(args));
    }
}

Inclua META-INF / MANIFEST.MF e coloque-o em um arquivo JAR com a classe do agente:

Manifest-Version: 1.0
Agent-Class: ClassPathAgent.ClassPathAgent

Execute o agente:

Isso usa a biblioteca byte-buddy-agent para incluir o agente na JVM em execução:

import java.io.File;

import net.bytebuddy.agent.ByteBuddyAgent;

public class ClassPathUtil {
    private static File AGENT_JAR = new File("/path/to/agent.jar");

    public static void addJarToClassPath(File jarFile) {
        ByteBuddyAgent.attach(AGENT_JAR, String.valueOf(ProcessHandle.current().pid()), jarFile.getPath());
    }
}
fgb
fonte
9

O melhor que encontrei é org.apache.xbean.classloader.JarFileClassLoader, que faz parte do projeto XBean .

Aqui está um método curto que eu usei no passado, para criar um carregador de classes a partir de todos os arquivos lib em um diretório específico

public void initialize(String libDir) throws Exception {
    File dependencyDirectory = new File(libDir);
    File[] files = dependencyDirectory.listFiles();
    ArrayList<URL> urls = new ArrayList<URL>();
    for (int i = 0; i < files.length; i++) {
        if (files[i].getName().endsWith(".jar")) {
        urls.add(files[i].toURL());
        //urls.add(files[i].toURI().toURL());
        }
    }
    classLoader = new JarFileClassLoader("Scheduler CL" + System.currentTimeMillis(), 
        urls.toArray(new URL[urls.size()]), 
        GFClassLoader.class.getClassLoader());
}

Em seguida, para usar o carregador de classe, basta:

classLoader.loadClass(name);
Zeusoflightning125
fonte
Observe que o projeto não parece estar muito bem mantido. O roteiro para o futuro contém vários lançamentos para 2014, por exemplo.
precisa saber é
6

Se você estiver trabalhando no Android, o seguinte código funcionará:

String jarFile = "path/to/jarfile.jar";
DexClassLoader classLoader = new DexClassLoader(jarFile, "/data/data/" + context.getPackageName() + "/", null, getClass().getClassLoader());
Class<?> myClass = classLoader.loadClass("MyClass");
Caner
fonte
6

Aqui está uma solução rápida para o método de Allain para torná-lo compatível com as versões mais recentes do Java:

ClassLoader classLoader = ClassLoader.getSystemClassLoader();
try {
    Method method = classLoader.getClass().getDeclaredMethod("addURL", URL.class);
    method.setAccessible(true);
    method.invoke(classLoader, new File(jarPath).toURI().toURL());
} catch (NoSuchMethodException e) {
    Method method = classLoader.getClass()
            .getDeclaredMethod("appendToClassPathForInstrumentation", String.class);
    method.setAccessible(true);
    method.invoke(classLoader, jarPath);
}

Observe que ele se baseia no conhecimento da implementação interna de JVM específica, portanto, não é o ideal e não é uma solução universal. Mas é uma solução rápida e fácil se você souber que usará o OpenJDK padrão ou o Oracle JVM. Também pode ser interrompido em algum momento no futuro quando a nova versão da JVM for lançada, portanto, é preciso ter isso em mente.

Anton Tananaev
fonte
Com o Java 11.0.2, recebo:Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make void jdk.internal.loader.ClassLoaders$AppClassLoader.appendToClassPathForInstrumentation(java.lang.String) accessible: module java.base does not "opens jdk.internal.loader" to unnamed module @18ef96
Richard Żak 27/03/19
Funciona com Java 8 EE no ambiente do servidor de aplicativos.
Janeiro
4

A solução proposta por jodonnell é boa, mas deve ser um pouco aprimorada. Eu usei este post para desenvolver meu aplicativo com sucesso.

Atribuir o segmento atual

Em primeiro lugar, temos que adicionar

Thread.currentThread().setContextClassLoader(classLoader);

ou você não poderá carregar o recurso (como spring / context.xml) armazenado no jar.

Não inclui

seus jars no carregador de classes pai ou você não conseguirá entender quem está carregando o quê.

consulte também Problema ao recarregar um jar usando URLClassLoader

No entanto, a estrutura OSGi continua sendo a melhor maneira.

venérgico
fonte
2
Sua resposta parece um pouco confusa e talvez seja mais adequada como um comentário à resposta de jodonnell, se for apenas uma simples melhoria.
precisa saber é
4

Outra versão da solução hackish da Allain, que também funciona no JDK 11:

File file = ...
URL url = file.toURI().toURL();
URLClassLoader sysLoader = new URLClassLoader(new URL[0]);

Method sysMethod = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
sysMethod.setAccessible(true);
sysMethod.invoke(sysLoader, new Object[]{url});

No JDK 11, ele fornece alguns avisos de descontinuação, mas serve como uma solução temporária para aqueles que usam a solução Allain no JDK 11.

czdepski
fonte
Também posso remover o jar?
user7294900
3

Outra solução de trabalho usando a Instrumentação que funciona para mim. Tem a vantagem de modificar a pesquisa do carregador de classes, evitando problemas na visibilidade de classes para classes dependentes:

Criar uma classe de agente

Para este exemplo, ele deve estar no mesmo jar chamado pela linha de comando:

package agent;

import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;

public class Agent {
   public static Instrumentation instrumentation;

   public static void premain(String args, Instrumentation instrumentation) {
      Agent.instrumentation = instrumentation;
   }

   public static void agentmain(String args, Instrumentation instrumentation) {
      Agent.instrumentation = instrumentation;
   }

   public static void appendJarFile(JarFile file) throws IOException {
      if (instrumentation != null) {
         instrumentation.appendToSystemClassLoaderSearch(file);
      }
   }
}

Modifique o MANIFEST.MF

Adicionando a referência ao agente:

Launcher-Agent-Class: agent.Agent
Agent-Class: agent.Agent
Premain-Class: agent.Agent

Na verdade, eu uso o Netbeans, portanto, este post ajuda a alterar o manifest.mf

Corrida

Ele Launcher-Agent-Classé suportado apenas no JDK 9+ e é responsável por carregar o agente sem defini-lo explicitamente na linha de comandos:

 java -jar <your jar>

A maneira que funciona no JDK 6+ é definir o -javaagentargumento:

java -javaagent:<your jar> -jar <your jar>

Adicionando novo Jar em tempo de execução

Você pode adicionar jar conforme necessário, usando o seguinte comando:

Agent.appendJarFile(new JarFile(<your file>));

Não encontrei nenhum problema ao usar isso na documentação.

czdepski
fonte
Por alguma razão, ao usar esta solução, recebo "Exception in thread" main "java.lang.ClassNotFoundException: agent.Agent". I embalados classe "Agente" em meu aplicativo principal "guerra", por isso estou certo que ele está lá
Sergei Ledvanov
3

Caso alguém o procure no futuro, isso funcionará para mim com o OpenJDK 13.0.2.

Eu tenho muitas classes que preciso instanciar dinamicamente no tempo de execução, cada uma potencialmente com um caminho de classe diferente.

Nesse código, eu já tenho um objeto chamado pack, que contém alguns metadados sobre a classe que estou tentando carregar. O método getObjectFile () retorna o local do arquivo de classe para a classe. O método getObjectRootPath () retorna o caminho para o diretório bin / que contém os arquivos de classe que incluem a classe que estou tentando instanciar. O método getLibPath () retorna o caminho para um diretório que contém os arquivos jar que constituem o caminho de classe para o módulo do qual a classe faz parte.

File object = new File(pack.getObjectFile()).getAbsoluteFile();
Object packObject;
try {
    URLClassLoader classloader;

    List<URL> classpath = new ArrayList<>();
    classpath.add(new File(pack.getObjectRootPath()).toURI().toURL());
    for (File jar : FileUtils.listFiles(new File(pack.getLibPath()), new String[] {"jar"}, true)) {
        classpath.add(jar.toURI().toURL());
    }
    classloader = new URLClassLoader(classpath.toArray(new URL[] {}));

    Class<?> clazz = classloader.loadClass(object.getName());
    packObject = clazz.getDeclaredConstructor().newInstance();

} catch (Exception e) {
    e.printStackTrace();
    throw e;
}
return packObject;

Eu estava usando a dependência do Maven: org.xeustechnologies: jcl-core: 2.8 para fazer isso antes, mas depois de passar pelo JDK 1.8, às vezes congelava e nunca voltava a ficar "aguardando referências" em Reference :: waitForReferencePendingList ().

Também estou mantendo um mapa dos carregadores de classes para que possam ser reutilizados se a classe que estou tentando instanciar estiver no mesmo módulo que uma classe que eu já instanciei, o que eu recomendaria.

ZGorlock
fonte
2

por favor, dê uma olhada neste projeto que eu iniciei: proxy-lib lib

Essa lib carregará o jar do sistema de arquivos ou de qualquer outro local. Ele dedicará um carregador de classes ao jar para garantir que não haja conflitos de biblioteca. Os usuários poderão criar qualquer objeto do jar carregado e chamar qualquer método nele. Essa lib foi projetada para carregar os jarros compilados no Java 8 a partir da base de código que suporta o Java 7.

Para criar um objeto:

    File libDir = new File("path/to/jar");

    ProxyCallerInterface caller = ObjectBuilder.builder()
            .setClassName("net.proxy.lib.test.LibClass")
            .setArtifact(DirArtifact.builder()
                    .withClazz(ObjectBuilderTest.class)
                    .withVersionInfo(newVersionInfo(libDir))
                    .build())
            .build();
    String version = caller.call("getLibVersion").asString();

O ObjectBuilder suporta métodos de fábrica, chamando funções estáticas e implementações de interface de retorno de chamada. estarei postando mais exemplos na página leia-me.

Aleksey
fonte
2

Isso pode ser uma resposta tardia, eu posso fazer isso da seguinte maneira (um exemplo simples para fastutil-8.2.2.jar) usando a classe jhplot.Web da DataMelt ( http://jwork.org/dmelt )

import jhplot.Web;
Web.load("http://central.maven.org/maven2/it/unimi/dsi/fastutil/8.2.2/fastutil-8.2.2.jar"); // now you can start using this library

De acordo com a documentação, esse arquivo será baixado dentro de "lib / user" e carregado dinamicamente, para que você possa começar a usar imediatamente as classes desse arquivo jar no mesmo programa.

steve212
fonte
1

Eu precisava carregar um arquivo jar em tempo de execução para o Java 8 e o Java 9+ (os comentários acima não funcionam para ambas as versões). Aqui está o método para fazer isso (usando o Spring Boot 1.5.2, se estiver relacionado).

public static synchronized void loadLibrary(java.io.File jar) {
    try {            
        java.net.URL url = jar.toURI().toURL();
        java.lang.reflect.Method method = java.net.URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{java.net.URL.class});
        method.setAccessible(true); /*promote the method to public access*/
        method.invoke(Thread.currentThread().getContextClassLoader(), new Object[]{url});
    } catch (Exception ex) {
        throw new RuntimeException("Cannot load library from jar file '" + jar.getAbsolutePath() + "'. Reason: " + ex.getMessage());
    }
}
Bằng Rikimaru
fonte
-2

Pessoalmente, acho que o java.util.ServiceLoader faz o trabalho muito bem. Você pode obter um exemplo aqui .

tanyehzheng
fonte
11
O ServiceLoader não adiciona arquivos jar dinamicamente em tempo de execução. Os arquivos jar devem estar no caminho de classe anteriormente.
12138 angelcervera