Как динамически загружать файлы JAR во время выполнения?

308

Почему так сложно сделать это на Java? Если вы хотите иметь какую-либо модульную систему, вы должны иметь возможность загружать файлы JAR динамически. Мне сказали, что есть способ сделать это, написав свой собственный ClassLoader, но это большая работа для чего-то, что должно (по крайней мере, на мой взгляд) быть таким же простым, как вызов метода с файлом JAR в качестве аргумента.

Любые предложения для простого кода, который делает это?

Аллен Лалонд
источник
4
Я хочу сделать то же самое, но запустить загруженный jar в более изолированной среде (очевидно, по соображениям безопасности). Например, я хочу заблокировать весь доступ к сети и файловой системе.
Jus12

Ответы:

254

Причина, по которой это сложно - безопасность. Загрузчики классов должны быть неизменными; Вы не должны иметь возможность произвольно добавлять к нему классы во время выполнения. Я на самом деле очень удивлен, что работает с системным загрузчиком классов. Вот как вы делаете свой собственный дочерний загрузчик классов:

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

Больно, но это так.

jodonnell
источник
16
Единственная проблема с этим подходом состоит в том, что вам нужно знать, какие классы находятся в каких банках. В отличие от простой загрузки директории jar, а затем создания экземпляров классов. Я неправильно это понимаю?
Аллен Лалонд
10
Этот метод отлично работает при запуске в моей IDE, но когда я собираю JAR, я получаю ClassNotFoundException при вызове Class.forName ().
Даррик
29
Используя этот подход, вы должны убедиться, что вы не будете вызывать этот метод загрузки более одного раза для каждого класса. Поскольку вы создаете новый загрузчик классов для каждой операции загрузки, он не может знать, был ли класс уже загружен ранее. Это может иметь плохие последствия. Например, синглтоны не работают, потому что класс загружался несколько раз, и поэтому статические поля существуют несколько раз.
Эдуард Уирч
8
Работает. Даже с зависимостями от других классов внутри фляги. Первая строка была неполной. Я использовал, URLClassLoader child = new URLClassLoader (new URL[] {new URL("file://./my.jar")}, Main.class.getClassLoader());предполагая, что файл JAR называется my.jarи находится в том же каталоге.
челюсть
4
Не забудьте указать URL url = file.toURI (). ToURL ();
Johnstosh
139

Следующее решение является хакерским, поскольку оно использует отражение, чтобы обойти инкапсуляцию, но оно работает безупречно:

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
Вся активность этого ответа заставляет задуматься, сколько хаков мы используем в разных системах. Я не уверен, что хочу знать ответ
Андрей Саву
6
Не работает так хорошо, если загрузчик системного класса оказывается чем-то отличным от URLClassLoader ...
Гас
6
Java 9+ предупреждает, что URLClassLoader.class.getDeclaredMethod("addURL", URL.class)это нелегальное использование рефлексии и в будущем произойдет сбой.
Charlweed
1
Есть идеи, как обновить этот код для работы с Java 9+?
FiReTiTi
1
@FiReTiTi Да !!
Мордехай
51

Вы должны взглянуть на OSGi , например, реализованный в платформе Eclipse . Это именно так. Вы можете устанавливать, удалять, запускать и останавливать так называемые пакеты, которые фактически являются файлами JAR. Но он делает немного больше, поскольку предлагает, например, сервисы, которые могут быть динамически обнаружены в файлах JAR во время выполнения.

Или посмотрите спецификацию для системы модулей Java .

Мартин Клинке
источник
41

Как насчет платформы загрузчика классов 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");
Крис
источник
9
Он также содержит ошибки и пропускает некоторые важные реализации, например findResources (...). Будьте готовы провести замечательные ночи, исследуя, почему некоторые вещи не работают =)
Сергей Карпушин
Мне все еще интересно, что @ Сергей Карпушин все еще присутствует, поскольку проект со временем обновился до второй основной версии. Хотелось бы услышать опыт.
Erdin Эрай
2
@ErdinEray, это очень хороший вопрос, который я тоже себе задаю, поскольку нас «заставили» перейти на OpenJDK. Я все еще работаю над Java-проектами, и у меня нет никаких доказательств того, что Open JDK подведет вас в эти дни (хотя у меня тогда была проблема). Я полагаю, что отозвал свое требование, пока не столкнулся с чем-то другим.
Сергей Карпушин
20

Вот версия, которая не является устаревшей. Я изменил оригинал, чтобы удалить устаревшую функциональность.

