5

I'm using a Java Agent (Agent.class) to transform a method in a program (Program.class) in a way that includes a call to the Agent class.

public Program.getMultiplier()F:
    ALOAD 1
    ALOAD 2
    FDIV
+   INVOKESTATIC Agent.getCustomMultiplier(F)F
    FRETURN

I've inspected the class loaders and their parents of both Agent and Program classes, and their hierarchy looks like this:

  • Agent.class: AppClassLoader <- PlatformClassLoader <- null
  • Program.class: URLClassLoader <- PlatformClassLoader <- null

When the Program executes the added INVOKESTATIC instruction, it throws a ClassNotFoundException -- it cannot find the Agent class as it was loaded by a different class loader.

As a temporary solution, I've tried forcing AppClassLoader to become a parent of URLClassLoader with reflection, which works in older Java versions but has been removed since Java 12.

Is there a more reliable way to make sure my Agent class is visible from any class loader?

Holger
  • 285,553
  • 42
  • 434
  • 765
user7401478
  • 1,372
  • 1
  • 8
  • 25
  • ClassLoaders with modular Java changed, giving the feeling of: _snakess in the grass!_ An other way, like static jar patching would be better. – Joop Eggen Dec 13 '21 at 14:15

3 Answers3

3

You can add classes to the bootstrap class loader using appendToBootstrapClassLoaderSearch. This makes the classes of the specified jar file available to all classes whose defining class loader follows the standard delegation pattern.

But this requires the classes to be packaged in a jar file. When you specify the Agent’s own jar file, you have to be aware that classes loaded through the bootstrap loader are distinct from the classes loaded through the app loader, even when they originate from the same jar file. Further, the classes loaded by the bootstrap loader must not have dependencies to classes loaded by by other class loaders.

If your getCustomMultiplier method is supposed to interact with the running Agent, you have to separate the agent and the class containing this method.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks, looks like this is the right approach, however I get this message when appending to bootstrap class loader search: `OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended`. Everything is working correctly except this message gets printed. Anything to worry about? – user7401478 Dec 13 '21 at 16:53
  • 2
    This message is just about the [class data sharing](https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html) optimization. It implies that if you have added application classes to such an archive, they won’t get the optimized initialization in this session. – Holger Dec 13 '21 at 16:59
  • Does a Map as a field in the class that gets loaded by the bootstrap loader qualify as a dependency to other classes? I'm using this to call any method from an INVOKEDYNAMIC that gets bootstrapped here to bypass the classloader visibility, given that this one class is now visible everywhere. – user7401478 Dec 13 '21 at 17:11
  • 3
    `Map`, `String`, and `CallSite` are all classes loaded through the bootstrap loader, hence, no problem. It’s important then, to consider the last sentence of my answer. This helper class owner the map must be different from the agent class and it must be added before the Agent first accesses it, to ensure that the version used by the Agent and the one used by the instrumented code is the same, i.e. the one resolved through the bootstrap loader. This works best, when the agent class and the helper class are also in different jar files. – Holger Dec 13 '21 at 17:16
0

Have your Agent listen to the creation of new ClassLoaders and then attach instances of them to the new ClassLoaders.

This way you preserve your "Agent listens to ClassLoader" interface, even if it now extends beyond the one platform class loader you expected the Agent to listen to.

Edwin Buck
  • 69,361
  • 7
  • 100
  • 138
  • Would it be possible to somehow load a synthetic class into the platform- or some other class loader that's a parent of all class loaders? This way if a class is not found, it will eventually be located when the class loader looks up its parent. – user7401478 Dec 13 '21 at 14:29
0

You may be able to do something specific that works for URLClassLoader, but not all classes are loaded by an instance of URLClassLoader. Any OSGi project won't, most web servers also use their own classloaders in order to support hot reload, etc.

As far as I know there's no way to just casually update some 'global parent of all classloaders' or inject one; there's no such parent, and even if there was, a classloader is free to ignore its parent entirely.

Therefore the general answer is: No, you can't do that.

But, let's get our hacking hats on!

You're an agent already. One of the things you get to do as agent is to 'witness' classes as they are being loaded. Just invoke .addTransformer on the instance of Instrumentation you get in your agentmain and register one.

When you notice the Program class being loaded, do the following:

  • Take the bytecode and toss it through ASM, BCEL, Bytecode Buddy, or any other java 'class file reader/transformer' framework.
  • Also open up a class from within your agent's code (I wouldn't use Agent itself, I'd make a class called ProgramAddonMethods or whatnot as a container - everything inside is for the program to use / for your agent to 'inject' into that program.
  • Add every static member in ProgramAddonMethods directly to Program. As you do so, modify the typename on all accesses (both INVOKESTATIC and the read/write field opcodes) where the etypename is ProgramAddonMethods and make it the fully qualified name of the targeted class instead.
  • inject the INVOKESTATIC as you already do, but, rewrite it so that it's going to its own class, as you just copied all the static methods and fields over there.
  • Then return the bytecode of that modified class from your transformer.

This 100% guarantees you cannot possibly run into any module or classpath boundary issues and it will work with any classloader abstraction, guaranteed, but there are some caveats:

  • Just don't attempt to futz with instance anything. Make it all static methods and fields. You can make fake instance fields using an IdentityHashMap if you must (e.g. a static IdentityHashMap<Foo, String> names; is effectively identical to adding private String name; to the Foo class.. except it's a bit slower of course; presumably as you're already in a mess o reflection that's acceptable here).
  • Your code has to be 'dependency free'. It cannot rely on anything else, no libraries other than java.*, not even a helper class. This idea quickly runs out of steam if the job you're injecting becomes complicated. If you must, make a classloader for your own agent jar using the appropriate 'thread-safely initialize it only once' guards, and have that load in a bundle that does have the benefit of allowing dependencies.

This is all highly complicated stuff but you appear to have already worked out how to inject INVOKESTATIC calls, so, I think you know how to do this.

This is precisely what lombok does to 'patch' some methods in eclipse to ensure that things like save actions, auto-formatting, and syntax highlighting don't break - lombok injects knowledge of generated notes where appropriate and does it in this exact manner because eclipse uses a classloader platform called Equinox which makes any other solution problematic. You can look at it for inspiration or guidelines, though it's not particularly well documented. You're looking in particular at:

Note that the next method may also interest you: lombok.patcher's 'insert' doesn't move the method - it injects the body of the method directly in there (it 'inlines'). This requires some serious finagling of the stack and is only advised for extremely simple one-liner-esque methods, and probably is excessive and unneccessary firepower for this problem.

DISCLAIMER: I wrote most of that.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Thanks for a detailed answer. I've omitted some details for the sake of making the question simpler, but the method I'm injecting is not known at compile time, it could be any method with any signature. What I'm doing is injecting an INVOKEDYNAMIC call with a bootstrap method in my Agent class, which returns a CallSite from a map. The problem is more about how to make this Agent class always accessible from any other class. – user7401478 Dec 13 '21 at 15:01
  • You could use the trick described in this answer, but, inject a method that creates a new classloader, initialized with your agent's jar (push in your own jar's path, you can figure it out with `MyType.class.getResource("MyType.class")` and 'parse out' the string this gives you). Then fix the fact that this is dog slow by saving this in a static volatile field with double checked locking so you only do it once. – rzwitserloot Dec 13 '21 at 15:48