¿Cómo cargar archivos JAR dinámicamente en tiempo de ejecución?

308

¿Por qué es tan difícil hacer esto en Java? Si desea tener algún tipo de sistema de módulos, debe poder cargar archivos JAR dinámicamente. Me han dicho que hay una manera de hacerlo escribiendo la suya ClassLoader, pero eso es mucho trabajo para algo que (al menos en mi opinión) debería ser tan fácil como llamar a un método con un archivo JAR como argumento.

¿Alguna sugerencia para el código simple que hace esto?

Allain Lalonde
fuente
44
Quiero hacer lo mismo, pero ejecutar el jar cargado en un entorno más aislado (por razones de seguridad, obviamente). Por ejemplo, quiero bloquear todo el acceso a la red y al sistema de archivos.
Jus12

Respuestas:

254

La razón por la que es difícil es la seguridad. Los cargadores de clases están destinados a ser inmutables; no deberías poder agregarle clases en tiempo de ejecución. De hecho, estoy muy sorprendido de que funcione con el cargador de clases del sistema. Así es como lo haces haciendo tu propio cargador de clases hijo:

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, pero ahí está.

jodonnell
fuente
16
El único problema con este enfoque es que necesita saber qué clases hay en qué frascos. En lugar de simplemente cargar un directorio de jarras y luego crear instancias de clases. ¿Lo estoy entendiendo mal?
Allain Lalonde
10
Este método funciona muy bien cuando se ejecuta en mi IDE, pero cuando construyo mi JAR obtengo una ClassNotFoundException al llamar a Class.forName ().
darrickc
29
Con este enfoque, debe asegurarse de no llamar a este método de carga más de una vez para cada clase. Como está creando un nuevo cargador de clases para cada operación de carga, no puede saber si la clase ya se cargó anteriormente. Esto puede tener malas consecuencias. Por ejemplo, los singleton no funcionan porque la clase se cargó varias veces y, por lo tanto, los campos estáticos existen varias veces.
Eduard Wirch
8
Trabajos. Incluso con dependencias de otras clases dentro del jar. La primera línea estaba incompleta. Solía URLClassLoader child = new URLClassLoader (new URL[] {new URL("file://./my.jar")}, Main.class.getClassLoader());suponer que se llama al archivo jar my.jary se encuentra en el mismo directorio.
mandíbula
44
No se olvide de URL url = file.toURI (). ToURL ();
johnstosh
139

La siguiente solución es agresiva, ya que utiliza la reflexión para evitar la encapsulación, pero funciona a la perfección:

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
fuente
40
Toda la actividad en esta respuesta me hace preguntarme cuántos hacks estamos ejecutando en producción en diferentes sistemas. No estoy seguro de querer saber la respuesta
Andrei Savu
66
No funciona tan bien si el cargador de clases del sistema no es un URLClassLoader ...
Gus
66
Java 9+ advierte que URLClassLoader.class.getDeclaredMethod("addURL", URL.class)es un uso ilegal de la reflexión y fallará en el futuro.
Charlweed
1
¿Alguna idea de cómo actualizar este código para que funcione con Java 9+?
FiReTiTi
1
@FiReTiTi ¡ !
Mordechai
51

Debe echar un vistazo a OSGi , por ejemplo, implementado en la plataforma Eclipse . Hace exactamente eso. Puede instalar, desinstalar, iniciar y detener los llamados paquetes, que son efectivamente archivos JAR. Pero hace un poco más, ya que ofrece, por ejemplo, servicios que se pueden descubrir dinámicamente en archivos JAR en tiempo de ejecución.

O vea la especificación para el Sistema de Módulo Java .

Martin Klinke
fuente
41

¿Qué tal el marco del cargador de clases JCL ? Tengo que admitir que no lo he usado, pero parece prometedor.

Ejemplo 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
fuente
99
También tiene errores y falta algunas implementaciones importantes, es decir, findResources (...). Prepárese para pasar noches maravillosas investigando por qué ciertas cosas no funcionan =)
Sergey Karpushin
Todavía me pregunto las afirmaciones de @ SergeyKarpushin todavía están presentes ya que el proyecto se ha actualizado con el tiempo a la segunda versión principal. Me gustaría escuchar experiencia.
Erdin Eray
2
@ErdinEray, es una muy buena pregunta que me hago también, ya que nos "obligaron" a cambiar a OpenJDK. Todavía trabajo en proyectos de Java, y no tengo ninguna evidencia de que Open JDK te falle en estos días (aunque tuve un problema en ese momento). Supongo que retiro mi reclamo hasta que me encuentre con algo más.
Sergey Karpushin
20

