Как загружать Jars динамически во время выполнения?
Почему так сложно сделать это на Java? Если вы хотите иметь какую-либо модульную систему, вам нужно будет динамически загружать банки. Мне говорят, что это способ сделать это, написав свой собственный ClassLoader
, но большая работа над тем, что должно (по моему мнению, по крайней мере) быть таким же простым, как вызов метода с файлом jar в качестве аргумента.
Любые предложения для простого кода, которые это делают?
Ответы
Ответ 1
Причиной этого является безопасность. Загрузчики классов должны быть неизменными; Вы не должны иметь возможность произвольно добавлять к нему классы во время выполнения. Я на самом деле очень удивлен, что работает с системным загрузчиком классов. Вот как вы делаете свой собственный дочерний загрузчик классов:
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);
Больно, но это так.
Ответ 2
Следующее решение хакерское, поскольку оно использует отражение для обхода инкапсуляции, но оно работает безупречно:
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);
Ответ 3
Вы должны взглянуть на OSGi, например. реализован в Eclipse Platform. Он делает именно это. Вы можете установить, удалить, запустить и остановить так называемые пакеты, которые являются файлами JAR. Но он делает немного больше, так как он предлагает, например. сервисы, которые могут быть динамически обнаружены в файлах JAR во время выполнения.
Или см. спецификацию Java Module System.
Ответ 4
Как насчет платформы загрузчика классов 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");
Ответ 5
Вот версия, которая не устарела. Я изменил оригинал, чтобы удалить устаревшие функции.
/**************************************************************************************************
* 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();
}
}
Ответ 6
В 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());
}
}
Ответ 7
Лучшее, что я нашел - это 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);
Ответ 8
Если вы работаете в Android, работает следующий код:
String jarFile = "path/to/jarfile.jar";
DexClassLoader classLoader = new DexClassLoader(jarFile, "/data/data/" + context.getPackageName() + "/", null, getClass().getClassLoader());
Class<?> myClass = classLoader.loadClass("MyClass");
Ответ 9
Решение, предложенное jodonnell, хорошо, но должно быть немного улучшено. Я использовал этот пост для успешного применения моего приложения.
Назначить текущий поток
Во-первых, мы должны добавить
Thread.currentThread().setContextClassLoader(classLoader);
или вы не сможете загрузить ресурс (например, spring/context.xml), хранящийся в банке.
Не включать
ваши банки в загрузчик родительского класса или вы не сможете понять, кто что загружает.
см. также Проблема с перезагрузкой банки с помощью URLClassLoader
Однако структура OSGi остается лучшим способом.
Ответ 10
Еще одна версия хакерского решения от 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.
Ответ 11
Другое рабочее решение, использующее Инструментарий, которое работает для меня. Он имеет преимущество в изменении поиска загрузчика классов, избегая проблем с видимостью классов для зависимых классов:
Создать класс агента
В этом примере он должен находиться в том же банке, который вызывается из командной строки:
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>));
Я не нашел никаких проблем с использованием этого в документации.
Ответ 12
Вот быстрый обходной путь для метода Allain, чтобы сделать его совместимым с более новыми версиями 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, так что вам нужно помнить об этом.
Ответ 13
Это может быть поздний ответ, я могу сделать это так (простой пример для 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 файла в той же программе.
Ответ 14
пожалуйста, посмотрите на этот проект, который я начал: 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.
Ответ 15
Я лично считаю, что java.util.ServiceLoader делает работу довольно хорошо. Вы можете получить пример здесь.