Как я должен загружать Jars динамически во время выполнения?
Почему так сложно сделать это на Java? Если вы хотите иметь какую-либо модульную систему, вы должны иметь возможность загружать банки динамически. Мне сказали, что есть способ сделать это, написав свой собственный ClassLoader
, но это большая работа для чего-то, что должно (по крайней мере, на мой взгляд) быть таким же простым, как вызов метода с файлом jar в качестве аргумента.
Любые предложения для простого кода, который делает это?
15 ответов
Причина, по которой это сложно, это безопасность. Загрузчики классов должны быть неизменными; Вы не должны иметь возможность произвольно добавлять к нему классы во время выполнения. Я на самом деле очень удивлен, что работает с системным загрузчиком классов. Вот как вы делаете свой собственный дочерний загрузчик классов:
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);
Больно, но это так.
Следующее решение является хакерским, поскольку оно использует отражение, чтобы обойти инкапсуляцию, но оно работает безупречно:
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);
Хотя большинство решений, перечисленных здесь, либо являются хаками (до JDK 9), которые сложно настроить (агенты), либо просто больше не работают (публикация JDK 9), я нахожу действительно удивительным, что никто не упомянул четко документированный метод.
Вы можете создать собственный загрузчик системных классов и делать все, что захотите. Отражения не требуется, и все классы используют один и тот же загрузчик классов.
При запуске JVM добавьте этот флаг:
java -Djava.system.class.loader=com.example.MyCustomClassLoader
Загрузчик классов должен иметь конструктор, принимающий загрузчик классов, который должен быть установлен в качестве его родителя. Конструктор будет вызываться при запуске JVM и будет передан реальный системный загрузчик классов, а основной класс будет загружен пользовательским загрузчиком.
Чтобы добавить банки, просто позвоните ClassLoader.getSystemClassLoader()
и передайте его своему классу.
Ознакомьтесь с этой реализацией, чтобы увидеть тщательно созданный загрузчик классов. Обратите внимание, что вы можете изменитьadd()
метод к общедоступному.
Вы должны взглянуть на OSGi, например, реализованный в платформе Eclipse. Это именно так. Вы можете устанавливать, удалять, запускать и останавливать так называемые пакеты, которые фактически являются файлами JAR. Но он делает немного больше, поскольку предлагает, например, сервисы, которые могут быть динамически обнаружены в файлах JAR во время выполнения.
Или посмотрите спецификацию для системы модулей Java.
Как насчет платформы загрузчика классов JCL? Я должен признать, я не использовал это, но это выглядит многообещающим.
Пример использования:
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");
Вот версия, которая не является устаревшей. Я изменил оригинал, чтобы удалить устаревшую функциональность.
/**************************************************************************************************
* 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();
}
}
С Java 9 ответы с URLClassLoader
теперь выдайте ошибку вроде:
java.lang.ClassCastException: java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to java.base/java.net.URLClassLoader
Это потому, что используемые загрузчики классов изменились. Вместо этого, чтобы добавить в системный загрузчик классов, вы можете использовать API инструментария через агента.
Создайте класс агента:
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));
}
}
Добавьте META-INF/MANIFEST.MF и поместите его в файл JAR с классом агента:
Manifest-Version: 1.0
Agent-Class: ClassPathAgent.ClassPathAgent
Запустите агент:
Здесь используется библиотека byte-buddy-agent для добавления агента в работающую JVM:
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());
}
}
Вот быстрый обходной путь для метода Аллена, чтобы сделать его совместимым с более новыми версиями 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);
}
Обратите внимание, что он основан на знании внутренней реализации конкретной JVM, поэтому он не идеален и не является универсальным решением. Но это быстрый и простой обходной путь, если вы знаете, что собираетесь использовать стандартный OpenJDK или Oracle JVM. Это может также сломаться в какой-то момент в будущем, когда выйдет новая версия JVM, так что вам нужно помнить об этом.
Лучшее, что я нашел - это org.apache.xbean.classloader.JarFileClassLoader, который является частью проекта XBean.
Вот краткий метод, который я использовал в прошлом, чтобы создать загрузчик классов из всех файлов lib в определенном каталоге
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());
}
Затем, чтобы использовать загрузчик классов, просто выполните:
classLoader.loadClass(name);
Если вы работаете на Android, работает следующий код:
String jarFile = "path/to/jarfile.jar";
DexClassLoader classLoader = new DexClassLoader(jarFile, "/data/data/" + context.getPackageName() + "/", null, getClass().getClassLoader());
Class<?> myClass = classLoader.loadClass("MyClass");
Другое рабочее решение, использующее Инструментарий, которое работает для меня. Он имеет преимущество в изменении поиска загрузчика классов, избегая проблем с видимостью классов для зависимых классов:
Создать класс агента
В этом примере он должен находиться в том же банке, который вызывается из командной строки:
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);
}
}
}
Изменить MANIFEST.MF
Добавление ссылки на агента:
Launcher-Agent-Class: agent.Agent
Agent-Class: agent.Agent
Premain-Class: agent.Agent
Я на самом деле использую Netbeans, так что этот пост помогает о том, как изменить manifest.mf
Бег
Launcher-Agent-Class
поддерживается только в JDK 9+ и отвечает за загрузку агента без явного определения его в командной строке:
java -jar <your jar>
То, как работает JDK 6+, определяет -javaagent
аргумент:
java -javaagent:<your jar> -jar <your jar>
Добавление нового Jar во время выполнения
Затем вы можете добавить jar по мере необходимости, используя следующую команду:
Agent.appendJarFile(new JarFile(<your file>));
Я не нашел никаких проблем с использованием этого в документации.
Еще одна версия хакерского решения от Allain, которая также работает на 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});
В JDK 11 он дает некоторые предупреждения об устаревании, но служит временным решением для тех, кто использует решение Allain в JDK 11.
Решение, предложенное Джодоннеллом, хорошо, но должно быть немного улучшено. Я использовал этот пост, чтобы успешно разработать свое приложение.
Назначить текущий поток
Во-первых, мы должны добавить
Thread.currentThread().setContextClassLoader(classLoader);
или вы не сможете загрузить ресурс (такой как spring/context.xml), хранящийся в банке.
Не включать
ваши фляги в загрузчик родительского класса или вы не сможете понять, кто что загружает.
см. также Проблема перезагрузки фляги с использованием URLClassLoader
Тем не менее, OSGi Framework остается лучшим способом.
В случае, если кто-то будет искать это в будущем, этот способ работает для меня с OpenJDK 13.0.2.
У меня есть много классов, которые мне нужно динамически создавать во время выполнения, каждый потенциально с другим путем к классам.
В этом коде у меня уже есть объект с именем pack, который содержит некоторые метаданные о классе, который я пытаюсь загрузить. Метод getObjectFile() возвращает расположение файла класса для класса. Метод getObjectRootPath() возвращает путь к каталогу bin/, содержащему файлы классов, которые включают класс, который я пытаюсь создать. Метод getLibPath() возвращает путь к каталогу, содержащему файлы jar, составляющие путь к классам для модуля, частью которого является класс.
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;
Я использовал Maven dependency: org.xeustechnologies:jcl-core:2.8, чтобы сделать это раньше, но после перехода на JDK 1.8 он иногда зависал и никогда не возвращался, застряв "в ожидании ссылок" в Reference::waitForReferencePendingList().
Я также храню карту загрузчиков классов, чтобы их можно было повторно использовать, если класс, который я пытаюсь создать, находится в том же модуле, что и класс, который я уже создал, что я бы рекомендовал.
Это может быть поздний ответ, я могу сделать это так (простой пример для fastutil-8.2.2.jar), используя класс jhplot.Web из 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
Согласно документации, этот файл будет загружен внутри "lib / user", а затем динамически загружен, так что вы можете сразу же начать использовать классы из этого jar-файла в той же программе.
Пожалуйста, посмотрите на этот проект, который я начал: proxy-object lib
Эта библиотека загрузит jar из файловой системы или любого другого места. Он будет использовать загрузчик классов для jar-файла, чтобы убедиться, что нет библиотечных конфликтов. Пользователи смогут создать любой объект из загруженного фляги и вызвать любой метод на нем. Эта библиотека была разработана для загрузки jar-файлов, скомпилированных в Java 8, из базы кода, поддерживающей Java 7.
Чтобы создать объект:
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 поддерживает фабричные методы, вызов статических функций и реализации интерфейса обратного вызова. я буду публиковать больше примеров на странице readme.
Для динамической загрузки jar-файлов вы можете использовать мою модификацию URLClassLoader. Эта модификация не имеет проблем с изменением файла jar во время работы приложения, как и стандартный загрузчик URLClassloader. Все загруженные файлы jar загружаются в ОЗУ и, следовательно, не зависят от исходного файла.
Мне нужно было загрузить файл jar во время выполнения как для java 8, так и для java 9+ (комментарии выше не работают для обеих этих версий). Вот способ сделать это (используя Spring Boot 1.5.2, если это может быть связано).
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());
}
}
Я лично нахожу, что java.util.ServiceLoader выполняет эту работу довольно хорошо. Вы можете получить пример здесь.