なぜこれをJavaで行うのがそれほど難しいのですか?任意の種類のモジュールシステムが必要な場合は、JARファイルを動的にロードできる必要があります。独自ClassLoader
にを作成する方法はあると言われていますが、JARファイルを引数としてメソッドを呼び出すのと同じくらい簡単な(少なくとも私の心の中で)必要な作業には、多くの作業が必要です。
これを行う簡単なコードの提案はありますか?
なぜこれを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);
痛みを伴うが、それはあります。
URLClassLoader child = new URLClassLoader (new URL[] {new URL("file://./my.jar")}, Main.class.getClassLoader());
jarファイルが呼び出さmy.jar
れ、同じディレクトリにあると想定して使用しました。
次のソリューションはリフレクションを使用してカプセル化をバイパスするためハックですが、問題なく機能します。
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);
URLClassLoader.class.getDeclaredMethod("addURL", URL.class)
は、これがリフレクションの違法な使用であり、将来は失敗することを警告しています。
たとえば、Eclipse Platformに実装されているOSGiを見てください。それはまさにそれを行います。いわゆるバンドルと呼ばれるバンドルをインストール、アンインストール、開始、および停止できます。これらは事実上JARファイルです。ただし、実行時にJARファイルで動的に検出できるサービスなどを提供するため、もう少し多くのことができます。
または、Java Module Systemの仕様を参照してください。
どの程度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();
}
}
ここにリストされているほとんどのソリューションは、ハック(JDK 9以前)の設定が難しい(エージェント)か、機能しなくなった(JDK 9以降)かのどちらかですが、明確に文書化された方法について誰も言及しなかったのは本当に衝撃的です。
カスタムシステムクラスローダーを作成して、自由に操作できます。リフレクションは不要で、すべてのクラスが同じクラスローダーを共有します。
JVMを起動するときに、次のフラグを追加します。
java -Djava.system.class.loader=com.example.MyCustomClassLoader
クラスローダーには、クラスローダーを受け入れるコンストラクターが必要です。これは、その親として設定する必要があります。コンストラクターはJVMの起動時に呼び出され、実際のシステムクラスローダーが渡されます。メインクラスはカスタムローダーによってロードされます。
jarを追加するにはClassLoader.getSystemClassLoader()
、呼び出してクラスにキャストするだけです。
慎重に作成されたクラスローダーについては、この実装を確認してください。add()
メソッドをパブリックに変更できることに注意してください。
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());
}
}
私が見つけた最高のものは、XBeanプロジェクトの一部であるorg.apache.xbean.classloader.JarFileClassLoaderです。
特定のディレクトリ内のすべての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);
以下は、Javaの新しいバージョンと互換性を持たせるためのAllainのメソッドの簡単な回避策です。
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の新しいバージョンがリリースされたときに、いつか壊れる可能性があるため、そのことを覚えておく必要があります。
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
jodonnellによって提案されたソリューションは優れていますが、少し強化する必要があります。この投稿を使用して、成功したアプリケーションを開発しました。
まず、追加する必要があります
Thread.currentThread().setContextClassLoader(classLoader);
または、jarに格納されているリソース(spring / context.xmlなど)をロードできなくなります。
jarを親クラスローダーに入れないと、だれが何をロードしているかを理解できません。
URLClassLoaderを使用したjarのリロードの問題も参照してください
ただし、OSGiフレームワークは依然として最良の方法です。
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では、いくつかの非推奨の警告が表示されますが、JDK 11でAllainソリューションを使用する場合の一時的なソリューションとして機能します。
インストゥルメンテーションを使用した別の実用的な解決策。これには、クラスローダーの検索を変更して、依存クラスのクラスの可視性に関する問題を回避できるという利点があります。
エージェントクラスを作成する
この例では、コマンドラインから呼び出されたのと同じjar上にある必要があります。
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>));
ドキュメントでこれを使用しても問題は見つかりませんでした。
将来誰かがこれを検索する場合に備えて、この方法は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依存関係:org.xeustechnologies:jcl-core:2.8を使用していましたが、JDK 1.8を通過した後、それがフリーズして、Reference :: waitForReferencePendingList()で「参照待ち」でスタックして戻ってこないことがありました。
また、インスタンス化しようとしているクラスが、すでにインスタンス化したクラスと同じモジュールにある場合に再利用できるように、クラスローダーのマップを保持しています。
私が始めたこのプロジェクトを見てください:proxy-object lib
このlibは、ファイルシステムまたはその他の場所からjarをロードします。ライブラリの競合がないことを確認するために、jar専用のクラスローダーを使用します。ユーザーは、ロードされたjarから任意のオブジェクトを作成し、その上で任意のメソッドを呼び出すことができます。このlibは、Java 7をサポートするコードベースからJava 8でコンパイルされたjarをロードするように設計されています。
オブジェクトを作成するには:
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ページにさらに例を掲載します。
これは遅い応答になる可能性があります。DataMelt(http://jwork.org/dmelt)のjhplot.Webクラスを使用して、これ(fastutil-8.2.2.jarの簡単な例)のように実行できます。
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ファイルのクラスをすぐに使用できます。
Java 8とJava 9+の両方の実行時にjarファイルをロードする必要がありました(上記のコメントは、これらのバージョンの両方では機能しません)。これを行う方法は次のとおりです(関連する場合は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がかなりうまく機能していることに気づきました。ここで例を得ることができます。