Using Class.forName() in Java Instrumentation Agent
Asked Answered
T

4

0

What I understand is that if I use:

Instrumentation#getAllLoadedClasses()

I do get a selection of all loaded classes by the target JVM. But If I do:

Class.forName("my.class.name")

This will not be the same class as the class loaded by VM. Yes, I can add this particular class as a jar in the agent MANIFEST.MF Class-Path - but that does not look the same to me as getAllLoadedClasses().

Could someone please confirm whether this is correct i.e. I would not be able to find a specific class using Class.forName() when instrumenting? My objective was not to iterate over all loaded classes using getAllLoadedClasses() - But if there is no alternative, I guess that's okay for now.

** UPDATE

What I made a mistake in writing is the Boot-Class-Path which I have now corrected in my manifest. Using -verbose:class logging I managed to see that my jars are being loaded as

[Opened C:\fullpath\someother.jar]
[Opened C:\fullpath\another.jar]
[Opened C:\fullpath\different.jar]

But I don't see any corresponding loading information. I tried adding a Class.forName("a.package.in.someother.jar.classname") and got NoClassDefFoundError. As soon as I jump into the agent jar, I cannot use Class.forName() to check if the class is loaded by the target VM. I am getting a NoClassDefFoundError.

FURTHER UPDATE

Okay I have "Fattened" the manifest to look up all classes in my WEB-INF/lib and tomcat's lib directory. What I can see is below:

1) When my custom class MyClass is loaded for the first time. -verbose shows:

[Loaded my.pkg.MyClass from file:/C:/base/webapps/ROOT/WEB-INF/lib/mypkg.jar]

2) If I try to load the class again, it is correctly showing the above order.

3) My agent jar is manifested with all classes for my tomcat lib and my web-inf/lib directory. And I can also confirm that the loader sees the jars correctly.

4) Now I inject the agent, and call Class.forName("my.pkg.MyClass") from within the agent class. I get the below results.

[Loaded my.pkg.MyClass from file:/C:/base/webapps/ROOT/WEB-INF/lib/mypkg.jar]

I acknowledge that it's system class loader loding it inside my agent code as @RafaelWinterhalter pointed out in one of his answers. Is there any way I can force a "Delegation" so that the a different classloader loads the agent class and therefore, correctly redefines a class.

Any help is appreciated.

Teetotalism answered 2/10, 2017 at 9:38 Comment(8)
what makes you say that Class.forName("[...]") finds a different class than Instrumentation.getAllLoadedClasses() ? Is your target class not referenced from the classes that are being loaded?Avidity
@Avidity I am saying that they are not found at all ! If I load a class "my.package.MyClass" before injecting the agent - the same class is not visible via Class.forName() inside the agent even if I have added relevant jar to Boot-Class-Path I might be wrong, but there is a fundamental gap in the instrumentation info provided on JDK page. I have raised a bug in the system for them to check.Teetotalism
The fact that instrumentation can observe (and modify) classes as they are being loaded does not mean that its classloader is responsible for loading these classes. Is your application a Web App ?Avidity
@dignoise yes it's a webapp, but I believe this will happen even if I had a single helloworld type application. I think you might have snatched the word just out of my mind. The solution would be to force Instrumentation to use VM's context classLoader?Teetotalism
you could create custom ClassLoader and pass it your CLASSPATHAvidity
Why do you insist on Class.forName(String) having to return the desired class instead of just calling loadClass on the right ClassLoader?Neurogram
@Neurogram it really doesn't matter using Class.forName or loadClass() it goes via the same route - The issue here is that agent is loaded using System class loader - I wanted to use a different classloader - java documentation doesn't say anything about allowing a different classloader to load the instrumentation agent - so i am slightly clueless to what needs to be done here.Teetotalism
You are focusing on the wrong issue. “allowing a different classloader to load the instrumentation agent” is only needed if you insist on using Class.forName. When you call loadClass() on a specific ClassLoader instance, it will use that specific ClassLoader instance, regardless of the caller.Neurogram
A
1

As it is stated in the javadoc:

Invoking this method is equivalent to: Class.forName(className, true, currentLoader) where currentLoader denotes the defining class loader of the current class.

You can also see from the source code that the method is marked @CallerSensitive which means that you get a different result based on the class loader that invokes the method.

