OpenTelemetry javaagent 类加载器

1,104 阅读5分钟

启动 javaagent

OpenTelemetry javaagent 入口位于OpenTelemetryAgent类。premainagentmain方法皆委托调用startAgent方法,startAgent方法主要逻辑为引导 opentelemetry-javaagent jar 文件并且委托AgentInitializer完成 agent 的初始化工作。

public final class OpenTelemetryAgent {

  public static void premain(String agentArgs, Instrumentation inst) {
    startAgent(inst, true);
  }
    
  public static void agentmain(String agentArgs, Instrumentation inst) {
    startAgent(inst, false);
  }

  private static void startAgent(Instrumentation inst, boolean fromPremain) {
    try {
      // ① 引导 bootstrap jar
      File javaagentFile = installBootstrapJar(inst);
      InstrumentationHolder.setInstrumentation(inst);
      JavaagentFileHolder.setJavaagentFile(javaagentFile);
      // ② 初始化 agent
      AgentInitializer.initialize(inst, javaagentFile, fromPremain);
    } catch (Throwable ex) {
      // ignore code
    }
  }
}

引导 javaagent jar 文件

installBootstrapJar方法主要逻辑为:

  • 通过io.opentelemetry.javaagent.OpenTelemetryAgent定位 opentelemetry-javaagent JAR 文件。
  • 通过java.lang.instrument.InstrumentationappendToBootstrapClassLoaderSearch 方法让 bootstrap class loader 能够加载 opentelemetry-javaagent JAR 中的类。
private static synchronized File installBootstrapJar(Instrumentation inst)
      throws IOException, URISyntaxException {
    ClassLoader classLoader = OpenTelemetryAgent.class.getClassLoader();
    if (classLoader == null) {
      classLoader = ClassLoader.getSystemClassLoader();
    }
    URL url =
        classLoader.getResource(OpenTelemetryAgent.class.getName().replace('.', '/') + ".class");
    if (url == null || !"jar".equals(url.getProtocol())) {
      throw new IllegalStateException("could not get agent jar location from url " + url);
    }
    String resourcePath = url.toURI().getSchemeSpecificPart();
    int protocolSeparatorIndex = resourcePath.indexOf(":");
    int resourceSeparatorIndex = resourcePath.indexOf("!/");
    String agentPath = resourcePath.substring(protocolSeparatorIndex + 1, resourceSeparatorIndex);
    File javaagentFile = new File(agentPath);
	// ① opentelemetry-javaagent jar
    JarFile agentJar = new JarFile(javaagentFile, false); 
    verifyJarManifestMainClassIsThis(javaagentFile, agentJar);
    // ② 让 bootstrap CL 能够加载 opentelemetry-javaagent jar 中的 class
    inst.appendToBootstrapClassLoaderSearch(agentJar);
    return javaagentFile;
}

初始化 agent

Agent 初始化工作代码位于AgentInitializerinitialize方法主要逻辑为:

  • 创建AgentClassLoader对象。
  • 创建AgentStarter执行start方法。
public static void initialize(Instrumentation inst, File javaagentFile, 
                              boolean fromPremain) throws Exception {
    isSecurityManagerSupportEnabled = isSecurityManagerSupportEnabled();
    execute(
        new PrivilegedExceptionAction<Void>() {
          @Override
          public Void run() throws Exception {
            // ① 创建 AgentClassLoader
            agentClassLoader = createAgentClassLoader("inst", javaagentFile);
            // ② 创建 AgentStarter 
            agentStarter = createAgentStarter(agentClassLoader, inst, javaagentFile);
            // ③ 执行 start 方法,启动 agent
            if (!fromPremain || !delayAgentStart()) {
              agentStarter.start();
            }
            return null;
          }
        });
  }

AgentClassLoader

创建 AgentClassLoader

private static ClassLoader createAgentClassLoader(String innerJarFilename, File javaagentFile) {
    // innerJarFilename = "inst"
	return new AgentClassLoader(javaagentFile, innerJarFilename, isSecurityManagerSupportEnabled);
}

加载 class

  • 根据 class 名称,尝试去inst目录下查找到应的 class,存在则加载。
  • class 不在inst目录下,则委托父 class loader 加载。
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // ...
    synchronized (getClassLoadingLock(name)) {
      Class<?> clazz = findLoadedClass(name);
      // ① first search agent classes
      if (clazz == null) {
        clazz = findAgentClass(name);
      }
      // ② search from parent and urls added to this loader
      if (clazz == null) {
        clazz = super.loadClass(name, false);
      }
      if (resolve) {
        resolveClass(clazz);
      }
      return clazz;
    }
}

查找 class

  • 根据 class 名称转换成对应的文件路径,为路径拼接上前缀inst/和后缀.classdata
  • 前缀来自成员变量jarEntryPrefix,在构造器内完成赋值,由AgentInitializercreateAgentClassLoader方法传入字符中"inst",在构造器内拼接上"/",变成"inst/"
  • 后缀为字符串".class"拼接上getClassSuffix方法返回的固定值"data"而来,即".classdata"

