* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
package io.opentelemetry.javaagent.bootstrap;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
* Classloader used to run the core agent.
* <p>It is built around the concept of a jar inside another jar. This class loader loads the files
* of the internal jar to load classes and resources.
public class AgentClassLoader extends URLClassLoader {
// NOTE it's important not to use logging in this class, because this class is used before logging
ClassLoader.registerAsParallelCapable();
private static final String AGENT_INITIALIZER_JAR =
System.getProperty("OTel.javaagent.experimental.initializer.jar", "");
private static final String META_INF = "META-INF/";
private static final String META_INF_VERSIONS = META_INF + "versions/";
// multi release jars were added in java 9
private static final int MIN_MULTI_RELEASE_JAR_JAVA_VERSION = 9;
private static final int JAVA_VERSION = getJavaVersion();
private static final boolean MULTI_RELEASE_JAR_ENABLE =
JAVA_VERSION >= MIN_MULTI_RELEASE_JAR_JAVA_VERSION;
// Calling java.lang.instrument.Instrumentation#appendToBootstrapClassLoaderSearch
// adds a jar to the bootstrap class lookup, but not to the resource lookup.
// As a workaround, we keep a reference to the bootstrap jar
// to use only for resource lookups.
private final BootstrapClassLoaderProxy bootstrapProxy;
private final JarFile jarFile;
private final URL jarBase;
private final String jarEntryPrefix;
private final CodeSource codeSource;
private final boolean isSecurityManagerSupportEnabled;
private final Manifest manifest;
public AgentClassLoader(File javaagentFile) {
this(javaagentFile, "", false);
* Construct a new AgentClassLoader.
* @param javaagentFile Used for resource lookups.
* @param internalJarFileName File name of the internal jar
* @param isSecurityManagerSupportEnabled Whether this class loader should define classes with all
File javaagentFile, String internalJarFileName, boolean isSecurityManagerSupportEnabled) {
super(new URL[] {}, getParentClassLoader());
if (javaagentFile == null) {
throw new IllegalArgumentException("Agent jar location should be set");
if (internalJarFileName == null) {
throw new IllegalArgumentException("Internal jar file name should be set");
this.isSecurityManagerSupportEnabled = isSecurityManagerSupportEnabled;
bootstrapProxy = new BootstrapClassLoaderProxy(this);
+ (internalJarFileName.isEmpty() || internalJarFileName.endsWith("/") ? "" : "/");
jarFile = new JarFile(javaagentFile, false);
// base url for constructing jar entry urls
// we use a custom protocol instead of typical jar:file: because we don't want to be affected
// by user code disabling URLConnection caching for jar protocol e.g. tomcat does this
new URL("x-internal-jar", null, 0, "/", new AgentClassLoaderUrlStreamHandler(jarFile));
codeSource = new CodeSource(javaagentFile.toURI().toURL(), (Certificate[]) null);
manifest = jarFile.getManifest();
} catch (IOException e) {
throw new IllegalStateException("Unable to open agent jar", e);
if (!AGENT_INITIALIZER_JAR.isEmpty()) {
url = new File(AGENT_INITIALIZER_JAR).toURI().toURL();
} catch (MalformedURLException e) {
throw new IllegalStateException(
"Filename could not be parsed: "
+ ". Initializer is not installed",
private static ClassLoader getParentClassLoader() {
return new PlatformDelegatingClassLoader();
private static int getJavaVersion() {
String javaSpecVersion = System.getProperty("java.specification.version");
if ("1.8".equals(javaSpecVersion)) {
return Integer.parseInt(javaSpecVersion);
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// ContextStorageOverride is meant for library instrumentation we don't want it to apply to our
if ("io.grpc.override.ContextStorageOverride".equals(name)) {
throw new ClassNotFoundException(name);
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass(name);
// first search agent classes
clazz = findAgentClass(name);
// search from parent and urls added to this loader
clazz = super.loadClass(name, false);
private Class<?> findAgentClass(String name) throws ClassNotFoundException {
JarEntry jarEntry = findJarEntry(name.replace('.', '/') + ".class");
bytes = getJarEntryBytes(jarEntry);
} catch (IOException exception) {
throw new ClassNotFoundException(name, exception);
definePackageIfNeeded(name);
return defineClass(name, bytes);
public Class<?> defineClass(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length, codeSource);
protected PermissionCollection getPermissions(CodeSource codeSource) {
if (isSecurityManagerSupportEnabled) {
Permissions permissions = new Permissions();
permissions.add(new AllPermission());
return super.getPermissions(codeSource);
private byte[] getJarEntryBytes(JarEntry jarEntry) throws IOException {
int size = (int) jarEntry.getSize();
byte[] buffer = new byte[size];
try (InputStream is = jarFile.getInputStream(jarEntry)) {
while (offset < size && (read = is.read(buffer, offset, size - offset)) != -1) {
private void definePackageIfNeeded(String className) {
String packageName = getPackageName(className);
if (packageName == null) {
if (getPackage(packageName) == null) {
definePackage(packageName, manifest, codeSource.getLocation());
} catch (IllegalArgumentException exception) {
if (getPackage(packageName) == null) {
throw new IllegalStateException("Failed to define package", exception);
private static String getPackageName(String className) {
int index = className.lastIndexOf('.');
return index == -1 ? null : className.substring(0, index);
private JarEntry findJarEntry(String name) {
// shading renames .class to .classdata
boolean isClass = name.endsWith(".class");
name += getClassSuffix();
JarEntry jarEntry = jarFile.getJarEntry(jarEntryPrefix + name);
if (MULTI_RELEASE_JAR_ENABLE) {
jarEntry = findVersionedJarEntry(jarEntry, name);
// suffix appended to class resource names
// this is in a protected method so that unit tests could override it
protected String getClassSuffix() {
private JarEntry findVersionedJarEntry(JarEntry jarEntry, String name) {
// same logic as in JarFile.getVersionedEntry
if (!name.startsWith(META_INF)) {
// search for versioned entry by looping over possible versions form high to low
int version = JAVA_VERSION;
while (version >= MIN_MULTI_RELEASE_JAR_JAVA_VERSION) {
JarEntry versionedJarEntry =
jarFile.getJarEntry(jarEntryPrefix + META_INF_VERSIONS + version + "/" + name);
if (versionedJarEntry != null) {
return versionedJarEntry;
public URL getResource(String resourceName) {
URL bootstrapResource = bootstrapProxy.getResource(resourceName);
if (null == bootstrapResource) {
return super.getResource(resourceName);
return bootstrapResource;
public URL findResource(String name) {
URL url = findJarResource(name);
// find resource from agent initializer jar
return super.findResource(name);
private URL findJarResource(String name) {
JarEntry jarEntry = findJarEntry(name);
return getJarEntryUrl(jarEntry);
private URL getJarEntryUrl(JarEntry jarEntry) {
return new URL(jarBase, jarEntry.getName());
} catch (MalformedURLException e) {
throw new IllegalStateException(
"Failed to construct url for jar entry " + jarEntry.getName(), e);
public Enumeration<URL> findResources(String name) throws IOException {
// find resources from agent initializer jar
Enumeration<URL> delegate = super.findResources(name);
// agent jar can have only one resource for given name
URL url = findJarResource(name);
return new Enumeration<URL>() {
public boolean hasMoreElements() {
return first || delegate.hasMoreElements();
public URL nextElement() {
return delegate.nextElement();
public BootstrapClassLoaderProxy getBootstrapProxy() {
* A stand-in for the bootstrap class loader. Used to look up bootstrap resources and resources
* appended by instrumentation.
* <p>This class is thread safe.
public static final class BootstrapClassLoaderProxy extends ClassLoader {
private final AgentClassLoader agentClassLoader;
ClassLoader.registerAsParallelCapable();
public BootstrapClassLoaderProxy(AgentClassLoader agentClassLoader) {
this.agentClassLoader = agentClassLoader;
public URL getResource(String resourceName) {
// find resource from boot loader
URL url = super.getResource(resourceName);
if (agentClassLoader != null) {
JarEntry jarEntry = agentClassLoader.jarFile.getJarEntry(resourceName);
return agentClassLoader.getJarEntryUrl(jarEntry);
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
private static class AgentClassLoaderUrlStreamHandler extends URLStreamHandler {
private final JarFile jarFile;
AgentClassLoaderUrlStreamHandler(JarFile jarFile) {
protected URLConnection openConnection(URL url) {
return new AgentClassLoaderUrlConnection(url, jarFile);
private static class AgentClassLoaderUrlConnection extends URLConnection {
private final JarFile jarFile;
@Nullable private final String entryName;
@Nullable private JarEntry jarEntry;
AgentClassLoaderUrlConnection(URL url, JarFile jarFile) {
String path = url.getFile();
if (path.startsWith("/")) {
path = path.substring(1);
public void connect() throws IOException {
jarEntry = jarFile.getJarEntry(entryName);
throw new FileNotFoundException(
"JAR entry " + entryName + " not found in " + jarFile.getName());
public InputStream getInputStream() throws IOException {
throw new IOException("no entry name specified");
throw new FileNotFoundException(
"JAR entry " + entryName + " not found in " + jarFile.getName());
return jarFile.getInputStream(jarEntry);
public Permission getPermission() {
public long getContentLengthLong() {
return jarEntry.getSize();
} catch (IOException ignored) {
// We don't always delegate to platform loader because platform class loader also contains user
// classes when running a modular application. We don't want these classes interfering with the
private static class PlatformDelegatingClassLoader extends ClassLoader {
// this class loader doesn't load any classes, so this is technically unnecessary,
// but included for safety, just in case we every change Class.forName() below back to
registerAsParallelCapable();
private final ClassLoader platformClassLoader = getPlatformLoader();
public PlatformDelegatingClassLoader() {
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// prometheus exporter uses jdk http server, load it from the platform class loader
// some custom extensions use java.sql classes, make these available to agent and extensions
&& (name.startsWith("com.sun.net.httpserver.") || name.startsWith("java.sql."))) {
return platformClassLoader.loadClass(name);
return Class.forName(name, false, null);
private static ClassLoader getPlatformLoader() {
Must invoke ClassLoader.getPlatformClassLoader by reflection to remain
Method method = ClassLoader.class.getDeclaredMethod("getPlatformClassLoader");
return (ClassLoader) method.invoke(null);
} catch (InvocationTargetException
| IllegalAccessException exception) {
throw new IllegalStateException(exception);