When calling Instrumentation::getAllLoadedClasses, the returned array contains classes of any class loader and not only of the current class loader which is the system class loader when running a Java agent. Therefore:

for (Class<?> type : instrumentation.getAllLoadedClasses()) {
  assert type == Class.forName(type.getName());
}

is not generally true.

Amble answered 3/10, 2017 at 19:30 Comment(1)
Thanks - so how could the instrumentation agent loading can be delegated with e.g. ParallelWebappClassLoader which correctly loads all the clasess in my web application? My agent is already in the web application lib directory. Do I need to explicitly load this agent during the webapp startup? That kind of defeats the purpose of "Starting agent after the VM has started".Teetotalism
T
0

After a bit of run around, and Thanks to @Holger who reminded me what the problem was - incorrect class loader.

before I inject the agent, I have done the following:

// Get the current context class loader, which is app ext. classLoader
ClassLoader original = Thread.currentThread().getContextClassLoader().getSystemClassLoader();

// Set the system classloader to app classloader which won't delegate anything
Field scl = ClassLoader.class.getDeclaredFields();
scl.setAccessible(true);
scl.set(null, Thread.currentThread().getContextClassLoader());

// Now inject agent
try {
    vm.loadAgent(agentPath, args);
} catch (all sorts of errors/exceptions in chain) {
// Log them and throw them back up the stack.
} finally {
  vm.detach();
  // Put back the classLoader linkage
  sc.set(null, original);
}

How I have confirmed

  1. When it goes in my Agent Class - Thread.currentThread().getContextClassLoader() becomes my application extn loader. But the system classloader now becomes `ParallelWebappClassLoader".

  2. I am assuming this is how it works, but could be totally worng:

    i) When I say Class.forName("my.pkg") it will check the system class loader which is pointing to my loader now. If the class is not found (i.e. not loaded), it will go to parents etc. I believe this is more or less the delegation model is.

    ii) In this way, the class is loaded in VM by the same class loader which would also load the class in my webapp under normal circumstances.

    iii) I will not instrument anything else apart from my own classes so the classloader will always be the same.

So far I have not seen any LinkageError happening. But I still feel this is too risky and if I break the link I am screwed.

Teetotalism answered 4/10, 2017 at 20:52 Comment(0)
P
0

Using Class.forName in a java profiler must be avoided to escape from the NoClassDef error. JVM Loads the class files in the different level of class loaders based on their classpath setting and the class file demand.

Java Cre libraries + Boot path defended libraries will be loaded in bootstrap Level
Java Agent will be loaded in system level and it goes on. Class.forName() will look the class files from the parent loaders, the current loader will not check the child loader (Until unless we implement our own loaders)

Java core classes will be accessible from your application code but our application code will not be accessible by the Java core classes. Its called class loader hierarchy.

You have three options.

  1. Lookup the class files from the Instrumentation.GetLoadedClassFiles()
  2. Through Transformers you can get all the loaders classes and you can track them and look for your class in every loader until you find.
  3. Have the Class.forname implementation in the lowest level of the hierarchy so that it can internally access all the path.

Maintain the hierarchy properly to avoid too many weird errors.

Pepperandsalt answered 7/12, 2018 at 21:6 Comment(0)
M
0

Assuming you are looking for the Class<?> in order to re-transform a class, it seems to me that you could save the ClassLoader passed to your transformer, and later use ClassLoader.loadClass(String). Something like:

class MyTransformer implements ClassFileTransformer {
    Map<String, ClassLoader> _name2loader = new ...;

    ...

    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain pd,
                            byte[] classfileBuffer) throws ... {
        ...
        _name2loader.put(className.replace("/","."), classLoader);
        ...
    }

    ...

    Class<?> getClass(String name) throws ClassNotFoundException {
        ClassLoader cl = _name2loader.get(name);
        if (cl == null) {
            throw ClassNotFoundException("No loader for class " + name);
        }
        return cl.loadClass(name);
    }
}

Note that the className passed to transform uses slashes, not dots... A better alternative than the String.replace may be to actually read the class name from the classfileBuffer using your bytecode library (such as javaassist or ASM, but if you're transforming bytecode, you're likely already using such a library).

Note: I'm not sure if you'd see the same class being passed for transformation with different ClassLoaders, but it would be good to look out for that (or research it).

Moron answered 23/9, 2022 at 19:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.