尝试在运行时从外部JAR加载类时发生ClassNotFoundException [英] ClassNotFoundException while trying to load class from external JAR at runtime
问题描述
我正在尝试在运行时从JAR加载一个表示为byte []数组的类.
我知道要加载的类有两件事:
1.它实现"RequiredInterface"
2.我知道它的限定名称:"sample.TestJarLoadingClass"
我找到了解决方案在其中我必须扩展ClassLoader但它会抛出:
I'm trying to load a class from JAR represented as a byte[] array at runtime.
I know two things about class to load:
1. it implements "RequiredInterface"
2. I know it's qualified name: "sample.TestJarLoadingClass"
I found the solution in which I have to extend ClassLoader but it throws:
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)
每当我要加载类时.
造成这种情况的原因是什么?如何消除这种情况?
任何帮助,高度赞赏
whenever I want to load the class.
What can be the reason of this situation and how can I get rid of that?
Any help highly appreciated
主要方法:
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();
}
自定义类加载器:
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;
}
}
RequiredInterface:
RequiredInterface:
public interface RequiredInterface {
String method();
}
JAR文件中的类:
package sample;
public class TestJarLoadingClass implements RequiredInterface {
@Override
public String method() {
return "works!";
}
}
推荐答案
在我看来,这里有两个问题:
In my opinion we have two problems here:
首先,您应该覆盖包含加载类实际逻辑的findClass
方法.这里的主要挑战是找到包含您的类的字节数组的一部分-由于您将整个jar都作为字节数组,因此您将需要使用JarInputStream
扫描类的字节数组.
First of all you should override findClass
method that contains actual logic of loading class. The main challenge here is to find part of byte array that contains your class - since you have whole jar as a byte array, you will need to use JarInputStream
to scan your byte array for your class.
但这可能还不够,因为ByteClassLoader
对于您的ByteClassLoader
是未知的-因此您将能够读取类本身,但是类的定义包含其实现RequiredInterface
的信息,这对于您的类加载器.这个很容易修复,您只需要将常规的类加载器作为构造函数参数传递给您的那个,并使用super(parentClassLoader)
.
But this might not be enough because your RequiredInterface
is unknown for your ByteClassLoader
- so you will be able to read class itself, but the definition of class contains information that it implements RequiredInterface
which is a problem for your class loader. This one is easy to fix, you just need to pass regular class loader as a constructor parameter to your one and use 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());
请注意,我的实现假定文件名=类名,因此,例如,将找不到非顶级类.当然,某些细节可能会更完善(例如异常处理).
Please be aware that my implementation assumes that file name = class name, so for example classes that are not top level will not be found. And of course some details might be more polished (like exception handling).
这篇关于尝试在运行时从外部JAR加载类时发生ClassNotFoundException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!