理解 Java类加载器与Agent字节码插桩中的类加载问题

zhidiantech · · 246 次点击 · · 开始浏览    

引言

在 Java 编程中,理解类加载器的机制对开发复杂的应用和工具非常重要,尤其是在涉及 Java Agent 这样高级技术时更是如此。在本文中,我们将深入探讨 Java 类加载器的工作机制,并通过一个实际的 Java Agent 示例来展现如何解决类加载的问题。

类加载器概述

Java 的类加载器是负责将类文件加载到 JVM 中的组件。类加载器的工作分为三个主要的过程:加载、链接和初始化。类加载器通常按以下层次结构工作:

  1. 引导类加载器(Bootstrap ClassLoader):JVM 自带的类加载器,用于加载核心类库如 java.lang.*
  2. 扩展类加载器(Extension ClassLoader):加载扩展目录中的类库
  3. 应用类加载器(App ClassLoader):加载应用的类路径中的类
  4. 自定义类加载器:由开发者自定义的类加载器,可以实现一些特殊的类加载机制

双亲委派模型

Java 的类加载器遵循“双亲委派模型”,即一个类加载器在加载类时,会首先将该请求委托给父类加载器进行加载。如果父类加载器找不到该类,才会由自身进行加载。这种机制确保了核心类库不会被篡改。

父类加载器与子类加载器的可见性

  1. 父类加载器加载的类可被子类加载器访问:父类加载器加载的类对所有子类加载器都是可见的。
  2. 子类加载器加载的类不可直接被父类加载器访问:父类加载器无法直接引用子类加载器加载的类。

以下是一个示例代码,展示类加载器的基本行为:

public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("System ClassLoader: " + systemClassLoader);

        ClassLoader customClassLoader = new CustomClassLoader(systemClassLoader);
        System.out.println("Custom ClassLoader: " + customClassLoader);

        // 使用自定义类加载器加载类
        Class<?> clazz = customClassLoader.loadClass("test.CustomClass");
        System.out.println("CustomClass's ClassLoader: " + clazz.getClassLoader());
        
        // 使用系统类加载器加载同一个类会失败
        try {
            Class<?> systemClass = systemClassLoader.loadClass("test.CustomClass");
            System.out.println("CustomClass loaded by System ClassLoader: " + systemClass);
        } catch (ClassNotFoundException e) {
            System.out.println("CustomClass cannot be loaded by System ClassLoader.");
        }
    }
}

class CustomClassLoader extends ClassLoader {
    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (name.equals("test.CustomClass")) {
            byte[] bytes = {/* 这里是类的字节码 */};
            return defineClass(name, bytes, 0, bytes.length);
        }
        return super.findClass(name);
    }
}

在 Java Agent 中使用类加载器

在实际开发中,特别是当涉及到 Java Agent 时,我们需要对类加载器有更多控制。在我们的示例中,我们希望在 Thread.start() 方法中插入代码,打印出新线程的名称以及调用栈信息。

问题描述

当我们进行字节码插桩时,Thread 类是由引导类加载器加载的。而通常我们定义的类(如 StackTracePrinter)是由应用类加载器加载的。从而在 Thread 类中无法直接访问 StackTracePrinter 类。

解决方案:使用 appendToBootstrapClassLoaderSearch

我们可以通过 Instrumentation 提供的 appendToBootstrapClassLoaderSearch 方法,将包含 StackTracePrinter 类的 JAR 文件添加到引导类加载器的搜索路径中。这样,Thread 类就可以访问 StackTracePrinter 类,从而解决类加载问题。

实现步骤

以下是一个完整的例子,展示了如何实现这个需求。

项目结构

project/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/
│       │       ├── AgentMain.java
│       │       ├── StackTracePrinter.java
│       │       └── ThreadClassFileTransformer.java
│       └── resources/
│           └── META-INF/
│               └── MANIFEST.MF
├── pom.xml

pom.xml

确保 Maven 配置正确:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>thread-agent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.2</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>9.2</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.example.AgentMain</mainClass>
                                    <manifestEntries>
                                        <Premain-Class>com.example.AgentMain</Premain-Class>
                                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

AgentMain.java

package com.example;

import java.lang.instrument.Instrumentation;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.net.URISyntaxException;
import java.util.jar.JarFile;

public class AgentMain {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Agent started");

        // 将当前 Agent JAR 添加到引导类加载器路径中
        try {
            Path agentJarPath = Paths.get(AgentMain.class.getProtectionDomain().getCodeSource().getLocation().toURI());
            inst.appendToBootstrapClassLoaderSearch(new JarFile(agentJarPath.toFile()));
        } catch (Exception e) {
            e.printStackTrace();
            return; // 如果发生异常,直接返回
        }

        inst.addTransformer(new ThreadClassFileTransformer(), true);

