Last active
February 19, 2018 14:39
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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