Jonathan S. Fisher
Jonathan S. Fisher

Reputation: 8837

Adding a class to the class path with a Java Agent

I'm using a Java Agent and Javassist to add some logging to some JDK classes. Essentially when the system loads some TLS classes, Javassist will add some additional bytecode to them to help me debug some connection problems.

Here's the problem, given this class is included in the agent jar:

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

In my agent's transform method, let's say I tried to invoke that class using javassist:

// 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();

You think that would work, but instead I get this:

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)

Is there any way to add that class [MyAgentPrinter] to the application's class path?

Upvotes: 6

Views: 3750

Answers (1)

Holger
Holger

Reputation: 298233

Your Agent’s jar file is already added to the class path, as specified by 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.

This is the reason why Javassist can find the Agent’s classes when you are transforming the byte code.

The problem seems to be that you are transforming a sun.** class which is likely loaded by the bootstrap loader or extension loader. The standard class loading delegation is
application loader → extension loader → bootstrap loader, so classes available to the application loader are not available to classes loaded by the extension or bootstrap loader.

So, to make them available to all classes, you have to add the Agent’s classes to the bootstrap loader:

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
    }
}

It’s critical to do this before any other action, i.e. before the classes you want to make available to instrumented code have been loaded. This implies that the Agent class itself, i.e. the class containing the premain method can not get accessed by the instrumented code. The Agent class also shouldn’t have direct references to MyAgentPrinter to avoid unintended early loading.

A more reliable way is to add a Boot-Class-Path entry to the Agent jar’s manifest, see the “Manifest Attributes” section of the package documentation, so that the entry gets added before the Agent starts. But then, the name of the jar file must not change afterwards.

Upvotes: 8

Related Questions