在运行时尝试从外部 JAR 加载 class 时出现 ClassNotFoundException
ClassNotFoundException while trying to load class from external JAR at runtime
我正在尝试从运行时表示为 byte[] 数组的 JAR 加载 class。
我知道关于 class 加载的两件事:
1.它实现了"RequiredInterface"
2. 我知道它的限定名称:"sample.TestJarLoadingClass"
我发现 solution 我必须在其中扩展 ClassLoader 但它抛出:
Exception in thread "main" java.lang.ClassNotFoundException: sample.TestJarLoadingClass
at java.lang.ClassLoader.findClass(ClassLoader.java:530)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at tasks.Main.main(Main.java:12)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
每当我想加载 class.
造成这种情况的原因是什么?我该如何摆脱它?
非常感谢任何帮助
主要方法:
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
Path path = Paths.get("src/main/java/tasks/sample.jar");
RequiredInterface requiredInterface = (RequiredInterface) Class.forName("sample.TestJarLoadingClass", true, new ByteClassLoader(Files.readAllBytes(path))).newInstance();
}
自定义 class 加载程序:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ByteClassLoader extends ClassLoader {
private final byte[] jarBytes;
private final Set<String> names;
public ByteClassLoader(byte[] jarBytes) throws IOException {
this.jarBytes = jarBytes;
this.names = loadNames(jarBytes);
}
private Set<String> loadNames(byte[] jarBytes) throws IOException {
Set<String> set = new HashSet<>();
try (ZipInputStream jis = new ZipInputStream(new ByteArrayInputStream(jarBytes))) {
ZipEntry entry;
while ((entry = jis.getNextEntry()) != null) {
set.add(entry.getName());
}
}
return Collections.unmodifiableSet(set);
}
@Override
public InputStream getResourceAsStream(String resourceName) {
if (!names.contains(resourceName)) {
return null;
}
boolean found = false;
ZipInputStream zipInputStream = null;
try {
zipInputStream = new ZipInputStream(new ByteArrayInputStream(jarBytes));
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.getName().equals(resourceName)) {
found = true;
return zipInputStream;
}
}
} catch (IOException e) {;
e.printStackTrace();
} finally {
if (zipInputStream != null && !found) {
try {
zipInputStream.close();
} catch (IOException e) {;
e.printStackTrace();
}
}
}
return null;
}
}
所需接口:
public interface RequiredInterface {
String method();
}
Class 在 JAR 文件中:
package sample;
public class TestJarLoadingClass implements RequiredInterface {
@Override
public String method() {
return "works!";
}
}
我认为这里有两个问题:
首先,您应该覆盖包含实际加载逻辑的 findClass
方法 class。这里的主要挑战是找到包含你的 class 的字节数组的一部分 - 因为你有整个 jar 作为字节数组,你需要使用 JarInputStream
来扫描你的字节数组以获得你的 class.
但这可能还不够,因为你的 RequiredInterface
对于你的 ByteClassLoader
是未知的 - 所以你将能够阅读 class 本身,但是 [=30= 的定义] 包含它实现 RequiredInterface
的信息,这对您的 class 加载程序来说是个问题。这个很容易修复,你只需要将常规 class 加载器作为构造函数参数传递给你的加载器并使用 super(parentClassLoader)
.
这是我的版本:
public class ByteClassLoader extends ClassLoader {
private final byte[] jarBytes;
public ByteClassLoader(ClassLoader parent, byte[] jarBytes) throws IOException {
super(parent);
this.jarBytes = jarBytes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// read byte array with JarInputStream
try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
JarEntry nextJarEntry;
// finding JarEntry will "move" JarInputStream at the begining of entry, so no need to create new input stream
while ((nextJarEntry = jis.getNextJarEntry()) != null) {
if (entryNameEqualsClassName(name, nextJarEntry)) {
// we need to know length of class to know how many bytes we should read
int classSize = (int) nextJarEntry.getSize();
// our buffer for class bytes
byte[] nextClass = new byte[classSize];
// actual reading
jis.read(nextClass, 0, classSize);
// create class from bytes
return defineClass(name, nextClass, 0, classSize, null);
}
}
throw new ClassNotFoundException(String.format("Cannot find %s class", name));
} catch (IOException e) {
throw new ClassNotFoundException("Cannot read from jar input stream", e);
}
}
private boolean entryNameEqualsClassName(String name, JarEntry nextJarEntry) {
// removing .class suffix
String entryName = nextJarEntry.getName().split("\.")[0];
// "convert" fully qualified name into path
String className = name.replace(".", "/");
return entryName.equals(className);
}
}
和用法
RequiredInterface requiredInterface = (RequiredInterface)Class.forName("com.sample.TestJarLoadingClass", true, new ByteClassLoader(ByteClassLoader.class.getClassLoader(), Files.readAllBytes(path))).newInstance();
System.out.println(requiredInterface.method());
请注意,我的实现假定文件名 = class 名称,因此例如 class 不是顶级的文件将不会被发现。当然还有一些细节可能会更完善(比如异常处理)。
我正在尝试从运行时表示为 byte[] 数组的 JAR 加载 class。
我知道关于 class 加载的两件事:
1.它实现了"RequiredInterface"
2. 我知道它的限定名称:"sample.TestJarLoadingClass"
我发现 solution 我必须在其中扩展 ClassLoader 但它抛出:
Exception in thread "main" java.lang.ClassNotFoundException: sample.TestJarLoadingClass
at java.lang.ClassLoader.findClass(ClassLoader.java:530)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at tasks.Main.main(Main.java:12)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
每当我想加载 class.
造成这种情况的原因是什么?我该如何摆脱它?
非常感谢任何帮助
主要方法:
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
Path path = Paths.get("src/main/java/tasks/sample.jar");
RequiredInterface requiredInterface = (RequiredInterface) Class.forName("sample.TestJarLoadingClass", true, new ByteClassLoader(Files.readAllBytes(path))).newInstance();
}
自定义 class 加载程序:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ByteClassLoader extends ClassLoader {
private final byte[] jarBytes;
private final Set<String> names;
public ByteClassLoader(byte[] jarBytes) throws IOException {
this.jarBytes = jarBytes;
this.names = loadNames(jarBytes);
}
private Set<String> loadNames(byte[] jarBytes) throws IOException {
Set<String> set = new HashSet<>();
try (ZipInputStream jis = new ZipInputStream(new ByteArrayInputStream(jarBytes))) {
ZipEntry entry;
while ((entry = jis.getNextEntry()) != null) {
set.add(entry.getName());
}
}
return Collections.unmodifiableSet(set);
}
@Override
public InputStream getResourceAsStream(String resourceName) {
if (!names.contains(resourceName)) {
return null;
}
boolean found = false;
ZipInputStream zipInputStream = null;
try {
zipInputStream = new ZipInputStream(new ByteArrayInputStream(jarBytes));
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.getName().equals(resourceName)) {
found = true;
return zipInputStream;
}
}
} catch (IOException e) {;
e.printStackTrace();
} finally {
if (zipInputStream != null && !found) {
try {
zipInputStream.close();
} catch (IOException e) {;
e.printStackTrace();
}
}
}
return null;
}
}
所需接口:
public interface RequiredInterface {
String method();
}
Class 在 JAR 文件中:
package sample;
public class TestJarLoadingClass implements RequiredInterface {
@Override
public String method() {
return "works!";
}
}
我认为这里有两个问题:
首先,您应该覆盖包含实际加载逻辑的 findClass
方法 class。这里的主要挑战是找到包含你的 class 的字节数组的一部分 - 因为你有整个 jar 作为字节数组,你需要使用 JarInputStream
来扫描你的字节数组以获得你的 class.
但这可能还不够,因为你的 RequiredInterface
对于你的 ByteClassLoader
是未知的 - 所以你将能够阅读 class 本身,但是 [=30= 的定义] 包含它实现 RequiredInterface
的信息,这对您的 class 加载程序来说是个问题。这个很容易修复,你只需要将常规 class 加载器作为构造函数参数传递给你的加载器并使用 super(parentClassLoader)
.
这是我的版本:
public class ByteClassLoader extends ClassLoader {
private final byte[] jarBytes;
public ByteClassLoader(ClassLoader parent, byte[] jarBytes) throws IOException {
super(parent);
this.jarBytes = jarBytes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// read byte array with JarInputStream
try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(jarBytes))) {
JarEntry nextJarEntry;
// finding JarEntry will "move" JarInputStream at the begining of entry, so no need to create new input stream
while ((nextJarEntry = jis.getNextJarEntry()) != null) {
if (entryNameEqualsClassName(name, nextJarEntry)) {
// we need to know length of class to know how many bytes we should read
int classSize = (int) nextJarEntry.getSize();
// our buffer for class bytes
byte[] nextClass = new byte[classSize];
// actual reading
jis.read(nextClass, 0, classSize);
// create class from bytes
return defineClass(name, nextClass, 0, classSize, null);
}
}
throw new ClassNotFoundException(String.format("Cannot find %s class", name));
} catch (IOException e) {
throw new ClassNotFoundException("Cannot read from jar input stream", e);
}
}
private boolean entryNameEqualsClassName(String name, JarEntry nextJarEntry) {
// removing .class suffix
String entryName = nextJarEntry.getName().split("\.")[0];
// "convert" fully qualified name into path
String className = name.replace(".", "/");
return entryName.equals(className);
}
}
和用法
RequiredInterface requiredInterface = (RequiredInterface)Class.forName("com.sample.TestJarLoadingClass", true, new ByteClassLoader(ByteClassLoader.class.getClassLoader(), Files.readAllBytes(path))).newInstance();
System.out.println(requiredInterface.method());
请注意,我的实现假定文件名 = class 名称,因此例如 class 不是顶级的文件将不会被发现。当然还有一些细节可能会更完善(比如异常处理)。