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

StackOverflow https://stackoverflow.com/questions/60764

  •  09-06-2019
  •  | 
  •  

Вопрос

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

Есть какие-нибудь предложения по простому коду, который это делает?

Это было полезно?

Решение

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

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

Вам следует взглянуть на ОСГи, напримерреализовано в Платформа Затмения.Он делает именно это.Вы можете устанавливать, удалять, запускать и останавливать так называемые пакеты, которые по сути представляют собой файлы 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();
    }
}

С Ява 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

Запустите агент:

При этом используется байт-приятель-агент библиотека для добавления агента в работающую 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());
    }
}

Лучшее, что я нашел, это org.apache.xbean.classloader.JarFileClassLoader который является частью XBean проект.

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

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

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

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

Сначала нам нужно добавить

Thread.currentThread().setContextClassLoader(classLoader);

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

Не включать

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

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

Однако структура OSGi остается лучшим способом.

Еще одна версия хакерского решения от Аллена, которая также работает на 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 он выдает некоторые предупреждения об устаревании, но служит временным решением для тех, кто использует решение Alllain в JDK 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, поэтому эта почта помогает изменить манифест.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>));

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

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

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

пожалуйста, взгляните на этот проект, который я начал: библиотека прокси-объекта

Эта библиотека загрузит jar из файловой системы или любого другого места.Он выделит загрузчик классов для 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.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top