Skip to content

Commit

Permalink
Make embedded class loader MRJAR aware (#86316)
Browse files Browse the repository at this point in the history
This change updates the embedded class loader to be MRJAR aware.
  • Loading branch information
ChrisHegarty committed May 1, 2022
1 parent a5f1782 commit a617712
Show file tree
Hide file tree
Showing 3 changed files with 661 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,44 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.CodeSigner;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.SecureClassLoader;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.jar.Manifest;

import static java.util.jar.Attributes.Name.MULTI_RELEASE;
import static java.util.stream.Collectors.toUnmodifiableMap;

/**
* A class loader that is responsible for loading implementation classes and resources embedded within an archive.
* A class loader that is responsible for loading implementation classes and resources embedded
* within an archive.
*
* <p> This loader facilitates a scenario whereby an API can embed its implementation and dependencies all within the same archive as the
* API itself. The archive can be put directly on the class path, where it's API classes are loadable by the application class loader, but
* the embedded implementation and dependencies are not. When locating a concrete provider, the API can create an instance of an
* EmbeddedImplClassLoader to locate and load the implementation.
* <p> This loader facilitates a scenario whereby an API can embed its implementation and
* dependencies all within the same archive as the API itself. The archive can be put directly on
* the class path, where it's API classes are loadable by the application class loader, but the
* embedded implementation and dependencies are not. When locating a concrete provider, the API can
* create an instance of an EmbeddedImplClassLoader to locate and load the implementation.
*
* <p> The archive typically consists of two disjoint logically groups:
* 1. the top-level classes and resources,
* 2. the embedded classes and resources
*
* <p> The top-level classes and resources are typically loaded and located, respectively, by the parent of an EmbeddedImplClassLoader
* loader. The embedded classes and resources, are located by the parent loader as pure resources with a provider specific name prefix, and
* classes are defined by the EmbeddedImplClassLoader. The list of prefixes is determined by reading the entries in the MANIFEST.TXT.
* <p> The top-level classes and resources are typically loaded and located, respectively, by the
* parent of an EmbeddedImplClassLoader loader. The embedded classes and resources, are located by
* the parent loader as pure resources with a provider specific name prefix, and classes are defined
* by the EmbeddedImplClassLoader. The list of prefixes is determined by reading the entries in the
* LISTING.TXT.
*
* <p> For example, the structure of the archive named x-content:
* <pre>
Expand All @@ -56,68 +64,65 @@
public final class EmbeddedImplClassLoader extends SecureClassLoader {

private static final String IMPL_PREFIX = "IMPL-JARS/";
private static final String MANIFEST_FILE = "/LISTING.TXT";
private static final String JAR_LISTING_FILE = "/LISTING.TXT";

private final List<String> prefixes;
private final ClassLoader parent;
record JarMeta(String prefix, boolean isMultiRelease) {}

/** Ordered list of jar metadata (prefixes and code sources) to use when loading classes and resources. */
private final List<JarMeta> jarMetas;

/** A map of prefix to codebase, used to determine the code source when defining classes. */
private final Map<String, CodeSource> prefixToCodeBase;

private static Map<String, CodeSource> getProviderPrefixes(ClassLoader parent, String providerName) {
String providerPrefix = IMPL_PREFIX + providerName;
URL manifest = parent.getResource(providerPrefix + MANIFEST_FILE);
if (manifest == null) {
throw new IllegalStateException("missing x-content provider jars list");
}
try (
InputStream in = manifest.openStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(isr)
) {
List<String> jars = reader.lines().toList();
Map<String, CodeSource> map = new HashMap<>();
for (String jar : jars) {
map.put(providerPrefix + "/" + jar, new CodeSource(new URL(manifest, jar), (CodeSigner[]) null /*signers*/));
}
return Collections.unmodifiableMap(map);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/** The loader used to find the class and resource bytes. */
private final ClassLoader parent;

static EmbeddedImplClassLoader getInstance(ClassLoader parent, String providerName) {
return new EmbeddedImplClassLoader(parent, getProviderPrefixes(parent, providerName));
PrivilegedAction<EmbeddedImplClassLoader> pa = () -> new EmbeddedImplClassLoader(parent, getProviderPrefixes(parent, providerName));
return AccessController.doPrivileged(pa);
}

private EmbeddedImplClassLoader(ClassLoader parent, Map<String, CodeSource> prefixToCodeBase) {
private EmbeddedImplClassLoader(ClassLoader parent, Map<JarMeta, CodeSource> prefixToCodeBase) {
super(null);
this.prefixes = prefixToCodeBase.keySet().stream().toList();
this.prefixToCodeBase = prefixToCodeBase;
this.jarMetas = prefixToCodeBase.keySet().stream().toList();
this.parent = parent;
this.prefixToCodeBase = prefixToCodeBase.entrySet()
.stream()
.collect(toUnmodifiableMap(k -> k.getKey().prefix(), Map.Entry::getValue));
}

record Resource(InputStream inputStream, CodeSource codeSource) {}

/** Searches for the named resource. Iterates over all prefixes. */
private Resource privilegedGetResourceOrNull(String name) {
return AccessController.doPrivileged(new PrivilegedAction<Resource>() {
@Override
public Resource run() {
for (String prefix : prefixes) {
URL url = parent.getResource(prefix + "/" + name);
if (url != null) {
try {
InputStream is = url.openStream();
return new Resource(is, prefixToCodeBase.get(prefix));
} catch (IOException e) {
// silently ignore, same as ClassLoader
}
return AccessController.doPrivileged((PrivilegedAction<Resource>) () -> {
for (JarMeta jarMeta : jarMetas) {
URL url = findResourceForPrefixOrNull(name, jarMeta);
if (url != null) {
try {
InputStream is = url.openStream();
return new Resource(is, prefixToCodeBase.get(jarMeta.prefix()));
} catch (IOException e) {
// silently ignore, same as ClassLoader
}
}
return null;
}
return null;
});
}

@Override
public Class<?> findClass(String moduleName, String name) {
try {
Class<?> c = findClass(name);
if (moduleName != null && moduleName.equals(c.getModule().getName()) == false) {
throw new AssertionError("expected module:" + moduleName + ", got: " + c.getModule().getName());
}
return c;
} catch (ClassNotFoundException ignore) {}
return null;
}

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String filepath = name.replace('.', '/').concat(".class");
Expand All @@ -136,25 +141,148 @@ public Class<?> findClass(String name) throws ClassNotFoundException {
@Override
protected URL findResource(String name) {
Objects.requireNonNull(name);
URL url = prefixes.stream().map(p -> p + "/" + name).map(parent::getResource).filter(Objects::nonNull).findFirst().orElse(null);
if (url != null) {
return url;
for (JarMeta jarMeta : jarMetas) {
URL url = findResourceForPrefixOrNull(name, jarMeta);
if (url != null) {
return url;
}
}
return parent.getResource(name);
}

/**
* Searches for the named resource, returning its url or null if not found.
* Iterates over all multi-release versions, then the root, for the given jar prefix.
*/
URL findResourceForPrefixOrNull(String name, JarMeta jarMeta) {
URL url;
if (jarMeta.isMultiRelease) {
url = findVersionedResourceForPrefixOrNull(jarMeta.prefix(), name);
if (url != null) {
return url;
}
}
return parent.getResource(jarMeta.prefix() + "/" + name);
}

URL findVersionedResourceForPrefixOrNull(String prefix, String name) {
for (int v = RUNTIME_VERSION_FEATURE; v >= BASE_VERSION_FEATURE; v--) {
URL url = parent.getResource(prefix + "/" + MRJAR_VERSION_PREFIX + v + "/" + name);
if (url != null) {
return url;
}
}
return null;
}

@Override
protected Enumeration<URL> findResources(String name) throws IOException {
final int size = prefixes.size();
Enumeration<URL> enum1 = new Enumeration<>() {
private int jarMetaIndex = 0;

private URL url = null;

private boolean next() {
if (url != null) {
return true;
} else {
while (jarMetaIndex < jarMetas.size()) {
URL u = findResourceForPrefixOrNull(name, jarMetas.get(jarMetaIndex));
jarMetaIndex++;
if (u != null) {
url = u;
return true;
}
}
return false;
}
}

@Override
public boolean hasMoreElements() {
return next();
}

@Override
public URL nextElement() {
if (next() == false) {
throw new NoSuchElementException();
}
URL u = url;
url = null;
return u;
}
};

@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[size + 1];
for (int i = 0; i < size; i++) {
tmp[i] = parent.getResources(prefixes.get(i) + "/" + name);
}
tmp[size] = parent.getResources(name);
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
tmp[0] = enum1;
tmp[1] = parent.getResources(name);
return new CompoundEnumeration<>(tmp);
}

// -- infra

private static Map<JarMeta, CodeSource> getProviderPrefixes(ClassLoader parent, String providerName) {
String providerPrefix = IMPL_PREFIX + providerName;
URL listingURL = parent.getResource(providerPrefix + JAR_LISTING_FILE);
if (listingURL == null) {
throw new IllegalStateException("missing %s provider jars list".formatted(providerName));
}
try (
InputStream in = listingURL.openStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(isr)
) {
List<String> jars = reader.lines().toList();
Map<JarMeta, CodeSource> map = new HashMap<>();
for (String jar : jars) {
final String jarPrefix = providerPrefix + "/" + jar;
JarMeta jam;
if (isMultiRelease(parent, jarPrefix)) {
jam = new JarMeta(jarPrefix, true);
} else {
jam = new JarMeta(jarPrefix, false);
}
map.put(jam, codeSource(listingURL, jar));
}
return Map.copyOf(map);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static CodeSource codeSource(URL baseURL, String jarName) throws MalformedURLException {
return new CodeSource(new URL(baseURL, jarName), (CodeSigner[]) null /*signers*/);
}

private static boolean isMultiRelease(ClassLoader parent, String jarPrefix) throws IOException {
try (InputStream is = parent.getResourceAsStream(jarPrefix + "/META-INF/MANIFEST.MF")) {
if (is != null) {
Manifest manifest = new Manifest(is);
return Boolean.parseBoolean(manifest.getMainAttributes().getValue(MULTI_RELEASE));
}
}
return false;
}

private static final int BASE_VERSION_FEATURE = 8; // lowest supported release version
private static final int RUNTIME_VERSION_FEATURE = Runtime.version().feature();

static {
assert RUNTIME_VERSION_FEATURE >= BASE_VERSION_FEATURE;
}

private static final String MRJAR_VERSION_PREFIX = "META-INF/versions/";

private static String getParent(String uriString) {
int index = uriString.lastIndexOf('/');
if (index > 0) {
return uriString.substring(0, index);
}
return "/";
}

static final class CompoundEnumeration<E> implements Enumeration<E> {
private final Enumeration<E>[] enumerations;
private int index;
Expand Down

0 comments on commit a617712

Please sign in to comment.