假设要加载的 class 为okhttp3.OkHttpClient,则最终在 opentelemetry-javaagent JAR 目录下查找的文件路径为inst/okhttp3/OkHttpClient.classdata

private Class<?> findAgentClass(String name) throws ClassNotFoundException {
    // ① 查找对应的 class 文件
    JarEntry jarEntry = findJarEntry(name.replace('.', '/') + ".class");
    if (jarEntry != null) {
      byte[] bytes;
      try {
        bytes = getJarEntryBytes(jarEntry);
      } catch (IOException exception) {
        throw new ClassNotFoundException(name, exception);
      }
      definePackageIfNeeded(name);
      return defineClass(name, bytes);
    }
    return null;
}

private JarEntry findJarEntry(String name) {
    // shading renames .class to .classdata
    boolean isClass = name.endsWith(".class");
    if (isClass) {
      // ② 为 .class 文件拼接后缀 "data"
      name += getClassSuffix(); 
    }
	// ③ 拼接后缀 "inst/"
    JarEntry jarEntry = jarFile.getJarEntry(jarEntryPrefix + name);
    if (MULTI_RELEASE_JAR_ENABLE) {
      jarEntry = findVersionedJarEntry(jarEntry, name);
    }
    return jarEntry;
}

protected String getClassSuffix() {
    return "data";
}

inst目录主要包含 javaagent instrumentation 模块,针对各框架及中间件的支持,展开目录inst/io/opentelemetry/javaagent/instrumentation,可看到 OpenTelemetry javaagent 支持的框架和中间件非常丰富,基本涵盖了常用的开源框架和中间件。由于文件目录过多,为了减少篇幅占用,以下示意图为执行 bash tree -L 1命令后仅保持了部分目录名称后的样子,实际上 OpenTelemetry javaagent 支持的框架和中间件远远多于以下示意图。

...
├── apachedubbo
├── apachehttpclient
├── cassandra
├── elasticsearch
├── executors
├── graphql
├── grizzly
├── grpc
├── hikaricp
├── httpclient
├── hystrix
├── jdbc
├── jedis
├── jetty
├── jms
├── kafkaclients
├── kafkastreams
├── kubernetesclient
├── lettuce
├── micrometer
├── mongo
├── netty
├── okhttp
├── opensearch
├── rabbitmq
├── reactor
├── reactornetty
├── rmi
├── rocketmqclient
├── rxjava
├── servlet
├── sparkjava
├── spring
├── springweb
├── tomcat
├── undertow
├── vertx
└── ...

ExtensionClassLoader

ExtensionClassLoader用于加载 otel instrumentation javaagent 扩展包内的类和资源文件。

创建 ExtensionClassLoader

通过AgentInitializercreateAgentStarter方法创建AgentStarter,调用其start方法执行agent 字节码增强。
创建AgentStarterImpl,代码如下:

private static AgentStarter createAgentStarter(
      ClassLoader agentClassLoader, Instrumentation instrumentation, File javaagentFile)
      throws Exception {
    Class<?> starterClass =
        agentClassLoader.loadClass("io.opentelemetry.javaagent.tooling.AgentStarterImpl");
    Constructor<?> constructor =
        starterClass.getDeclaredConstructor(Instrumentation.class, File.class, boolean.class);
    return (AgentStarter)
        constructor.newInstance(instrumentation, javaagentFile, isSecurityManagerSupportEnabled);
  }

AgentStarterImplstart方法主要逻辑为:

  • 创建 ExtensionClassLoader。
  • 通过 Bytebuddy 执行 agent 字节码增强。
@Override
public void start() {
    // ① 创建 ExtensionClassLoader
    extensionClassLoader = createExtensionClassLoader(getClass().getClassLoader());
    // ② 安装 Bytebuddy agent
    AgentInstaller.installBytebuddyAgent(instrumentation, extensionClassLoader);
    WeakConcurrentMapCleaner.start();
}

创建 Extension ClassLoader,代码位于类ExtensionClassLoader中的getInstance方法,它会为每一个扩展的 otel instrumentation javaagent JAR 创建一个单独的ExtensionClassLoader对象。这些扩展可能包括 SDK 组件(exporterspropagators)和其他工具,它们必须被隔离和着色以减少对用户应用程序的干扰并使其与 otel javaagent 使用的 SDK 兼容。 因此,每个扩展 JAR 都有一个单独的类加载器 并最终将这些类加载器对象通过组合模式封装在MultipleParentClassLoader中,该类由 Bytebuddy 提供,其loadClass类加载方法会委托给通过构造器函数传入的类加载器列表来依次执行,相关代码请见下文。Extension ClassLoader 主要包含三类:

  • opentelemetry javaagent JAR 文件中内嵌的扩展 javaagent JAR。
  • 通过系统变量otel.javaagent.extensions或环境变量OTEL_JAVAAGENT_EXTENSIONS指定的扩展 JAR 列表。
  • 实验性的扩展 jar,可通过系统变量otel.javaagent.experimental.extensions或环境变量OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS指定。