/**************************************************************************************************
 * 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
Я ненавижу натыкаться на старый поток, но я хотел бы отметить, что весь контент в stackoverflow лицензирован CC. Ваше заявление об авторском праве фактически неэффективно. stackoverflow.com/faq#editing
Гекл
43
Um. Технически, оригинальный контент имеет лицензию CC, но если вы размещаете здесь защищенный авторским правом контент, это не устраняет тот факт, что контент защищен авторским правом. Если я опубликую фотографию Микки Мауса, она не будет лицензирована по CC. Поэтому я добавляю заявление об авторских правах обратно.
Джейсон С
19

В то время как большинство решений, перечисленных здесь, являются либо взломами (до JDK 9), которые сложно настроить (агенты), или просто больше не работают (после JDK 9), я нахожу действительно шокирующим, что никто не упомянул четко документированный метод .

Вы можете создать собственный загрузчик классов системы, и тогда вы сможете делать все, что пожелаете. Отражение не требуется, и все классы используют один и тот же загрузчик классов.

При запуске JVM добавьте этот флаг:

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

Загрузчик классов должен иметь конструктор, принимающий загрузчик классов, который должен быть установлен в качестве его родителя. Конструктор будет вызываться при запуске JVM, и будет передан настоящий системный загрузчик классов, основной класс будет загружен пользовательским загрузчиком.

Чтобы добавить банки, просто позвоните ClassLoader.getSystemClassLoader()и приведите их к своему классу.

Проверьте эту реализацию для тщательно созданного загрузчика классов. Обратите внимание, вы можете изменить add()метод на публичный.

Мордехай
источник
Спасибо - это действительно полезно! Все остальные ссылки на веб-сайты используют методы для JDK 8 или более ранних версий, что имеет множество проблем.
Вишал Бияни
15

С 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());
    }
}
ФГБ
источник
9

Лучшее, что я нашел, - это 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);
Zeusoflightning125
источник
Обратите внимание, что проект не очень хорошо поддерживается. Например, их план на будущее содержит несколько выпусков на 2014 год.
Zero3
6

Если вы работаете на Android, работает следующий код:

String jarFile = "path/to/jarfile.jar";
DexClassLoader classLoader = new DexClassLoader(jarFile, "/data/data/" + context.getPackageName() + "/", null, getClass().getClassLoader());
Class<?> myClass = classLoader.loadClass("MyClass");
Джанер
источник
6

Вот быстрый обходной путь для метода Аллена, чтобы сделать его совместимым с более новыми версиями 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, так что вам нужно помнить об этом.

Антон Тананаев
источник
С Java 11.0.2 я получаю: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
Ричард
Работает с Java 8 EE в среде сервера приложений.
Jan
4

Решение, предложенное Джодоннеллом, хорошо, но должно быть немного улучшено. Я использовал этот пост, чтобы успешно разработать свое приложение.

Назначить текущий поток

Во-первых, мы должны добавить

Thread.currentThread().setContextClassLoader(classLoader);

или вы не сможете загрузить ресурс (такой как spring / context.xml), хранящийся в банке.

Не включать

ваши фляги в загрузчик родительского класса или вы не сможете понять, кто что загружает.

см. также Проблема перезагрузки банки с помощью URLClassLoader

Тем не менее, OSGi Framework остается лучшим способом.

venergiac
источник
2
Ваш ответ кажется немного запутанным и, возможно, больше подходит в качестве комментария к ответу Джодоннелла, если это просто простое улучшение.
Zero3
4

Еще одна версия хакерского решения от 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.

czdepski
источник
Могу ли я также удалить банку?
user7294900
3

Другое рабочее решение, использующее Инструментарий, которое работает для меня. Он имеет преимущество в изменении поиска загрузчика классов, избегая проблем с видимостью классов для зависимых классов:

Создать класс агента

В этом примере он должен находиться в том же банке, который вызывается из командной строки:

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

Я не нашел никаких проблем с использованием этого в документации.

czdepski
источник
Почему-то при использовании этого решения я получаю «Исключение в потоке» main «java.lang.ClassNotFoundException: agent.Agent». Я упаковал класс «Агент» в свое основное «военное» приложение, поэтому я уверен, что оно есть
Сергей
3

В случае, если кто-то ищет это в будущем, этот способ работает для меня с OpenJDK 13.0.2.

У меня есть много классов, которые мне нужно динамически создавать во время выполнения, каждый из которых может иметь свой путь к классу.

В этом коде у меня уже есть объект с именем pack, который содержит метаданные о классе, который я пытаюсь загрузить. Метод getObjectFile () возвращает местоположение файла класса для класса. Метод getObjectRootPath () возвращает путь к каталогу bin /, содержащему файлы классов, в которые входит класс, который я пытаюсь создать. Метод getLibPath () возвращает путь к каталогу, содержащему файлы jar, составляющие classpath для модуля, частью которого является класс.

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: org.xeustechnologies: jcl-core: 2.8, чтобы сделать это раньше, но после перехода с JDK 1.8 он иногда зависал и никогда не возвращался, застряв в ожидании ссылок в Reference :: waitForReferencePendingList ().

Я также храню карту загрузчиков классов, чтобы их можно было повторно использовать, если класс, который я пытаюсь создать, находится в том же модуле, что и класс, который я уже создал, что я бы рекомендовал.

ZGorlock
источник
2

пожалуйста, посмотрите на этот проект, который я начал: 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.

Алексей
источник
2

Это может быть поздний ответ, я могу сделать это так (простой пример для 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-файла в той же программе.

steve212
источник
1

Мне нужно было загрузить файл 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());
    }
}
Бон Рикимару
источник
-2

Лично я нахожу, что java.util.ServiceLoader выполняет эту работу довольно хорошо. Вы можете получить пример здесь .

tanyehzheng
источник
11
ServiceLoader не добавляет файлы JAR динамически во время выполнения. JAR-файлы должны быть в classpath ранее.
angelcervera