Come caricare dinamicamente i file JAR in Runtime?


308

Perché è così difficile farlo in Java? Se si desidera disporre di qualsiasi tipo di sistema di moduli, è necessario essere in grado di caricare i file JAR in modo dinamico. Mi è stato detto che esiste un modo per farlo scrivendo il tuo ClassLoader, ma è un sacco di lavoro per qualcosa che dovrebbe (almeno nella mia mente) essere facile come chiamare un metodo con un file JAR come argomento.

Qualche suggerimento per un semplice codice che fa questo?


4
Voglio fare lo stesso, ma eseguire il jar caricato in un ambiente più sandbox (per motivi di sicurezza ovviamente). Ad esempio, voglio bloccare tutti gli accessi alla rete e al filesystem.
Jus12,

Risposte:


254

Il motivo per cui è difficile è la sicurezza. I caricatori di classi devono essere immutabili; non dovresti essere in grado di aggiungere classi volenti o nolenti in fase di esecuzione. In realtà sono molto sorpreso che funzioni con il classloader di sistema. Ecco come lo fai creando il tuo classloader figlio:

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, ma eccolo qui.


16
L'unico problema con questo approccio è che devi sapere quali sono le classi in quali vasetti. Invece di caricare una directory di vasetti e quindi istanziare le classi. Lo sto fraintendendo?
Allain Lalonde,

10
Questo metodo funziona benissimo quando si esegue il mio IDE, ma quando creo il mio JAR ottengo un ClassNotFoundException quando chiamo Class.forName ().
darrickc,

29
Usando questo approccio devi assicurarti di non chiamare questo metodo di caricamento più di una volta per ogni classe. Poiché stai creando un nuovo programma di caricamento classi per ogni operazione di caricamento, non può sapere se la classe è già stata caricata in precedenza. Ciò può avere conseguenze negative. Ad esempio i singoli non funzionano perché la classe è stata caricata più volte e quindi i campi statici esistono più volte.
Eduard Wirch,

8
Lavori. Anche con dipendenze da altre classi all'interno del vaso. La prima riga era incompleta. Ho usato URLClassLoader child = new URLClassLoader (new URL[] {new URL("file://./my.jar")}, Main.class.getClassLoader());supponendo che il file jar sia chiamato my.jare si trova nella stessa directory.
mascella

4
Non dimenticare di url url = file.toURI (). ToURL ();
johnstosh,

139

La seguente soluzione è hacker, in quanto utilizza la riflessione per bypassare l'incapsulamento, ma funziona perfettamente:

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

40
Tutta l'attività su questa risposta mi fa pensare a quanti hack stiamo eseguendo in produzione in diversi sistemi. Non sono sicuro di voler conoscere la risposta
Andrei Savu,

6
Non funziona così bene se il caricatore della classe di sistema è qualcosa di diverso da un URLClassLoader ...
Gus

6
Java 9+ avverte che URLClassLoader.class.getDeclaredMethod("addURL", URL.class)è un uso illegale della riflessione e fallirà in futuro.
Charlweed,

1
Qualche idea su come aggiornare questo codice per funzionare con Java 9+?
FiReTiTi

1
@FiReTiTi !!
Mordechai,

51

Dovresti dare un'occhiata a OSGi , ad esempio implementato nella piattaforma Eclipse . Lo fa esattamente. È possibile installare, disinstallare, avviare e arrestare i cosiddetti bundle, che sono effettivamente file JAR. Ma fa un po 'di più, poiché offre ad esempio servizi che possono essere scoperti dinamicamente nei file JAR in fase di esecuzione.

Oppure vedi le specifiche per Java Module System .


41

Che ne dici del framework del caricatore di classe JCL ? Devo ammetterlo, non l'ho usato, ma sembra promettente.

Esempio di utilizzo:

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

9
È anche difettoso e manca alcune importanti implementazioni, ad esempio findResources (...). Preparati a trascorrere notti meravigliose indagando sul perché certe cose non funzionano =)
Sergey Karpushin,

Mi chiedo ancora che le affermazioni di @ SergeyKarpushin siano ancora presenti poiché il progetto è stato aggiornato nel tempo alla seconda versione principale. Vorrei sentire l'esperienza.
Erdin Eray,

2
@ErdinEray, è un'ottima domanda che mi pongo anch'io poiché siamo stati "costretti" a passare a OpenJDK. Lavoro ancora su progetti Java e non ho alcuna prova che Open JDK ti mancherà in questi giorni (ho avuto problemi allora). Immagino di ritirare la mia richiesta fino a quando non mi imbatto in qualcos'altro.
Sergey Karpushin,

