Comment charger dynamiquement des fichiers JAR au Runtime?


308

Pourquoi est-il si difficile de faire cela en Java? Si vous voulez avoir n'importe quel type de système de modules, vous devez pouvoir charger dynamiquement les fichiers JAR. On me dit qu'il existe un moyen de le faire en écrivant le vôtre ClassLoader, mais c'est beaucoup de travail pour quelque chose qui devrait (dans mon esprit au moins) être aussi simple que d'appeler une méthode avec un fichier JAR comme argument.

Des suggestions de code simple pour cela?


4
Je veux faire de même mais exécuter le pot chargé dans un environnement plus bac à sable (pour des raisons de sécurité évidemment). Par exemple, je souhaite bloquer tous les accès au réseau et au système de fichiers.
Jus12

Réponses:


254

La raison pour laquelle c'est difficile est la sécurité. Les chargeurs de classe sont censés être immuables; vous ne devriez pas pouvoir y ajouter des classes au moment de l'exécution. Je suis en fait très surpris que cela fonctionne avec le chargeur de classe système. Voici comment procéder en créant votre propre chargeur de classe enfant:

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

Douloureux, mais ça y est.


16
Le seul problème avec cette approche est que vous devez savoir quelles classes sont dans quels pots. Par opposition au simple chargement d'un répertoire de jars puis à l'instanciation des classes. Je le comprends mal?
Allain Lalonde

10
Cette méthode fonctionne très bien lors de l'exécution dans mon IDE, mais lorsque je crée mon JAR, j'obtiens une exception ClassNotFoundException lors de l'appel de Class.forName ().
darrickc

29
En utilisant cette approche, vous devez vous assurer que vous n'appellerez pas cette méthode de chargement plus d'une fois pour chaque classe. Puisque vous créez un nouveau chargeur de classe pour chaque opération de chargement, il ne peut pas savoir si la classe a déjà été chargée précédemment. Cela peut avoir de graves conséquences. Par exemple, les singletons ne fonctionnent pas car la classe a été chargée plusieurs fois et les champs statiques existent donc plusieurs fois.
Eduard Wirch

8
Travaux. Même avec des dépendances à d'autres classes à l'intérieur du pot. La première ligne était incomplète. J'ai utilisé en URLClassLoader child = new URLClassLoader (new URL[] {new URL("file://./my.jar")}, Main.class.getClassLoader());supposant que le fichier jar est appelé my.jaret se trouve dans le même répertoire.
mâchoire

4
N'oubliez pas d'URL url = file.toURI (). ToURL ();
johnstosh

139

La solution suivante est piratée, car elle utilise la réflexion pour contourner l'encapsulation, mais elle fonctionne parfaitement:

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
Toute l'activité sur cette réponse me fait me demander combien de hacks nous exécutons en production dans différents systèmes. Je ne suis pas sûr de vouloir connaître la réponse
Andrei Savu

6
Ne fonctionne pas si bien si le chargeur de classe système se trouve être autre chose qu'un URLClassLoader ...
Gus

6
Java 9+ prévient qu'il URLClassLoader.class.getDeclaredMethod("addURL", URL.class)s'agit d'une utilisation illégale de la réflexion et qu'elle échouera à l'avenir.
Charlweed

1
Une idée de la façon de mettre à jour ce code pour fonctionner avec Java 9+?
FiReTiTi

1
@FiReTiTi Oui !!
Mordechai

51

Vous devriez jeter un œil à OSGi , par exemple implémenté dans la plate-forme Eclipse . C'est exactement cela. Vous pouvez installer, désinstaller, démarrer et arrêter les soi-disant bundles, qui sont en fait des fichiers JAR. Mais il en fait un peu plus, car il offre par exemple des services qui peuvent être découverts dynamiquement dans les fichiers JAR lors de l'exécution.

Ou consultez les spécifications du système de modules Java .


41

Que diriez-vous de l' infrastructure de chargeur de classe JCL ? Je dois admettre que je ne l'ai pas utilisé, mais cela semble prometteur.

Exemple d'utilisation:

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
Il est également bogué et manque certaines implémentations importantes, comme findResources (...). Soyez prêt à passer de merveilleuses nuits à chercher pourquoi certaines choses ne fonctionnent pas =)
Sergey Karpushin

Je me demande toujours si les affirmations de @ SergeyKarpushin sont toujours présentes depuis que le projet a été mis à jour au fil du temps vers la deuxième version majeure. Voudrais entendre l'expérience.
Erdin Eray

