Reputation: 1376
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:
AppClassLoader
<- PlatformClassLoader
<- null
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?
Upvotes: 5
Views: 3006
Reputation: 298599
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.
Upvotes: 3
Reputation: 103913
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:
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.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.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:
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).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:
lombok.eclipse.agent
package in the eclipseAgent
source root.lombok.patcher
project which is lombok's only actual dependency, in particular the lombok.patcher.PatchScript.transplantMethod
method.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.
Upvotes: 0
Reputation: 70999
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.
Upvotes: 0