Is it possible to load classes on-the-fly (in memory) from a pack.gz file created by Pack200 without unpacking it back into a jar first? All of the examples that I can find just show me how to unpack it into a .jar file and then load the classes from the .jar file.
Asked
Active
Viewed 146 times
0
-
Yes, and this is routinely done with JWS. – user207421 Oct 29 '17 at 01:05
1 Answers
0
Yes, it’s possible. The Pack200.Unpacker.unpack method writes to a JarOutputStream; if you do that in a background thread, you can connect a new JarOutputStream to a JarInputStream using a pipe, and read from that.
public JarInputStream readPackFile(Path packGzFile)
throws IOException {
PipedInputStream pipeIn = new PipedInputStream();
PipedOutputStream pipeOut = new PipedOutputStream(pipeIn);
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Void> unpacker = new Callable<Void>() {
@Override
public Void call()
throws IOException {
try (InputStream file =
new GZIPInputStream(
new BufferedInputStream(
Files.newInputStream(packGzFile)));
JarOutputStream jarOutput = new JarOutputStream(pipeOut)) {
Pack200.newUnpacker().unpack(file, jarOutput);
return null;
} finally {
executor.shutdown();
}
}
};
executor.submit(unpacker);
return new JarInputStream(pipeIn);
}
That should be sufficient. However, there are still two issues:
- If there’s an error with the unpack operation, you’ll never know about it, since ExecutorServices suppress their taskṣ’ exceptions.
- JarInputStream and JarOutputStream (more specifically, their superclasses, InflaterInputStream and DeflaterOutputStream) don’t work well with pipes. This is because closing the JarOutputStream forces a call to its finish() method, but the JarInputStream on the other side of the pipe will never read that data. This means the JarInputStream will almost certainly be closed before
finish()
is called, causingfinish()
to generate an IOException due to a broken pipe.
To resolve the first problem, we can override the JarInputStream’s close
method to account for any failure in the unpack operation. To resolve the second, we can override finish()
in the JarOutputStream to do nothing:
public JarInputStream readPackFile(Path packGzFile)
throws IOException {
PipedInputStream pipeIn = new PipedInputStream();
PipedOutputStream pipeOut = new PipedOutputStream(pipeIn);
class NonFinishingJarOutputStream
extends JarOutputStream {
NonFinishingJarOutputStream(OutputStream out)
throws IOException {
super(out);
}
@Override
public void finish()
throws IOException {
// Deliberately empty.
}
}
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Void> unpacker = new Callable<Void>() {
@Override
public Void call()
throws IOException {
try (InputStream file =
new GZIPInputStream(
new BufferedInputStream(
Files.newInputStream(packGzFile)));
JarOutputStream jarOutput =
new NonFinishingJarOutputStream(pipeOut)) {
Pack200.newUnpacker().unpack(file, jarOutput);
return null;
} finally {
executor.shutdown();
}
}
};
Future<?> unpackerTask = executor.submit(unpacker);
return new JarInputStream(pipeIn) {
@Override
public void close()
throws IOException {
super.close();
try {
// If the unpack generated an exception, propagate it here.
unpackerTask.get();
} catch (ExecutionException e) {
throw new IOException(e);
} catch (InterruptedException e) {
InterruptedIOException iie = new InterruptedIOException();
iie.initCause(e);
throw iie;
}
}
};
}

VGR
- 40,506
- 4
- 48
- 63
-
That gives me a JarInputStream, not a JarFile. https://stackoverflow.com/questions/16602668/creating-a-classloader-to-load-a-jar-file-from-a-byte-array seems to indicate that it's difficult at best to actually do much with a JarInputStream. – Joseph Sible-Reinstate Monica Oct 29 '17 at 18:11
-
1All JarFile constructors require a file. I’m pretty sure there is no way to use the JarFile class without one. – VGR Oct 30 '17 at 00:13