2
@ErdinEray, c'est une très bonne question que je me pose aussi puisque nous avons été "obligés" de passer à OpenJDK. Je travaille toujours sur des projets java, et je n'ai aucune preuve qu'Open JDK vous fera défaut ces jours-ci (j'ai eu un problème à l'époque cependant). Je suppose que je retire ma demande jusqu'à ce que je tombe sur quelque chose d'autre.
Sergey Karpushin

20

Voici une version qui n'est pas déconseillée. J'ai modifié l'original pour supprimer la fonctionnalité obsolète.

/**************************************************************************************************
 * 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
Je déteste bump un vieux thread, mais je voudrais souligner que tout le contenu sur stackoverflow est sous licence CC. Votre déclaration de droits d'auteur est effectivement inefficace. stackoverflow.com/faq#editing
Huckle

43
Hum. Techniquement, le contenu original est sous licence CC, mais si vous publiez du contenu sous copyright ici, cela ne supprime pas le fait que le contenu est sous copyright. Si je poste une photo de Mickey Mouse, cela ne la rend pas sous licence CC. J'ajoute donc à nouveau la déclaration de copyright.
Jason S

19

Bien que la plupart des solutions répertoriées ici soient des hacks (avant JDK 9) difficiles à configurer (agents) ou ne fonctionnent tout simplement plus (post JDK 9), je trouve vraiment choquant que personne n'ait mentionné une méthode clairement documentée .

Vous pouvez créer un chargeur de classe système personnalisé, puis vous êtes libre de faire ce que vous voulez. Aucune réflexion requise et toutes les classes partagent le même chargeur de classe.

Au démarrage de la JVM, ajoutez cet indicateur:

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

Le chargeur de classe doit avoir un constructeur acceptant un chargeur de classe, qui doit être défini comme parent. Le constructeur sera appelé au démarrage de la machine virtuelle Java et le chargeur de classe système réel sera transmis, la classe principale sera chargée par le chargeur personnalisé.

Pour ajouter des pots, appelez-le ClassLoader.getSystemClassLoader()et lancez-le dans votre classe.

Découvrez cette implémentation pour un chargeur de classe soigneusement conçu. Veuillez noter que vous pouvez changer la add()méthode en public.


Merci - c'est vraiment utile! Toutes les autres références sur les méthodes d'utilisation du Web pour JDK 8 ou avant - ce qui pose plusieurs problèmes.
Vishal Biyani

15

Avec Java 9 , les réponses avec URLClassLoaderdonnent maintenant une erreur comme:

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

En effet, les chargeurs de classe utilisés ont changé. Au lieu de cela, pour ajouter au chargeur de classe système, vous pouvez utiliser l' API Instrumentation via un agent.

Créez une classe d'agent:

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

Ajoutez META-INF / MANIFEST.MF et placez-le dans un fichier JAR avec la classe d'agent:

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

Exécutez l'agent:

Celui-ci utilise la bibliothèque byte-buddy-agent pour ajouter l'agent à la JVM en cours d'exécution:

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

Le meilleur que j'ai trouvé est org.apache.xbean.classloader.JarFileClassLoader qui fait partie du projet XBean .

Voici une courte méthode que j'ai utilisée dans le passé, pour créer un chargeur de classe à partir de tous les fichiers lib dans un répertoire spécifique

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

Ensuite, pour utiliser le chargeur de classe, faites simplement:

classLoader.loadClass(name);

A noter que le projet ne semble pas très bien entretenu. Leur feuille de route pour l'avenir contient plusieurs versions pour 2014, par exemple.
Zero3

6

Si vous travaillez sur Android, le code suivant fonctionne:

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

6

Voici une solution rapide pour la méthode d'Allain pour la rendre compatible avec les nouvelles versions de 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);
}

Notez qu'il repose sur la connaissance de l'implémentation interne de JVM spécifique, donc ce n'est pas idéal et ce n'est pas une solution universelle. Mais c'est une solution rapide et facile si vous savez que vous allez utiliser OpenJDK standard ou Oracle JVM. Il se peut également qu'il se brise à un moment donné à l'avenir lors de la sortie de la nouvelle version JVM, vous devez donc garder cela à l'esprit.


Avec Java 11.0.2, j'obtiens: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

Fonctionne avec Java 8 EE dans un environnement de serveur d'applications.
janvier

4

La solution proposée par jodonnell est bonne mais devrait être un peu améliorée. J'ai utilisé ce message pour développer mon application avec succès.

Attribuer le thread actuel

Premièrement, nous devons ajouter

Thread.currentThread().setContextClassLoader(classLoader);

ou vous ne pourrez pas charger les ressources (telles que spring / context.xml) stockées dans le bocal.

N'incluez pas

vos pots dans le chargeur de classe parent ou vous ne pourrez pas comprendre qui charge quoi.

voir aussi Problème de rechargement d'un bocal avec URLClassLoader

Cependant, le cadre OSGi reste le meilleur moyen.


2
Votre réponse semble un peu confuse, et est peut-être plus adaptée en tant que commentaire à la réponse de jodonnell si ce n'est qu'une simple amélioration.
Zero3

4

Une autre version de la solution hackeuse d'Allain, qui fonctionne également sur 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});

Sur JDK 11, il donne des avertissements de dépréciation mais sert de solution temporaire à ceux qui utilisent la solution Allain sur JDK 11.


Puis-je également retirer le pot?
user7294900

3

Une autre solution de travail utilisant Instrumentation qui fonctionne pour moi. Il a l'avantage de modifier la recherche du chargeur de classe, en évitant les problèmes de visibilité des classes pour les classes dépendantes:

Créer une classe d'agent

Pour cet exemple, il doit être sur le même pot appelé par la ligne de commande:

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

Modifiez le MANIFEST.MF

Ajout de la référence à l'agent:

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

J'utilise en fait Netbeans, donc ce message aide à changer le manifest.mf

Fonctionnement

Le Launcher-Agent-Classn'est pris en charge que sur JDK 9+ et est responsable du chargement de l'agent sans le définir explicitement sur la ligne de commande:

 java -jar <your jar>

La façon dont cela fonctionne sur JDK 6+ définit l' -javaagentargument:

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

Ajout d'un nouveau pot au moment de l'exécution

Vous pouvez ensuite ajouter le pot si nécessaire à l'aide de la commande suivante:

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

Je n'ai trouvé aucun problème en utilisant ceci sur la documentation.


Pour une raison quelconque, lorsque j'utilise cette solution, j'obtiens "Exception dans le thread" principal "java.lang.ClassNotFoundException: agent.Agent". J'ai intégré la classe "Agent" dans ma principale application "guerre", donc je suis sûr qu'elle est là
Sergei Ledvanov

3

Si quelqu'un le recherche à l'avenir, cette méthode fonctionne pour moi avec OpenJDK 13.0.2.

J'ai de nombreuses classes dont j'ai besoin pour instancier dynamiquement au moment de l'exécution, chacune potentiellement avec un chemin de classe différent.

Dans ce code, j'ai déjà un objet appelé pack, qui contient des métadonnées sur la classe que j'essaie de charger. La méthode getObjectFile () renvoie l'emplacement du fichier de classe pour la classe. La méthode getObjectRootPath () renvoie le chemin d'accès au répertoire bin / contenant les fichiers de classe qui incluent la classe que j'essaie d'instancier. La méthode getLibPath () renvoie le chemin d'accès à un répertoire contenant les fichiers jar constituant le chemin d'accès aux classes du module dont la classe fait partie.

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;

J'utilisais la dépendance Maven: org.xeustechnologies: jcl-core: 2.8 pour le faire auparavant, mais après avoir dépassé JDK 1.8, il se bloquait parfois et ne retournait jamais être coincé "en attente de références" sur Reference :: waitForReferencePendingList ().

Je garde également une carte des chargeurs de classe afin qu'ils puissent être réutilisés si la classe que j'essaie d'instancier est dans le même module qu'une classe que j'ai déjà instanciée, ce que je recommanderais.


2

jetez un œil à ce projet que j'ai commencé: proxy-object lib

Cette bibliothèque chargera le pot à partir du système de fichiers ou de tout autre emplacement. Il dédiera un chargeur de classe pour le pot afin de s'assurer qu'il n'y a pas de conflits de bibliothèque. Les utilisateurs pourront créer n'importe quel objet à partir du pot chargé et appeler n'importe quelle méthode dessus. Cette bibliothèque a été conçue pour charger des fichiers jar compilés en Java 8 à partir de la base de code qui prend en charge Java 7.

Pour créer un objet:

    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 prend en charge les méthodes d'usine, l'appel de fonctions statiques et les implémentations d'interfaces de rappel. je posterai plus d'exemples sur la page Lisezmoi.


2

Cela peut être une réponse tardive, je peux le faire comme ceci (un exemple simple pour fastutil-8.2.2.jar) en utilisant la classe jhplot.Web de 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

Selon la documentation, ce fichier sera téléchargé dans "lib / user" puis chargé dynamiquement, vous pouvez donc commencer immédiatement à utiliser les classes de ce fichier jar dans le même programme.


1

J'avais besoin de charger un fichier jar lors de l'exécution pour java 8 et java 9+ (les commentaires ci-dessus ne fonctionnent pas pour ces deux versions). Voici la méthode pour le faire (en utilisant Spring Boot 1.5.2 si cela peut se rapporter).

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.