Aquí hay una versión que no está en desuso. Modifiqué el original para eliminar la funcionalidad 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();
    }
}
Jonathan Nadeau
fuente
19
Odio tropezar con un hilo antiguo, pero me gustaría señalar que todo el contenido en stackoverflow tiene licencia CC. Su declaración de derechos de autor es efectivamente ineficaz. stackoverflow.com/faq#editing
Huckle
43
Um. Técnicamente, el contenido original tiene licencia CC, pero si publica contenido con derechos de autor aquí, no elimina el hecho de que el contenido tiene derechos de autor. Si publico una foto de Mickey Mouse, no tiene licencia CC. Entonces estoy agregando la declaración de derechos de autor.
Jason S
19

Si bien la mayoría de las soluciones enumeradas aquí son hacks (anteriores a JDK 9) difíciles de configurar (agentes) o simplemente ya no funcionan (después de JDK 9), encuentro realmente impactante que nadie mencione un método claramente documentado .

Puede crear un cargador de clases de sistema personalizado y luego puede hacer lo que desee. No se requiere reflexión y todas las clases comparten el mismo cargador de clases.

Al iniciar la JVM, agregue esta bandera:

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

El cargador de clases debe tener un constructor que acepte un cargador de clases, que debe establecerse como padre. Se llamará al constructor en el inicio de JVM y se pasará el cargador de clases del sistema real, el cargador personalizado cargará la clase principal.

Para agregar frascos simplemente llame ClassLoader.getSystemClassLoader()y eche a su clase.

Echa un vistazo a esta implementación para un cargador de clases cuidadosamente diseñado. Tenga en cuenta que puede cambiar el add()método a público.

Mordechai
fuente
Gracias, ¡esto es realmente útil! Todas las demás referencias sobre métodos de uso web para JDK 8 o anteriores, lo que tiene múltiples problemas.
Vishal Biyani
15

Con Java 9 , las respuestas con URLClassLoaderahora dan un error como:

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

Esto se debe a que los cargadores de clases utilizados han cambiado. En cambio, para agregar al cargador de clases del sistema, puede usar la API de instrumentación a través de un agente.

Crear una clase 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));
    }
}

Agregue META-INF / MANIFEST.MF y póngalo en un archivo JAR con la clase de agente:

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

Ejecute el agente:

Utiliza la biblioteca byte-buddy-agent para agregar el agente a la JVM en ejecución:

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
fuente
9

Lo mejor que he encontrado es org.apache.xbean.classloader.JarFileClassLoader, que forma parte del proyecto XBean .

Aquí hay un método breve que he usado en el pasado, para crear un cargador de clases a partir de todos los archivos lib en un directorio 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());
}

Luego, para usar el cargador de clases, simplemente haga:

classLoader.loadClass(name);
Zeusoflightning125
fuente
Tenga en cuenta que el proyecto no parece estar muy bien mantenido. Su hoja de ruta para el futuro contiene varios lanzamientos para 2014, por ejemplo.
Zero3
6

Si está trabajando en Android, funciona el siguiente código:

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

Aquí hay una solución rápida para el método de Allain para que sea compatible con las versiones más recientes 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);
}

Tenga en cuenta que se basa en el conocimiento de la implementación interna de JVM específica, por lo que no es ideal y no es una solución universal. Pero es una solución rápida y fácil si sabe que va a utilizar OpenJDK u Oracle JVM estándar. También podría romperse en algún momento en el futuro cuando se lance la nueva versión de JVM, por lo que debe tenerlo en cuenta.

Anton Tananaev
fuente
Con Java 11.0.2, obtengo: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
Funciona con Java 8 EE en el entorno del servidor de aplicaciones.
Jan
4

La solución propuesta por jodonnell es buena pero debería mejorarse un poco. Usé esta publicación para desarrollar mi aplicación con éxito.

Asignar el hilo actual

Primero tenemos que agregar

Thread.currentThread().setContextClassLoader(classLoader);

o no podrá cargar recursos (como spring / context.xml) almacenados en el jar.

No incluye

sus jarras en el cargador de clases principal o no podrá comprender quién está cargando qué.

vea también Problema al recargar un jar usando URLClassLoader

Sin embargo, el marco OSGi sigue siendo la mejor manera.