20

Ecco una versione che non è obsoleta. Ho modificato l'originale per rimuovere la funzionalità obsoleta.

/**************************************************************************************************
 * 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();
    }
}

19
Odio imbattermi in un vecchio thread, ma vorrei sottolineare che tutto il contenuto su StackOverflow è concesso in licenza CC. La tua dichiarazione sul copyright è effettivamente inefficace. stackoverflow.com/faq#editing
Huckle

43
Um. Tecnicamente, il contenuto originale è concesso in licenza CC, ma se pubblichi contenuti protetti da copyright qui, non rimuove il fatto che il contenuto sia protetto da copyright. Se invio una foto di Topolino, non viene rilasciato con licenza CC. Quindi sto aggiungendo nuovamente la dichiarazione sul copyright.
Jason S,

19

Mentre la maggior parte delle soluzioni elencate qui sono hack (pre JDK 9) difficili da configurare (agenti) o semplicemente non funzionano più (post JDK 9) trovo davvero scioccante che nessuno abbia menzionato un metodo chiaramente documentato .

È possibile creare un caricatore di classi di sistema personalizzato e quindi si è liberi di fare tutto ciò che si desidera. Nessuna riflessione richiesta e tutte le classi condividono lo stesso programma di caricamento classi.

All'avvio di JVM aggiungi questo flag:

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

Il classloader deve avere un costruttore che accetta un classloader, che deve essere impostato come suo genitore. Il costruttore verrà chiamato all'avvio di JVM e verrà passato il classloader di sistema reale, la classe principale verrà caricata dal caricatore personalizzato.

Per aggiungere vasi basta chiamare ClassLoader.getSystemClassLoader()e lanciarlo nella tua classe.

Dai un'occhiata a questa implementazione per un classloader accuratamente realizzato. Si noti che è possibile modificare il add()metodo in pubblico.


Grazie - questo è davvero utile! Tutti gli altri riferimenti sul web utilizzano metodi per JDK 8 o precedenti, che presentano numerosi problemi.
Vishal Biyani,

15

Con Java 9 , le risposte con URLClassLoaderora forniscono un errore del tipo:

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

Questo perché i caricatori di classi utilizzati sono cambiati. Invece, per aggiungere al caricatore di classi di sistema, è possibile utilizzare l' API di strumentazione tramite un agente.

Crea una classe 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));
    }
}

Aggiungi META-INF / MANIFEST.MF e inseriscilo in un file JAR con la classe agent:

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

Esegui l'agente:

Questo utilizza la libreria byte-buddy-agent per aggiungere l'agent alla JVM in esecuzione:

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

9

Il migliore che ho trovato è org.apache.xbean.classloader.JarFileClassLoader che fa parte del progetto XBean .

Ecco un metodo breve che ho usato in passato per creare un caricatore di classi da tutti i file lib in una directory specifica

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

Quindi per utilizzare il classloader, basta fare:

classLoader.loadClass(name);

Si noti che il progetto non sembra essere molto ben mantenuto. La loro tabella di marcia per il futuro contiene diverse versioni per il 2014, ad esempio.
Zero3,

6

Se stai lavorando su Android, il seguente codice funziona:

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

6

Ecco una rapida soluzione per il metodo di Allain per renderlo compatibile con le versioni più recenti di 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);
}

Si noti che si basa sulla conoscenza dell'implementazione interna di JVM specifica, quindi non è l'ideale e non è una soluzione universale. Ma è una soluzione rapida e semplice se sai che stai per utilizzare OpenJDK standard o Oracle JVM. Potrebbe anche rompersi ad un certo punto in futuro quando verrà rilasciata la nuova versione di JVM, quindi è necessario tenerlo a mente.


Con Java 11.0.2 ottengo: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 il

Funziona con Java 8 EE in ambiente server applicazioni.
Jan

4

La soluzione proposta da jodonnell è buona ma dovrebbe essere leggermente migliorata. Ho usato questo post per sviluppare la mia applicazione con successo.

Assegna il thread corrente

Innanzitutto dobbiamo aggiungere

Thread.currentThread().setContextClassLoader(classLoader);

altrimenti non sarai in grado di caricare risorse (come spring / context.xml) archiviate nel vaso.

Non comprendono

i tuoi vasetti nel caricatore della classe genitore o non sarai in grado di capire chi sta caricando cosa.

vedi anche Problema nel ricaricare un vaso usando URLClassLoader

Tuttavia, il framework OSGi rimane il modo migliore.


2
La tua risposta sembra un po 'confusa ed è forse più adatta come commento alla risposta di jodonnell se si tratta semplicemente di un semplice miglioramento.
Zero3,

4

Un'altra versione della soluzione hacker di Allain, che funziona anche su 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});

Su JDK 11 fornisce alcuni avvisi di deprecazione ma funge da soluzione temporanea per coloro che utilizzano la soluzione di Allain su JDK 11.


Posso anche rimuovere il barattolo?
user7294900

3

Un'altra soluzione funzionante che utilizza la strumentazione che funziona per me. Ha il vantaggio di modificare la ricerca del caricatore di classi, evitando problemi di visibilità delle classi per le classi dipendenti:

Creare una classe agente

Per questo esempio, deve trovarsi sullo stesso jar invocato dalla riga di 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);
      }
   }
}

Modifica MANIFEST.MF

Aggiunta del riferimento all'agente:

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

In realtà uso Netbeans, quindi questo post aiuta su come cambiare manifest.mf

In esecuzione

È Launcher-Agent-Classsupportato solo su JDK 9+ ed è responsabile del caricamento dell'agente senza definirlo esplicitamente sulla riga di comando:

 java -jar <your jar>

Il modo in cui funziona su JDK 6+ sta definendo l' -javaagentargomento:

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

Aggiunta di un nuovo Jar in fase di esecuzione

È quindi possibile aggiungere jar come necessario utilizzando il seguente comando:

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

Non ho riscontrato alcun problema utilizzando questo sulla documentazione.


Per qualche motivo quando uso questa soluzione ottengo "Eccezione nel thread" principale "java.lang.ClassNotFoundException: agent.Agent". Ho impacchettato la classe "Agente" nella mia principale applicazione di "guerra", quindi sono sicuro che sia lì
Sergei Ledvanov,

3

Nel caso qualcuno lo cerchi in futuro, in questo modo funziona con OpenJDK 13.0.2.

Ho molte classi di cui ho bisogno per istanziare dinamicamente in fase di esecuzione, ognuna potenzialmente con un percorso di classe diverso.

In questo codice, ho già un oggetto chiamato pack, che contiene alcuni metadati sulla classe che sto cercando di caricare. Il metodo getObjectFile () restituisce il percorso del file di classe per la classe. Il metodo getObjectRootPath () restituisce il percorso alla directory bin / contenente i file di classe che includono la classe che sto provando a creare un'istanza. Il metodo getLibPath () restituisce il percorso a una directory contenente i file jar che costituiscono il percorso di classe per il modulo di cui fa parte la classe.

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;

Stavo usando la dipendenza Maven: org.xeustechnologies: jcl-core: 2.8 per farlo prima, ma dopo aver superato JDK 1.8, a volte si bloccava e non tornava mai bloccato "in attesa di riferimenti" in Reference :: waitForReferencePendingList ().

Sto anche tenendo una mappa dei caricatori di classi in modo che possano essere riutilizzati se la classe che sto provando a creare un'istanza si trova nello stesso modulo di una classe che ho già istanziato, che consiglierei.


2

per favore, dai un'occhiata a questo progetto che ho avviato: lib proxy-object

Questa libreria caricherà il jar dal file system o da qualsiasi altra posizione. Dedica un caricatore di classi al jar per assicurarsi che non vi siano conflitti tra librerie. Gli utenti saranno in grado di creare qualsiasi oggetto dal vaso caricato e chiamare qualsiasi metodo su di esso. Questa libreria è stata progettata per caricare barattoli compilati in Java 8 dalla base di codice che supporta Java 7.

Per creare un oggetto:

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

ObjectBuilder supporta i metodi di fabbrica, chiamando le funzioni statiche e richiamando le implementazioni dell'interfaccia. posterò altri esempi sulla pagina del readme.


2

Questa può essere una risposta tardiva, posso farlo così (un semplice esempio per fastutil-8.2.2.jar) usando la 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

Secondo la documentazione, questo file verrà scaricato all'interno di "lib / user" e quindi caricato dinamicamente, in modo da poter iniziare immediatamente a utilizzare le classi da questo file jar nello stesso programma.


1

Avevo bisogno di caricare un file jar in fase di runtime sia per java 8 che per java 9+ (i commenti sopra non funzionano per entrambe queste versioni). Ecco il metodo per farlo (usando Spring Boot 1.5.2, se possibile).

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

-2

Personalmente trovo che java.util.ServiceLoader faccia abbastanza bene il lavoro. Puoi fare un esempio qui .


11
ServiceLoader non aggiunge dinamicamente i file jar in fase di esecuzione. i file jar devono essere precedentemente nel percorso di classe.
Angelcervera,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.