        try {
            Class<?> threadClass = Class.forName("java.lang.Thread");
            System.out.println("Thread class's class loader: " + threadClass.getClassLoader());
            inst.retransformClasses(threadClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

StackTracePrinter.java

提供一个静态方法来打印当前线程和新线程的名字以及调用栈信息:

package com.example;

public class StackTracePrinter {
    public static void printCurrentThreadStackTrace(Thread newThread) {
        Thread currentThread = Thread.currentThread();
        System.out.println("Current thread name: " + currentThread.getName());
        System.out.println("New thread name: " + newThread.getName());
        
        System.out.println("Call Stack:");
        StackTraceElement[] stackTraceElements = currentThread.getStackTrace();
        for (StackTraceElement element : stackTraceElements) {
            System.out.println(element.toString());
        }
    }
}

ThreadClassFileTransformer.java

我们将 Thread.start 方法进行字节码插桩:

package com.example;

import org.objectweb.asm.*;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class ThreadClassFileTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if (className != null && className.equals("java/lang/Thread")) {
            System.out.println("Transforming java/lang/Thread with class loader: " + loader);
            return transformThreadClass(classfileBuffer);
        }
        return null;
    }

    private byte[] transformThreadClass(byte[] classfileBuffer) {
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        cr.accept(new ClassVisitor(Opcodes.ASM9, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
                if ("start".equals(name) && "()V".equals(descriptor)) {
                    return new MethodVisitor(Opcodes.ASM9, mv) {
                        @Override
                        public void visitCode() {
                            super.visitCode();
                            // 插入调用 StackTracePrinter.printCurrentThreadStackTrace(this) 的指令
                            mv.visitVarInsn(Opcodes.ALOAD, 0); // 加载 this,即当前线程对象
                            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/StackTracePrinter", "printCurrentThreadStackTrace", "(Ljava/lang/Thread;)V", false);
                        }
                    };
                }
                return mv;
            }
        }, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }
}

MANIFEST.MF

src/main/resources/META-INF/MANIFEST.MF 中添加以下内容:

Manifest-Version: 1.0
Premain-Class: com.example.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

测试应用程序 (TestApplication.java)

public class TestApplication {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Running in new thread")).start();

        // 多启动几个线程进行测试
        new Thread(() -> System.out.println("Another thread running")).start();
        new Thread(() -> System.out.println("Yet another thread running")).start();
    }
}

使用 Maven 构建项目

在 IDEA 中选择 View > Tool Windows > Terminal 打开终端,然后运行以下命令:

mvn clean package

生成 target/thread-agent-1.0-SNAPSHOT.jar 文件。

运行应用程序并附加 Java Agent

在 IDEA 中创建一个新的 Run/Debug 配置:

  1. 打开 Run > Edit Configurations...
  2. 点击 +,选择 Application
  3. 设置 Name 为 TestApplication with Agent
  4. 设置 Main class 为 TestApplication
  5. 在 VM options 中,添加 -javaagent:<path-to-jar>/thread-agent-1.0-SNAPSHOT.jar

<path-to-jar> 替换为前面生成的 JAR 文件的实际路径(例如 target/thread-agent-1.0-SNAPSHOT.jar)。

运行配置

点击 Run 按钮运行 TestApplication with Agent 配置。你应该可以看到为新线程分配的线程名以及当前线程的名字和调用栈信息,类似如下的日志输出:

Agent started
Thread class's class loader: null
Transforming java/lang/Thread with class loader: null
Current thread name: main
New thread name: Thread-0
Call Stack:
java.lang.Thread.getStackTrace(Thread.java:1660)
java.lang.Thread.start(Thread.java:717)
com.example.TestApplication.lambda$main$0(TestApplication.java:4)
com.example.TestApplication$$Lambda$1.run(Unknown Source)
Running in new thread
Current thread name: main
New thread name: Thread-1
Call Stack:
java.lang.Thread.getStackTrace(Thread.java:1660)
java.lang.Thread.start(Thread.java:717)
com.example.TestApplication.lambda$main$1(TestApplication.java:7)
com.example.TestApplication$$Lambda$2.run(Unknown Source)
Another thread running
Current thread name: main
New thread name: Thread-2
Call Stack:
java.lang.Thread.getStackTrace(Thread.java:1660)
java.lang.Thread.start(Thread.java:717)
com.example.TestApplication.lambda$main$2(TestApplication.java:8)
com.example.TestApplication$$Lambda$3.run(Unknown Source)
Yet another thread running

总结

通过这篇文章,我们了解了 Java 类加载器的工作机制以及如何在实际项目中应用这些知识,特别是在涉及到 Java Agent 时。使用 appendToBootstrapClassLoaderSearch 方法,我们可以确保引导类加载器可以访问自定义的类,从而解决类加载的问题。希望这篇文章对你理解类加载器和 Java Agent 有所帮助。

246 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传