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