使用 Java 代理将 class 添加到 class 路径

Adding a class to the class path with a Java Agent

我正在使用 Java 代理和 Javassist 向某些 JDK classes 添加一些日志记录。本质上,当系统加载一些 TLS classes 时,Javassist 会向它们添加一些额外的字节码,以帮助我调试一些连接问题。

这是问题所在,鉴于此 class 包含在代理 jar 中:

package com.something.myagent;
public class MyAgentPrinter {
    public static final void sayHello() {
        System.out.println("Hello!");
    }
}

在我的代理的转换方法中,假设我尝试使用 javassist 调用 class:

// this is only called for sun.security.ssl.Handshaker
ClassPool cp = getClassPool(classfileBuffer, className);
CtClass cc = cp.get(className);
CtMethod declaredMethod = cc.getDeclaredMethod("calculateKeys");
declaredMethod.insertAfter("com.something.myagent.MyAgentPrinter.sayHello();");
cc.freeze();
return cc.toBytecode();

你认为这行得通,但我得到的是:

java.lang.NoClassDefFoundError: com/something/myagent/MyAgentPrinter
    at sun.security.ssl.Handshaker.printLogLine(Handshaker.java)
    at sun.security.ssl.Handshaker.calculateKeys(Handshaker.java:1160)
    at sun.security.ssl.ServerHandshaker.processMessage(ServerHandshaker.java:292)

有什么方法可以将 class [MyAgentPrinter] 添加到应用程序的 class 路径中吗?

您的代理的 jar 文件已添加到 class 路径,如 the java.lang.instrument package documentation 所指定:

The agent class will be loaded by the system class loader (see ClassLoader.getSystemClassLoader). This is the class loader which typically loads the class containing the application main method. The premain methods will be run under the same security and classloader rules as the application main method.

这就是Javassist在你转换字节码的时候能找到Agent的classes的原因

问题似乎是您正在转换一个 sun.** class,它可能由 bootstrap 加载器或扩展加载器加载。标准的class加载委托是
application loader → extension loader → bootstrap loader,因此应用程序加载器可用的 classes 不适用于扩展或 bootstrap 加载器加载的 classes。

因此,要让所有 classes 都可以使用它们,您必须将 Agent 的 classes 添加到 bootstrap 加载器:

public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) throws IOException {
        JarURLConnection connection = (JarURLConnection)
            MyAgent.class.getResource("MyAgent.class").openConnection();
        inst.appendToBootstrapClassLoaderSearch(connection.getJarFile());

        // proceed
    }
}

在任何其他操作之前执行此操作至关重要,即在加载要提供给检测代码的 classes 之前。这意味着代理 class 本身,即包含 premain 方法的 class 无法被检测代码访问。代理 class 也不应该直接引用 MyAgentPrinter 以避免意外的提前加载。

一种更可靠的方法是在代理 jar 的清单中添加一个 Boot-Class-Path 条目,请参阅 the “Manifest Attributes” section of the package documentation,以便在代理启动之前添加该条目。但是,之后jar文件的名字一定不能改。