简体   繁体   中英

ClassNotFoundException while trying to load class from external JAR at runtime

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

Main method:

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

Custom class loader:

    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:

public interface RequiredInterface {
    String method();
}

Class in JAR file:

package sample;
public class TestJarLoadingClass implements RequiredInterface {
    @Override
    public String method() {
        return "works!";
    }
}

In my opinion we have two problems here:

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.

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

Here is my version:

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

And usage

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM