Loading from the stream

For as long as I have been searching for, I have not found anything like a JarInputStreamClassLoader. That is, or at least that would have been in my intent, a ClassLoader that takes a stream corresponding to a Jar file and load classes from that. Why would I need something like that?, you may ask. Because I want to send a jar file to a server (via a servlet, a web service, or whatever is reachable by TCP/IP) as a stream of bytes, and have the server dynamically load classes from it (why do I want to do that? I will explain later). Maybe it really does not exist anywhere or, more probably, I have not searched hard enough. If you know about anything like that, please let me know in the comments.

However, the fact is: I decided to build it myself since I was actually doing this for training and learning purposes and, most important, I took it as a challenge: aren’t you really preventing me from doing it, Java, are you?

Actually there is the very useful java.net.URLClassLoader that works perfectly with the URL of a jar file. So, if you can receive the stream on the server and save it locally as a jar file, you are done. The problem was that I wanted to accomplish the task in a Google Application Engine webapp, an environment that does not let you to write on the local file system (unless you use their storage API, but it is basically your database interface and I did not want to use it for this task). So I needed to do the thing in memory. I can better explain the feature that I wanted to obtain with a JUnit test case:

	@Test
	public void testLoadStupidClass() throws FileNotFoundException, IOException, ClassNotFoundException
	{
		String pathname = "/Temp/stupidDTO-lib.jar";
		File inputFile = new File(pathname);
		JarInputStream jarStream = new JarInputStream(new FileInputStream(inputFile));
		JarInputStreamClassLoader loader = new JarInputStreamClassLoader(jarStream);
		Class<?> clazz = loader.loadClass("org.xxx.stupiddto.StupidPOJO");
		assertTrue(clazz != null);
	}

Imagine that instead of loading the stream from a file you already have a stream, in the form of an input stream of a servlet request, and that’s it:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
	{
		StringBuilder builder = new StringBuilder("loading...");
		JarInputStream jarStream = null;
		try
		{
			jarStream = new JarInputStream(new ByteArrayInputStream(ServletUtils.getHttpRequestBody(req.getInputStream())));
...

of course, it takes a bit of work to extract the jar data from the http request InputStream, and the work is performed by the ServletUtils.getHttpRequestBody (I’m not gonna show you the implementation here, since I have done that in a quite dirty way and I am not proud of it. I will return on that later).
The JarInputStreamClassLoader itself is quite simple once you have uderstood how to cycle on the entries of a JarInputStream and to properly load the raw bytes of each class. The hard thing, for me, has actually been extracting the jar data from the InputStream of the request.
Here is the complete code of the JarInputStreamClassLoader that I implemented:

package org.xxx.mockgen.web.classloader;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * Loads a class from a byte array representing the content of a jar file
 *
 */
public class JarInputStreamClassLoader extends ClassLoader
{
	private JarInputStream inputStream;
	protected RawClassList rawClasses;

	protected RawClassList getRawClasses()
	{
		return rawClasses;
	}

	protected void setRawClasses(RawClassList rawClasses)
	{
		this.rawClasses = rawClasses;
	}

	protected JarInputStream getInputStream()
	{
		return inputStream;
	}

	protected void setInputStream(JarInputStream inputStream)
	{
		this.inputStream = inputStream;
	}

	public JarInputStreamClassLoader(JarInputStream jarStream) throws IOException
	{
		JarEntry entry = null;
		// JarInputStream stream = getInputStream();
		setRawClasses(new RawClassList());
		while ((entry = jarStream.getNextJarEntry()) != null)
		{
			String entryName = entry.getName();
			int lastIndexOf = entryName.lastIndexOf(".class");
			String classCandidateName = "";
			if(lastIndexOf != -1)
			{
				classCandidateName = entryName.replace('/', '.').substring(0, entryName.lastIndexOf(".class"));
			}
			if (!classCandidateName.isEmpty())
			{
				ByteArrayOutputStream classBytesStream = new ByteArrayOutputStream();
				byte[] read = new byte[256];
				while (jarStream.read(read) != -1)
				{
					classBytesStream.write(read);
				}

				byte[] rawClassBytes = classBytesStream.toByteArray();
				RawClass rawClass = new RawClass(rawClassBytes, classCandidateName);
				getRawClasses().add(rawClass);
			}
		}
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException
	{
		Class<?> clazz = null;

		for (RawClass rawClass : getRawClasses())
		{
			String className = rawClass.getName();
			if (name.equals(className))
			{
				byte[] rawClassBytes = rawClass.getRawClassBytes();
				clazz = defineClass(name, rawClassBytes, 0, rawClassBytes.length);
				break;
			}
		}
		if (clazz == null)
		{
			throw new ClassNotFoundException("Class " + name + " not found.");
		}
		return clazz;
	}

	public List getAvailableClasses()
	{
		return getRawClasses().getClassesNames();
	}
}

The RawClass class is a very simple one: it stores the name and the raw bytes of each class entry found in the jar input stream. The RawClassList class is nothing more than a typed wrapper of ArrayList.
Lines from 43 to 65 of JarInputStreamClassLoader are the key: it iterates through the entries of the jar input stream and, if it finds that the entry is a java class, it stores it in a RawClass object for future use. The remainder of the class (method findClass) is just standard Java class loading.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s