Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
EmJar, embedded jar class loader for jar-in-jar bundles.
  • Loading branch information
Arne Georg Gleditsch committed May 21, 2014
commit 4cfcf9451a14ceda4aa7d1ee0adb7d78d58adbf9
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ The list of components currently reads as follows:
* [logging-context-gelf](logging-context-gelf) Context-aware GELF log handler
* [logging-context-json](logging-context-json) Context-aware JSON/logstash log formatter
* [pb-json](pb-json) Hassle-free conversion from Protobuf to JSON and back
* [emjar](emjar) Class loader and supporting cast for using jar-in-jar embedded archives as part of classpath
51 changes: 51 additions & 0 deletions emjar/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>commons</artifactId>
<groupId>com.comoyo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>

<name>emjar -- for loading dependencies from embedded jar files</name>
<groupId>com.comoyo.commons</groupId>
<artifactId>emjar</artifactId>

<url>https://github.com/comoyo/commons/emjar</url>
<packaging>jar</packaging>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12</version>
<configuration>
<redirectTestOutputToFile>false</redirectTestOutputToFile>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
105 changes: 105 additions & 0 deletions emjar/src/main/java/com/comoyo/emjar/Boot.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.comoyo.emjar;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Enumeration;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
* Dispatching main class that allows for <strong><code>java -jar
* bundle.jar</code></strong> invocation of bundled jars. To use:
*
* <ul>
* <li>Configure the bundle jar file's manifest attribute
* <strong><code>Main-Class</code></strong> as
* <strong><code>com.comoyo.emjar.Boot</code></strong>.</li>
*
* <li>Specify the actual main class using the
* <strong><code>EmJar-Main-Class</code></strong> manifest attribute
* or the <strong><code>emjar.main.class</code></strong>
* property.</li>
* </ul>
*
* <p/>
* (This class relies on reflection and some slightly questionable
* practices to hook the EmJar class loader into the system. See also
* {@link EmJarClassLoader} for an approach using explicit
* configuration that avoids this.)
*
*/
public class Boot
{
public final static String EMJAR_MAIN_CLASS_PROP = "emjar.main.class";
public final static String EMJAR_MAIN_CLASS_ATTR = "EmJar-Main-Class";
public final static String EMJAR_SYSTEM_PROPS_PROP = "emjar.system.properties";
public final static String EMJAR_SYSTEM_PROPS_ATTR = "EmJar-System-Properties";

public static void main(String[] args)
throws Exception
{
final EmJarClassLoader loader = new EmJarClassLoader(ClassLoader.getSystemClassLoader());
Thread.currentThread().setContextClassLoader(loader);

try {
// Make a best-effort attempt to update the system class
// loader. This is required to allow e.g LogManager to
// find classes inside embedded jars when parsing files
// given using the java.util.logging.config.file property.
final Field systemClassLoader = ClassLoader.class.getDeclaredField("scl");
systemClassLoader.setAccessible(true);
systemClassLoader.set(null, loader);
}
catch (NoSuchFieldException e) {
// Unable to set system class loader; continuing anyway
}

final String systemPropsName
= System.getProperty(EMJAR_SYSTEM_PROPS_PROP,
getManifestAttribute(EMJAR_SYSTEM_PROPS_ATTR));
if (systemPropsName != null) {
final Properties props = System.getProperties();
final InputStream is = loader.getResourceAsStream(systemPropsName);
if (is != null) {
props.load(new InputStreamReader(is));
is.close();
}
}
final String mainClassName
= System.getProperty(EMJAR_MAIN_CLASS_PROP,
getManifestAttribute(EMJAR_MAIN_CLASS_ATTR));
if (mainClassName == null) {
throw new RuntimeException(
"No main class specified using "
+ EMJAR_MAIN_CLASS_PROP + " or " + EMJAR_MAIN_CLASS_ATTR);
}

final Class<?> mainClass
= Class.forName(mainClassName, false, loader);
final Method mainMethod
= mainClass.getDeclaredMethod("main", new Class[]{String[].class});
mainMethod.invoke(null, new Object[]{args});
}

private static String getManifestAttribute(String key)
throws IOException
{
final Enumeration<URL> manifests
= Boot.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
while (manifests.hasMoreElements()) {
final URL url = manifests.nextElement();
final Manifest manifest = new Manifest(url.openStream());
final Attributes attributes = manifest.getMainAttributes();
final String value = attributes.getValue(key);
if (value != null) {
return value;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.comoyo.emjar;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

public class ByteBufferBackedInputStream
extends InputStream
{
private final ByteBuffer buf;

public ByteBufferBackedInputStream(ByteBuffer buf)
{
this.buf = buf;
}

public int read()
throws IOException
{
if (!buf.hasRemaining()) {
return -1;
}
return buf.get() & 0xff;
}

public int read(byte[] bytes, int off, int len)
throws IOException
{
if (!buf.hasRemaining()) {
return -1;
}

len = Math.min(len, buf.remaining());
buf.get(bytes, off, len);
return len;
}
}
186 changes: 186 additions & 0 deletions emjar/src/main/java/com/comoyo/emjar/EmJarClassLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package com.comoyo.emjar;

import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.io.File;
import java.io.IOException;

/**
* Class loader able to use embedded jars as part of the classpath.
* All jars specified in the original classpath are opened and
* inspected, any jar files found within will be added to the
* classpath. If the embedded jar files are stored using the ZIP
* archiving method <em>stored</em> (i.e no compression), they will be
* mapped directly and individual classes/elements can be loaded
* on-demand. For compressed embedded jars, initial access will
* preload the contents of all contained elements.
*
* <p/>
* The EmJar class loader can be invoked by setting the system
* property <strong><code>java.system.class.loader</code></strong> to
* the value
* <strong><code>com.comoyo.emjar.EmJarClassLoader</code></strong>,
* e.g by using the <strong><code>-D</code></strong> flag to the
* <strong><code>java</code></strong> executable. (The emjar classes
* must for obvious reasons be stored directly inside the bundle jar;
* i.e not within an embedded jar.)
*
* <p/>
* For a less manual approach that embeds all configuration in the
* bundled jar, see {@link Boot}.
*
* <p/>
* (To ensure that embedded jars are stored as-is and not compressed,
* use e.g <strong><code>maven-assembly-plugin</code></strong> version
* 2.4 or higher, and keep the
* <strong><code>recompressZippedFiles</code></strong> configuration
* option set to false (the default as of version 2.4)).
*
*/
public class EmJarClassLoader
extends URLClassLoader
{
private final static HandlerFactory factory = new HandlerFactory();

static {
try {
ClassLoader.registerAsParallelCapable();
}
catch (Throwable ignored) {
}
}

public EmJarClassLoader()
{
super(getClassPath(), null, factory);
}

public EmJarClassLoader(ClassLoader parent)
{
super(getClassPath(), parent, factory);
}

private static URL[] getClassPath()
{
final Properties props = System.getProperties();
final String classPath = props.getProperty("java.class.path");
final String pathSep = props.getProperty("path.separator");
final String fileSep = props.getProperty("file.separator");
final String userDir = props.getProperty("user.dir");

final ArrayList<URL> urls = new ArrayList<>();
for (String elem : classPath.split(pathSep)) {
if (!elem.endsWith(".jar")) {
continue;
}
final String full = elem.startsWith(fileSep) ? elem : userDir + fileSep + elem;
try {
urls.add(new URL("file:" + full));
final JarFile jar = new JarFile(elem);
Enumeration<JarEntry> embedded = jar.entries();
while (embedded.hasMoreElements()) {
final JarEntry entry = embedded.nextElement();
if (entry.getName().endsWith(".jar")) {
urls.add(new URL("jar:file:" + full + "!/" + entry.getName()));
}
}
jar.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
return urls.toArray(new URL[0]);
}

@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException
{
return super.loadClass(name);
}

private static class HandlerFactory
implements URLStreamHandlerFactory
{
private final Handler handler = new Handler();

@Override
public URLStreamHandler createURLStreamHandler(String protocol)
{
return "jar".equals(protocol) ? handler : null;
}
}

private static class Handler
extends URLStreamHandler
{
private final Map<String, JarURLConnection> connections
= new ConcurrentHashMap<>();
private final Map<String, Map<String, Map<String, OndemandEmbeddedJar.Descriptor>>> rootJars
= new ConcurrentHashMap<>();

@Override
protected URLConnection openConnection(URL url)
throws IOException
{
final String spec = url.getFile();
if (!spec.startsWith("jar:file:")) {
throw new IOException("Unable to handle " + spec);
}
JarURLConnection conn = connections.get(spec);
if (conn == null) {
synchronized (connections) {
conn = connections.get(spec);
if (conn == null) {
final int i = spec.indexOf("!/");
final int j = spec.indexOf("!/", i + 1);
if (i < 0 || j < 0) {
throw new IOException("Unable to parse " + spec);
}
final String root = spec.substring(9, i);
final String nested = spec.substring(i + 2, j);
final String entry = spec.substring(j + 2);

Map<String, Map<String, OndemandEmbeddedJar.Descriptor>> rootJar
= rootJars.get(root);
if (rootJar == null) {
synchronized (rootJars) {
rootJar = rootJars.get(root);
if (rootJar == null) {
final ZipScanner scanner
= new ZipScanner(new File(root));
rootJar = scanner.scan();
rootJars.put(root, rootJar);
}
}
}
final Map<String, OndemandEmbeddedJar.Descriptor> descriptors
= rootJar.get(nested);
if (descriptors != null) {
conn = new OndemandEmbeddedJar.Connection(
new URL(spec), root, descriptors, entry);
}
else {
conn = new PreloadedEmbeddedJar.Connection(
new URL(spec), root, nested, entry);
}
connections.put(spec, conn);
}
}
}
return conn;
}
}
}
Loading