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?
CodePudding user response:
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.
CodePudding user response:
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.
CodePudding user response:
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 calledProgramAddonMethods
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 toProgram
. As you do so, modify the typename on all accesses (bothINVOKESTATIC
and the read/write field opcodes) where the etypename isProgramAddonMethods
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. astatic IdentityHashMap<Foo, String> names;
is effectively identical to addingprivate String name;
to theFoo
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:
- The
lombok.eclipse.agent
package in theeclipseAgent
source root. - The
lombok.patcher
project which is lombok's only actual dependency, in particular thelombok.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.