venergiac
fuente
2
Su respuesta parece un poco confusa, y quizás sea más adecuada como comentario a la respuesta de jodonnell si es simplemente una mejora simple.
Zero3
4

Otra versión de la solución pirata de Allain, que también funciona en 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});

En JDK 11 da algunas advertencias de desaprobación, pero sirve como una solución temporal para aquellos que usan la solución Allain en JDK 11.

czdepski
fuente
¿Puedo también quitar el frasco?
user7294900
3

Otra solución de trabajo usando Instrumentación que funciona para mí. Tiene la ventaja de modificar la búsqueda del cargador de clases, evitando problemas en la visibilidad de las clases dependientes:

Crear una clase de agente

Para este ejemplo, tiene que estar en el mismo jar invocado por la línea 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);
      }
   }
}

Modificar el MANIFEST.MF

Agregar la referencia al agente:

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

De hecho, uso Netbeans, por lo que esta publicación me ayuda a cambiar el manifiesto.

Corriendo

El Launcher-Agent-Classsolo es compatible con JDK 9+ y es responsable de cargar el agente sin definirlo explícitamente en la línea de comando:

 java -jar <your jar>

La forma en que funciona en JDK 6+ es definir el -javaagentargumento:

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

Agregar nuevo Jar en tiempo de ejecución

Luego puede agregar jar según sea necesario con el siguiente comando:

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

No encontré ningún problema al usar esto en la documentación.

czdepski
fuente
Por alguna razón, cuando uso esta solución, obtengo "Exception in thread" main "java.lang.ClassNotFoundException: agent.Agent".
Empaqueté la
3

En caso de que alguien busque esto en el futuro, de esta manera me funciona con OpenJDK 13.0.2.

Tengo muchas clases que necesito instanciar dinámicamente en tiempo de ejecución, cada una potencialmente con un classpath diferente.

En este código, ya tengo un objeto llamado paquete, que contiene algunos metadatos sobre la clase que estoy tratando de cargar. El método getObjectFile () devuelve la ubicación del archivo de clase para la clase. El método getObjectRootPath () devuelve la ruta al directorio bin / que contiene los archivos de clase que incluyen la clase que estoy tratando de instanciar. El método getLibPath () devuelve la ruta a un directorio que contiene los archivos jar que constituyen la ruta de clase para el módulo del que forma parte la clase.

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;

Estaba usando la dependencia Maven: org.xeustechnologies: jcl-core: 2.8 para hacer esto antes, pero después de pasar JDK 1.8, a veces se congeló y nunca regresó atascado "esperando referencias" en Reference :: waitForReferencePendingList ().

También mantengo un mapa de cargadores de clases para que puedan reutilizarse si la clase que estoy tratando de instanciar está en el mismo módulo que una clase que ya he instanciado, lo cual recomendaría.

ZGorlock
fuente
2

Por favor, eche un vistazo a este proyecto que comencé: proxy-object lib

Esta biblioteca cargará jar desde el sistema de archivos o cualquier otra ubicación. Dedicará un cargador de clases para el jar para asegurarse de que no haya conflictos de biblioteca. Los usuarios podrán crear cualquier objeto desde el jar cargado y llamar a cualquier método en él. Esta lib fue diseñada para cargar frascos compilados en Java 8 desde la base de código que admite Java 7.

Para crear un 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();

ObjectBuilder admite métodos de fábrica, llamadas a funciones estáticas e implementaciones de interfaz de devolución de llamada. Publicaré más ejemplos en la página Léame.

Aleksey
fuente
2

Esto puede ser una respuesta tardía, puedo hacerlo así (un ejemplo simple para fastutil-8.2.2.jar) usando la clase 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

De acuerdo con la documentación, este archivo se descargará dentro de "lib / user" y luego se cargará dinámicamente, por lo que puede comenzar a usar inmediatamente las clases de este archivo jar en el mismo programa.

steve212
fuente
1

Necesitaba cargar un archivo jar en tiempo de ejecución para java 8 y java 9+ (los comentarios anteriores no funcionan para ambas versiones). Aquí está el método para hacerlo (usando Spring Boot 1.5.2 si puede relacionarse).

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
fuente
-2

Personalmente encuentro que java.util.ServiceLoader hace el trabajo bastante bien. Puedes obtener un ejemplo aquí .

tanyehzheng
fuente
11
ServiceLoader no agrega archivos jar dinámicamente en tiempo de ejecución. los archivos jar deben estar en classpath previamente.
angelcervera