5

Intent:

I'm using the java.lang.instrument package to create some instrumentation for Java programs. The idea is that I use bytecode manipulation via this system in order to add method calls at the beginning and end of each method. Generally speaking, a modified Java method would look like:

public void whateverMethod(){
    MyFancyProfiler.methodEntered("whateverMethod");
    //the rest of the method as usual...
    MyFancyProfiler.methodExited("whateverMethod");
}

MyFancyProfiler is an entry point to a relatively complex system that gets initialized during the premain method (which is part of java.lang.instrument).

edit - MyFancyProfiler contains a static API that will get a reference to the rest of the system through a mechanism like the one described in the solution to this question. The reference is obtained as an Object, and the appropriate calls are made via reflection, so that even if the current ClassLoader doesn't know about the underlying classes, it will still work.

Difficulties

For a simple Java program, the approach works fine. For "real" apps (like windowed applications, and especially RCP/OSGi applications), I ran into issues with ClassLoaders. Some ClassLoaders won't know how to find the MyFancyProfiler class, so it will throw Exceptions when it tries to call the static methods in MyFancyProfiler.

My solution to this (and where my real problem is happening) is currently to "inject" MyFancyProfiler into each encountered ClassLoader by reflectively calling defineClass. The gist of it is:

public byte[] transform(ClassLoader loader, String className, /* etc... */) {
  if(/* this is the first time I've seen loader */){
    //try to look up `MyFancyProfiler` in `loader`.
    if(/* loader can't find my class */){
      // get the bytes for the MyFancyProfiler class
      // reflective call to 
      // loader.defineClass(
      //   "com.foo.bar.MyFancyProfiler", classBytes, 0, classBytes.length);
    }
  }
  // actually do class transformation via ASM bytecode manipulation
}

edit for more info - The reason for this injection is to ensure that every class, no matter which ClassLoader loaded it, will be able to call MyFancyProfiler.methodEntered directly. Once it makes that call, MyFancyProfiler will need to use reflection to interact with the rest of the system, or else I will get an InvocationTargetException or NoClassDef exception when it tries to refer directly. I currently have it working so that the only "direct" dependencies of MyFancyProfiler are JRE system classes, so it seems to be fine.

Problem

This even works! Most of the time! But for at least two separate ClassLoaders that I encounter while trying to trace Eclipse (launching the IDE from the command line), I get a NullPointerException coming from inside the ClassLoader.defineClass method:

java.lang.NullPointerException
    at java.lang.ClassLoader.checkPackageAccess(ClassLoader.java:500)
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:791)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:634)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    // at my code that calls the reflection

Line 500 of ClassLoader.java is a call to domains.add(pd), where domains seems to be a Set that gets initialized at constructor time, and pd is a ProtectionDomain that (as far as I can tell) should be the "default" ProtectionDomain. So I don't see an obvious way for that line to cause a NullPointerException. Currently I'm stumped, and I'm hoping that somebody can offer some insight into this.

What could cause the defineClass to fail in this way? And in case there's no obvious solution, can you offer a potential alternative approach to my overall problem?

Community
  • 1
  • 1
Dylan
  • 13,645
  • 3
  • 40
  • 67
  • This is a research project? if not, why don't you use AspectJ? – DiogoSantana Mar 28 '13 at 21:46
  • Is your profiler being inserted using the -javaagent flag, along with premain defined? – Amir Afghani Mar 28 '13 at 21:47
  • see [http://stackoverflow.com/questions/6750392/implementing-a-selective-classloader] – Łukasz Rzeszotarski Mar 28 '13 at 21:48
  • @DiogoSantana this is for work. I've renamed things so as to remain vague. We started with AspectJ but found that it has this problem and many more. Suffice to say that we cannot and will not use AspectJ. – Dylan Mar 29 '13 at 02:28
  • @AmirAfghani yes, everything works except for this rather specific case of when the target traced application uses many different classloaders. In the case where I encounter the problem, I'm trying to trace Eclipse, and it gives a new ClassLoader for each "bundle" that it loads. My current solution works for probably 90% of the bundles, but produces the exception I mentioned for the remaining 10%. – Dylan Mar 29 '13 at 02:33
  • @ŁukaszRzeszotarski Your link doesn't help me because I am attaching my agent to a (read "any") third party application: I have no control over the ClassLoaders that get passed in to me. – Dylan Mar 29 '13 at 02:35
  • Did you look at the OSGi Byte Code weaving service? This allows you to add imports to the woven bundle so no need to define the class yourself. Also, your code seems to define the MyFancyProfiler in every class loader? Last, could those failing class loaders be the System class loader? There are special security considerations there. – Peter Kriens Mar 29 '13 at 07:40
  • @PeterKriens I will have to take a closer look at the OSGi Byte Code weaving service today while I'm at work, but my gut feeling is that it won't work for me simply because the goal is not to weave "my" code, but to weave "any third party app's" code. Also yes, my code system defines `MyFancyProfiler` in every class loader that can't otherwise find it. It uses code similar to http://stackoverflow.com/questions/2063829/how-can-i-use-java-lang-instrument-in-an-eclipse-rcp-application so that it should be the *only* added dependency. The failing classloaders are `DelegatingClassLoader` instances. – Dylan Mar 29 '13 at 11:33
  • @PeterKriens I used all of the available characters in the last comment.. I've edited my question to give some more details about `MyFancyProfiler` and why I'm adding it to every ClassLoader – Dylan Mar 29 '13 at 11:41

2 Answers2

4

Instead of injecting code to the ClassLoaders, try loading the jar that contains MyFancyProfiler in the bootstrap class-loader. Easiest way to do that is by adding the following line to the manifest of your javaagent jar:

Boot-Class-Path: fancy-profiler-bootstrap-stuff.jar

This will make everything in that jar accessible to all class loaders, including, I believe OSGi and friends.

adaf
  • 258
  • 1
  • 8
0

You're getting NullPointerException because this is null in this case. If you want to load a class using bootstrap classloader (which is null) you need to bypass the security checks by using sun.misc.Unsafe.defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); method with null as a ClassLoader and null as ProtectionDomain.

bedrin
  • 4,458
  • 32
  • 53