1

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!";
    }
}
Community
  • 1
  • 1
Michał Szewczyk
  • 7,540
  • 8
  • 35
  • 47
  • I used code you've posted on my machine and all worked. Could you please post exact exception you've got? Also please check if path you're using is **really** pointing to jar that contains class that you want to load? – Michał Przybylak Feb 19 '17 at 10:35
  • Ok, I posted the whole exception message. I checked all of the possible "stupid" mistakes, everything should work. Please check if you don't have the class required to be load in a range of your classloader. – Michał Szewczyk Feb 19 '17 at 10:45
  • 1
    Yes, you are right, by mistake I added dependency to jar with class instead of jar with interface. Sorry – Michał Przybylak Feb 19 '17 at 10:48
  • Why don't you just use a URLClassLoader, instead of reimplementing one from scratch? And why don't you just put that jar in the classpath in the first place? – JB Nizet Feb 19 '17 at 10:48
  • Because it would require me to have JAR stored locally. In a real situation, my input is a byte[] array from front-end of my application and I would prefer not to build JAR file locally and make solution based only on byte[]. – Michał Szewczyk Feb 19 '17 at 10:55

1 Answers1

2

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

  • I'm trying to run your solution but every time I'm receiving for name "TestJarLoadingClass" exception: Exception in thread "main" java.lang.NoClassDefFoundError: TestJarLoadingClass (wrong name: sample/TestJarLoadingClass) – Michał Szewczyk Feb 19 '17 at 14:16
  • @adsqew it is probably because I've tested it on `TestJarLoadingClass` that I've put in default package, so my example did not take it into consideration. I've updated example and now it takes into consideration different packages (take a look at new `entryNameEqualsClassName`). Also try to put breakpoint at the beginning of `findClass` method and take a look how JarEntry instances will looks like in each iteration. It will probably give you more knowledge about how classes are stored internally in Jar file – Michał Przybylak Feb 19 '17 at 14:45
  • Great, now it's working as expected. I would like to ask you at the end if you know why nextJarEntry.getSize() gives result -1 (it's default value) every time? I have to pass the .class size manually. – Michał Szewczyk Feb 19 '17 at 15:18
  • 1
    It is difficult to say, because in my case I have size = 0 in case of not-classes and positive size in case of classes (in my case TestJarLoadingClass have size = 445). Obviously your environment will be little different then mine - the only one thing that I can do right now is to quote from javadoc (https://docs.oracle.com/javase/7/docs/api/java/util/zip/ZipEntry.html#getSize()) : `Returns the uncompressed size of the entry data, or -1 if not known.` but I assume it will be not very helpful :) – Michał Przybylak Feb 19 '17 at 15:27
  • Actually I have one idea - my class is super small, but yours may be much bigger - and I'm just casting class size from long to int - could you please check in debug if value without casting is positive? – Michał Przybylak Feb 19 '17 at 15:32
  • My size is 410 so I'm afraid that it's not the reason. I will try to solve it and I will give feedback what was the reason in a case of success. Thank you so much for help :) – Michał Szewczyk Feb 19 '17 at 15:38
  • 1
    All I had to do was to catch amount of read bytes into variable: int read = jis.read(nextClass, 0, maxFileSize); And then use it as follows: defineClass(className, nextClass, 0, read, null) – Michał Szewczyk Feb 19 '17 at 17:15