A ClassLoader in Java helps in dynamically loading Java classes into the JVM. Java comes with a many different types of ClassLoader(s). However, many a times we are required to develop our own custom ClassLoader. Let us check how we can do that. In the following example, we will develop a ClassLoader to load classes from an in-memory Stream without writing it to a file and using a URLClassLoader.

First step would be to actually create a jar say calculations-1.0.jar. We will load it in a stream and then load all its classes. The jar file will have the following 2 classes.

//Addition.java

package com.techitmore.calculations;

public class Addition {
    public static int add(int a, int b) {
        return a + b;
    }
}

//Subtraction.java

package com.techitmore.calculations;

public class Subtraction {
    public static int subtract(int a, int b) {
        return a - b;
    }
}

Let us now switch to the main program where we will load this jar and its classes. Since our aim is to write a custom ClassLoader for loading classes from an in-memory stream, we should first load this jar in a JarInputStream. In this example, we are using a FileInputStream to create a JarInputStream. However, we could have used any InputStream where we are getting the jar file contents from.

//Startup.java

package com.techitmore.jarloaderexample;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.jar.JarInputStream;

public class Startup {
    public static void main(String[] args) throws FileNotFoundException, IOException {
        String path = "../calculations/target/calculations-1.0.jar";
        FileInputStream fileInputStream = new FileInputStream(path);
        JarInputStream jarInputStream = new JarInputStream(fileInputStream);
    }
}

Next step is to focus on our custom ClassLoader say StreamClassLoader. The constructor of the ClassLoader will take a JarInputStream, loads its data and then create a map of the class names in the jar and its corresponding data as byte[]. This way it can load the data of any class contained in the stream.

//StreamClassLoader.java

package com.techitmore.jarloaderexample;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

public class StreamClassLoader extends ClassLoader {

    private final Map<String, byte[]> classData;

    public StreamClassLoader(JarInputStream jarInputStream) throws IOException {
        classData = new HashMap();

        JarEntry jarEntry = null;
        while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
            String entryName = jarEntry.getName();
            int entrySize = (int) jarEntry.getSize();
            byte[] entryData = new byte[entrySize];
            jarInputStream.read(entryData, 0, entrySize);
            
            if (entryName.endsWith(".class")) {
                String className = entryName.replace("/", ".").replace(".class", "");
                classData.put(className, entryData);
            }
        }   
    }
}

Override the following 2 functions to load a class. So the trick is to use the defineClass function and pass it the class data that we have stored in our map corresponding to the class name asked for.

@Override
public Class loadClass(String name) throws ClassNotFoundException {
    // note that it is required to first try loading the class using parent loader
    try {
        return super.loadClass(name);
    } catch (ClassNotFoundException e) {
        return findClass(name);
    }
}

@Override
public Class findClass(String name) throws ClassNotFoundException {
    Class loadedClass = null;

    byte[] data = classData.getOrDefault(name, new byte[0]);
    if (data.length == 0) {
        throw new ClassNotFoundException();
    }

    loadedClass = defineClass(name, data, 0, data.length, null);

    return loadedClass;
}

Thus the complete StreamClassLoader would look like:

//StreamClassLoader.java

package com.techitmore.jarloaderexample;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

public class StreamClassLoader extends ClassLoader {

    private final Map<String, byte[]> classData;

    public StreamClassLoader(JarInputStream jarInputStream) throws IOException {
        classData = new HashMap();

        JarEntry jarEntry = null;
        while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
            String entryName = jarEntry.getName();
            int entrySize = (int) jarEntry.getSize();
            byte[] entryData = new byte[entrySize];
            jarInputStream.read(entryData, 0, entrySize);

            if (entryName.endsWith(".class")) {
                String className = entryName.replace("/", ".").replace(".class", "");
                classData.put(className, entryData);
            }
        }
    }

    public String[] getAllClassNames() {
        Set<String> keyset = classData.keySet();
        return keyset.toArray(new String[keyset.size()]);
    }

    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        // note that it is required to first try loading the class using parent loader
        try {
            return super.loadClass(name);
        } catch (ClassNotFoundException e) {
            return findClass(name);
        }
    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        Class loadedClass = null;

        byte[] data = classData.getOrDefault(name, new byte[0]);
        if (data.length == 0) {
            throw new ClassNotFoundException();
        }

        loadedClass = defineClass(name, data, 0, data.length, null);

        return loadedClass;
    }
}

The ClassLoader can be tested from our Startup class. The complete code for the Startup class is:

//Startup.java

package com.techitmore.jarloaderexample;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.jar.JarInputStream;

public class Startup {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        String path = "../calculations/target/calculations-1.0.jar";
        FileInputStream fileInputStream = new FileInputStream(path);
        JarInputStream jarInputStream = new JarInputStream(fileInputStream);

        StreamClassLoader scl = new StreamClassLoader(jarInputStream);
        jarInputStream.close();
        fileInputStream.close();

        String[] classes = scl.getAllClassNames();

        for (String cl : classes) {
            Class c = scl.loadClass(cl);
            if (c != null) {
                System.out.println("Successfully loaded " + cl);
            }
        }
    }
}


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *