Skip to content

Instantly share code, notes, and snippets.

@I-Al-Istannen
Last active February 19, 2018 14:39
Show Gist options
  • Save I-Al-Istannen/ec5586ec94a44ae63c23d096148c3cfa to your computer and use it in GitHub Desktop.
Save I-Al-Istannen/ec5586ec94a44ae63c23d096148c3cfa to your computer and use it in GitHub Desktop.
A small POC class loader, operating purely in-memory. Useful if you do not want to save a temporary file on the disk. Very limited testing (one Javafx app) and likely to not play well with ServideLocators and tons of other features you normally need.
package me.ialistannen.jafafxtests.memoryclassloader;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
/**
* This is a minimally tested {@link ClassLoader} that is able to read a jar file in memory and then
* execute it (if you know the main class).
*
* Service locators and many other things might be royally screwed, it was enough to load my small
* sample JavaFx application though.
*
* <p>
*
* <br><strong>This class will register its own {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)},
* so look out for that. If you need your own, register it before calling the constructor of this
* class and be sure to provide an implementation of {@code "inmemoryurl"} that defers to this class
* loader.</strong>
*
* <p>
*
* <br>The code for this class can be found <a href="https://gist.github.com/I-Al-Istannen/ec5586ec94a44ae63c23d096148c3cfa"!>here</a>.
*
* @author I Al Istannen
* @version 2018-02-18
*/
public class MemoryClassLoader extends ClassLoader {
private Map<String, byte[]> memoryData;
private Map<String, Class<?>> loadedClasses;
/**
* Creates a new {@link MemoryClassLoader} using the given lookup map.
*
* @param memoryData the map to use for looking up data
*/
private MemoryClassLoader(Map<String, byte[]> memoryData) {
this.memoryData = memoryData;
this.loadedClasses = new HashMap<>();
// Register our custom protocol, so URLs created in the target application work correctly
// This means we also expose these resource globally, but oh well
try {
URL.setURLStreamHandlerFactory(new UrlStreamHandlerFactory());
} catch (Error e) {
System.err.println("Some factory is already registered. Not sure if its me.");
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (loadedClasses.containsKey(name)) {
return loadedClasses.get(name);
}
if (!memoryData.containsKey(name)) {
throw new ClassNotFoundException(name);
}
byte[] classBytes = memoryData.get(name);
return defineClass(name, classBytes, 0, classBytes.length);
}
@Override
public InputStream getResourceAsStream(String name) {
if (memoryData.containsKey(name)) {
return new ByteArrayInputStream(memoryData.get(name));
}
return super.getResourceAsStream(name);
}
@Override
protected URL findResource(String name) {
try {
if (!memoryData.containsKey(name)) {
return null;
}
// as this will always return an url, we need the if before
return createUrlForName(name);
} catch (MalformedURLException e) {
throw new RuntimeException("Invalid url: '" + name + "'", e);
}
}
private URL createUrlForName(String name) throws MalformedURLException {
return new URL("inmemoryurl", null, 0, name, new InMemoryUrlStreamHandler());
}
private static class UrlStreamHandlerFactory implements URLStreamHandlerFactory {
private final Map<String, URLStreamHandler> protocolHandlers;
UrlStreamHandlerFactory() {
protocolHandlers = new HashMap<>();
protocolHandlers.put("inmemoryurl", new InMemoryUrlStreamHandler());
}
public URLStreamHandler createURLStreamHandler(String protocol) {
return protocolHandlers.get(protocol);
}
}
/**
* A {@link URLStreamHandler} that reads from this loader's data map.
*/
private static class InMemoryUrlStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) {
return new InMemoryUrlConnection(u);
}
}
/**
* A {@link URLConnection} that reads from this loader's data map.
*/
private static class InMemoryUrlConnection extends URLConnection {
InMemoryUrlConnection(URL url) {
super(url);
}
@Override
public void connect() {
}
@Override
public InputStream getInputStream() {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(getURL().getFile());
}
}
/**
* Reads a Jar file from an {@link InputStream} (via {@link JarInputStream}), and returns an
* appropriate class loader.
*
* <br><strong>Please note that you must call {@link Thread#setContextClassLoader(ClassLoader)}
* with the returned class loader on the current thread.</strong>
*
* @param inputStream the inputStream to read from
* @return the created {@link MemoryClassLoader}
* @throws IOException if any error occurred reading from the stream
*/
public static MemoryClassLoader forInputStream(InputStream inputStream) throws IOException {
if (inputStream instanceof JarInputStream) {
return forJarInputStream((JarInputStream) inputStream);
}
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
JarInputStream jarInputStream = new JarInputStream(bufferedInputStream)) {
return forJarInputStream(jarInputStream);
}
}
/**
* Reads a Jar file from a {@link JarInputStream} and returns an appropriate class loader.
*
* <br><strong>Please note that you must call {@link Thread#setContextClassLoader(ClassLoader)}
* with the returned class loader on the current thread.</strong>
*
* @param jarInputStream the {@link JarInputStream} to read from
* @return the created {@link MemoryClassLoader}
* @throws IOException if any error occurred reading from the stream
*/
public static MemoryClassLoader forJarInputStream(JarInputStream jarInputStream)
throws IOException {
Map<String, byte[]> map = new HashMap<>();
JarEntry entry;
while ((entry = jarInputStream.getNextJarEntry()) != null) {
String className = entry.getName();
if (className.endsWith(".class")) {
className = className
.replace(".class", "")
.replace('/', '.');
}
byte[] buffer = readFully(jarInputStream);
map.put(className, buffer);
}
return new MemoryClassLoader(map);
}
private static byte[] readFully(InputStream inputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int tmp;
while ((tmp = inputStream.read()) != -1) {
byteArrayOutputStream.write(tmp);
}
return byteArrayOutputStream.toByteArray();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment