Як динамічно завантажувати файли JAR під час виконання?


308

Чому так важко зробити це на Java? Якщо ви хочете мати будь-яку модульну систему, вам потрібно мати можливість динамічно завантажувати файли JAR. Мені кажуть, що є спосіб зробити це, написавши свій власнийClassLoader , але це дуже багато роботи для чогось, що повинно бути (як мінімум, на увазі) таким же простим, як викликати метод з файлом JAR як його аргумент.

Будь-які пропозиції щодо простого коду, який це робить?


4
Я хочу зробити те саме, але запустити завантажену банку в більш пісочному середовищі (очевидно, з міркувань безпеки). Наприклад, я хочу заблокувати весь доступ до мережі та файлової системи.
Jus12

Відповіді:


253

Причина цього важко - безпека. Навантажувачі класів повинні бути незмінні; ви не повинні мати можливість вольово-невольно додавати до нього класи під час виконання. Я насправді дуже здивований, що працює з системним завантажувачем. Ось як ви це робите, роблячи власний дитячий навантажувач:

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

Болісно, ​​але є.


16
Єдина проблема такого підходу полягає в тому, що вам потрібно знати, які класи є в яких банках. На відміну від просто завантаження каталогу jar і потім інстанціювання класів. Я це неправильно розумію?
Аллайн Лалонде

10
Цей метод чудово працює при запуску в моєму IDE, але коли я будую свій JAR, я отримую ClassNotFoundException при виклику Class.forName ().
darrickc

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 ();
Джонстош

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 ...
Gus

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 (...). Будьте готові провести чудові ночі, досліджуючи, чому певні речі не працюють =)
Сергій Карпушин

Мені все ще цікаво, що заявки @ СергіяКарпушина все ще існують, оскільки проект був оновлений з часом до другої основної версії. Хотілося б почути досвід.
Ердін

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
Гм. Технічно оригінальний вміст має ліцензію CC, але якщо ви розміщуєте тут вміст, захищений авторським правом, це не усуває факту, що вміст захищений авторським правом. Якщо я публікую фотографії Міккі Мауса, це не робить його ліцензованим CC. Тому я додаю заяву про авторські права назад.
Jason S

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

Запустіть агент:

Для використання агента до запущеного JVM використовується бібліотека byte-buddy-agent :

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

Зауважте, що проект, здається, не дуже доглянутий. Їх дорожня карта на майбутнє містить, наприклад, кілька випусків на 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
Річард Żak

Працює з Java 8 EE в середовищі сервера додатків.
Jan

4

Рішення, запропоноване jodonnell, добре, але його слід трохи вдосконалити. Я використовував цю посаду, щоб успішно розвивати свою програму.

Призначте поточну нитку

По-перше, ми повинні додати

Thread.currentThread().setContextClassLoader(classLoader);

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

Не включайте

ваші банки в завантажувач батьківського класу або ви не зможете зрозуміти, хто що завантажує.

див. також Проблема з перезавантаженням банку за допомогою URLClassLoader

Однак рамки OSGi залишаються найкращим способом.


2
Ваша відповідь здається трохи заплутаною і, можливо, більше підходить як коментар до відповіді jodonnell, якщо це просто просте вдосконалення.
Zero3

4

Ще одна версія хакського рішення від Alain, яка також працює на 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 він дає попередження про депресію, але є тимчасовим рішенням для тих, хто використовує рішення Аллайна на JDK 11.


Чи можна також зняти банку?
користувач7294900

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, тому ця публікація допомагає змінити маніфест.mf

Біг

Підтримується Launcher-Agent-Classлише в JDK 9+ і відповідає за завантаження агента, не чітко визначаючи його в командному рядку:

 java -jar <your jar>

Спосіб, що працює на JDK 6+, визначає -javaagentаргумент:

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

Додавання нового Jar під час виконання

Потім можна додати банку за необхідності за допомогою наступної команди:

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

Я не знайшов жодних проблем, використовуючи це в документації.


Чомусь під час використання цього рішення я отримую "Виняток у потоці" 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 він іноді застигав і ніколи не повертався, "зачекавши посилань" за посиланням :: waitForReferencePendingList ().

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


2

будь ласка, подивіться на цей проект, який я розпочав: проксі-об'єкт lib

Ця вікно завантажить банку з файлової системи або будь-якого іншого місця. Він присвятить завантажувач класів для банку, щоб переконатися у відсутності конфліктів у бібліотеці. Користувачі зможуть створити будь-який об’єкт із завантаженої банки та викликати на ньому будь-який метод. Ця версія була розроблена для завантаження банок, складених на 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 в тій же програмі.


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 виконує цю роботу досить добре. Ви можете отримати приклад тут .


11
ServiceLoader не додає jar файли динамічно під час виконання. Файли jar повинні бути на classpath раніше.
angelcervera
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.