public static ClassLoader getInstance(
      ClassLoader parent, File javaagentFile, boolean isSecurityManagerSupportEnabled) {
    List<URL> extensions = new ArrayList<>();
    // ① opentelemetry javaagent JAR 文件中内嵌的扩展 javaagent JAR
    includeEmbeddedExtensionsIfFound(parent, extensions, javaagentFile);
    // ② opentelemetry 扩展类 javaagent JAR
    extensions.addAll(
        parseLocation(
            System.getProperty(EXTENSIONS_CONFIG, System.getenv("OTEL_JAVAAGENT_EXTENSIONS")),
            javaagentFile));
	// ③ 实验性的扩展类 javaagent JAR
    extensions.addAll(
        parseLocation(
            System.getProperty(
                "otel.javaagent.experimental.extensions",
                System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS")),
            javaagentFile));
    if (extensions.isEmpty()) {
      return parent;
    }
    List<ClassLoader> delegates = new ArrayList<>(extensions.size());
    for (URL url : extensions) {
      // ④ 分别创建专用的 ExtensionClassLoader
      delegates.add(getDelegate(parent, url, isSecurityManagerSupportEnabled));
    }
    return new MultipleParentClassLoader(parent, delegates);
}

private static URLClassLoader getDelegate(ClassLoader parent, URL extensionUrl, 
	boolean isSecurityManagerSupportEnabled) {
	return new ExtensionClassLoader(extensionUrl, parent, isSecurityManagerSupportEnabled);
}

ExtensionClassLoader派生自URLClassLoader,此类加载器用于从引用的 JAR 文件和目录的 URL 的搜索路径加载类和资源,此类加载器支持从给定 URL 引用的多版本 JAR 文件的内容加载类和资源。

public class ExtensionClassLoader extends URLClassLoader {
	private ExtensionClassLoader(
      URL url, ClassLoader parent, boolean isSecurityManagerSupportEnabled) {
    	super(new URL[] {url}, parent);
    	this.isSecurityManagerSupportEnabled = isSecurityManagerSupportEnabled;
	} 
}

内嵌扩展 JAR

在 opentelemetry javaagent JAR 的extensions/目录下查找扩展 JAR 文件,将它们复制到临时目录,为复制后的文件映射成URL对象,用于后续创建专用的ExtensionClassLoader

private static void includeEmbeddedExtensionsIfFound(
      ClassLoader parent, List<URL> extensions, File javaagentFile) {
    try {
      JarFile jarFile = new JarFile(javaagentFile, false);
      Enumeration<JarEntry> entryEnumeration = jarFile.entries();
      String prefix = "extensions/";
      File tempDirectory = null;
      while (entryEnumeration.hasMoreElements()) {
        JarEntry jarEntry = entryEnumeration.nextElement();
        String name = jarEntry.getName();
        // ① 查找 opentelemetry javaagent JAR extensions/ 目录下的文件
        if (name.startsWith(prefix) && !jarEntry.isDirectory()) {
          tempDirectory = ensureTempDirectoryExists(tempDirectory);
          File tempFile = new File(tempDirectory, name.substring(prefix.length()));
          if (!tempFile
              .getCanonicalFile()
              .toPath()
              .startsWith(tempDirectory.getCanonicalFile().toPath())) {
            throw new IllegalStateException("Invalid extension " + name);
          }
          if (tempFile.createNewFile()) {
            tempFile.deleteOnExit();
            // ② 将找到的文件拷贝至临时目录下
            extractFile(jarFile, jarEntry, tempFile);
            // ③ 将拷贝后的文件对象映射为 URL 对象添加到扩展列表,
            // 用于后续创建单独的 ExtensionClassLoader
            addFileUrl(extensions, tempFile);
          } else {
            System.err.println("Failed to create temp file " + tempFile);
          }
        }
      }
    } catch (IOException ex) {
      System.err.println("Failed to open embedded extensions " + ex.getMessage());
    }
  }

MultipleParentClassLoader

MultipleParentClassLoader聚合了所有的ExtensionClassLoader对象,其类加载逻辑为依次委托给各Extension Class Loader 对象来完成,直至完成为止;若这些 Extension Class Loader 无法完成类的加载工作,则最终委托给 Bootstrap Class Loader。
MultipleParentClassLoader 的类加载逻辑如下:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // ① 在 opentelemetry javaagent 中,
    // 这里的 parents 指上文代码中传入 ExtensionClassLoader 列表
    for (ClassLoader parent : parents) {
        try {
            Class<?> type = parent.loadClass(name);
            if (resolve) {
                resolveClass(type);
            }
            return type;
        } catch (ClassNotFoundException ignored) {
            /* try next class loader */
        }
    }
    return super.loadClass(name, resolve);
}

类加载器层级

OpenTelemetry javaagent 中的类层级关系如下图所示[1](图片来自官方项目)。
image.png

参考资料

[1] OpenTelemetry Instrumentation for Java, github.com/open-teleme…