Java 运行时尝试从外部JAR加载类时发生ClassNotFoundException
我试图在运行时从表示为byte[]数组的JAR加载一个类。Java 运行时尝试从外部JAR加载类时发生ClassNotFoundException,java,class,classloader,dynamic-class-loaders,Java,Class,Classloader,Dynamic Class Loaders,我试图在运行时从表示为byte[]数组的JAR加载一个类。 关于要加载的类,我知道两件事: 1.它实现了“RequiredInterface” 2.我知道它是限定名:“sample.TestJarLoadingClass” 我找到了我必须扩展类加载器的,但它抛出: Exception in thread "main" java.lang.ClassNotFoundException: sample.TestJarLoadingClass at java.lang.ClassLoader.
关于要加载的类,我知道两件事:
1.它实现了“RequiredInterface”
2.我知道它是限定名:“sample.TestJarLoadingClass”
我找到了我必须扩展类加载器的,但它抛出:
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)
每当我想加载类时。出现这种情况的原因是什么?我怎样才能摆脱这种情况?
非常感谢您的帮助 主要方法:
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;
}
}
JAR文件中的类:
package sample;
public class TestJarLoadingClass implements RequiredInterface {
@Override
public String method() {
return "works!";
}
}
在我看来,我们有两个问题: 首先,您应该重写包含装入类的实际逻辑的
findClass
方法。这里的主要挑战是找到包含类的字节数组的一部分——因为整个jar都是字节数组,所以需要使用JarInputStream
来扫描类的字节数组
但这可能还不够,因为您的字节类加载器不知道您的requireInterface
,所以您可以读取类本身,但类的定义包含它实现的requireInterface
信息,这对于类加载器来说是个问题。这个问题很容易解决,您只需将常规类加载器作为构造函数参数传递给您的类加载器,然后使用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);
}
}
请注意,我的实现假定文件名=类名,因此,例如,不会找到非顶级的类。当然,有些细节可能会更加精巧(比如异常处理)。我使用了您在我的机器上发布的代码,所有这些代码都工作正常。你能把你得到的确切的例外情况公布出来吗?另外,请检查您使用的路径是否真的指向包含要加载的类的jar?好的,我发布了整个异常消息。我检查了所有可能的“愚蠢”错误,一切都应该正常。请检查您的类加载器范围内是否没有需要加载的类。是的,您是对的,我错误地将依赖项添加到带有类的jar,而不是带有接口的jar。对不起,为什么不直接使用URLClassLoader,而不是从头开始重新实现一个呢?为什么不首先把这个jar放在类路径中呢?因为它需要我将jar存储在本地。在实际情况中,我的输入是来自应用程序前端的一个byte[]数组,我不希望在本地构建JAR文件,而只基于byte[]生成解决方案。我正在尝试运行您的解决方案,但每次我收到名为“TestJarLoadingClass”的异常:线程“main”中的异常java.lang.NoClassDefFoundError:TestJarLoadingClass(错误名称:sample/TestJarLoadingClass)@adsqew可能是因为我在TestJarLoadingClass
上测试了它,所以我的示例没有考虑它。我已经更新了这个示例,现在它考虑了不同的包(看看newEntryNameequalClassName
)。还要尝试将断点放在findClass
方法的开头,并查看JarEntry实例在每次迭代中的外观。它可能会让您更多地了解类是如何在Jar文件中内部存储的。很好,现在它可以按预期工作了。最后我想问一下,您是否知道为什么nextJarEntry.getSize()每次都给出结果-1(它是默认值)?我必须手动传递.class size。这很难说,因为在我的例子中,非类的大小为0,类的大小为正(在我的例子中TestJarLoadingClass的大小为445)。显然,您的环境与我的环境略有不同-我现在唯一能做的就是引用javadoc():返回输入数据的未压缩大小,或者-1(如果未知)。
但我认为这不会很有帮助:)我所要做的就是捕获变量的读取字节数:int read=jis.read(nextClass,0,maxFileSize);然后按如下方式使用它:defineClass(className,nextClass,0,read,null)
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());