Инструментарий байт-кода Java: NullPointerException в рефлексивном вызове defineClass

Намерение:

Я использую java.lang .instrument для создания инструментов для Java-программ. Идея состоит в том, что я использую манипуляции с байт-кодом через эту систему, чтобы добавлять вызовы методов в начале и в конце каждого метода. Вообще говоря, модифицированный метод Java будет выглядеть так:

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

MyFancyProfiler — это точка входа в относительно сложную систему, которая инициализируется во время метода premain (который является частью java.lang.instrument).

редактировать - MyFancyProfiler содержит статический API, который получит ссылку на остальную часть системы с помощью механизма, подобного описанному в решении этого вопроса. Ссылка получается как Object, а соответствующие вызовы выполняются через отражение, так что даже если текущий ClassLoader не знает о базовых классах, он все равно будет работать.

Трудности

Для простой Java-программы этот подход отлично работает. Для «настоящих» приложений (таких как оконные приложения и особенно приложения RCP/OSGi) я столкнулся с проблемами с ClassLoaders. Некоторые ClassLoaders не будут знать, как найти класс MyFancyProfiler, поэтому будут генерировать исключения при попытке вызвать статические методы в MyFancyProfiler.

Мое решение этого (и где происходит моя настоящая проблема) в настоящее время состоит в том, чтобы «внедрить» MyFancyProfiler в каждый встреченный ClassLoader путем рефлексивного вызова defineClass. Суть в следующем:

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
}

отредактируйте для получения дополнительной информации. Причина этой инъекции заключается в том, чтобы гарантировать, что каждый класс, независимо от того, какой ClassLoader его загрузил, сможет напрямую вызывать MyFancyProfiler.methodEntered. Как только он сделает этот вызов, MyFancyProfiler потребуется использовать отражение для взаимодействия с остальной системой, иначе я получу исключение InvocationTargetException или NoClassDef, когда он попытается обратиться напрямую. В настоящее время у меня это работает, так что единственными «прямыми» зависимостями MyFancyProfiler являются системные классы JRE, так что все в порядке.

Проблема

Это даже работает! Большую часть времени! Но по крайней мере для двух отдельных загрузчиков классов, с которыми я столкнулся при попытке отследить Eclipse (запуск IDE из командной строки), я получаю NullPointerException из метода ClassLoader.defineClass:

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

Строка 500 файла ClassLoader.java — это вызов domains.add(pd), где domains кажется набором, который инициализируется во время конструктора, а pd — это ProtectionDomain, который (насколько я могу судить) должен быть "по умолчанию" ProtectionDomain. Поэтому я не вижу очевидного способа, чтобы эта строка вызывала ошибку NullPointerException. В настоящее время я в тупике, и я надеюсь, что кто-то может дать некоторое представление об этом.

Что может привести к тому, что defineClass выйдет из строя таким образом? И в случае отсутствия очевидного решения, можете ли вы предложить потенциальный альтернативный подход к моей общей проблеме?


person Dylan    schedule 28.03.2013    source источник
comment
Это исследовательский проект? если нет, то почему бы вам не использовать AspectJ?   -  person DiogoSantana    schedule 29.03.2013
comment
Вставляется ли ваш профилировщик с использованием флага -javaagent вместе с определенным premain?   -  person Amir Afghani    schedule 29.03.2013
comment
см. [stackoverflow.com/questions/6750392/   -  person Łukasz Rzeszotarski    schedule 29.03.2013
comment
@DiogoSantana это для работы. Я переименовал вещи, чтобы они оставались расплывчатыми. Мы начали с AspectJ, но обнаружили, что у него есть эта проблема и многие другие. Достаточно сказать, что мы не можем и не будем использовать AspectJ.   -  person Dylan    schedule 29.03.2013
comment
@AmirАфгани да, все работает, за исключением этого довольно конкретного случая, когда целевое отслеживаемое приложение использует много разных загрузчиков классов. В случае, когда я сталкиваюсь с проблемой, я пытаюсь отследить Eclipse, и он выдает новый ClassLoader для каждого загружаемого пакета. Мое текущее решение работает, вероятно, для 90% пакетов, но создает исключение, о котором я упоминал, для оставшихся 10%.   -  person Dylan    schedule 29.03.2013
comment
@ŁukaszRzeszotarski Ваша ссылка мне не помогает, потому что я подключаю своего агента к (читай любому) стороннему приложению: я не контролирую ClassLoaders, которые передаются мне.   -  person Dylan    schedule 29.03.2013
comment
Вы смотрели на сервис плетения байт-кода OSGi? Это позволяет вам добавлять импорт в составной пакет, поэтому вам не нужно определять класс самостоятельно. Кроме того, ваш код, похоже, определяет MyFancyProfiler в каждом загрузчике классов? Наконец, могут ли эти неудачные загрузчики классов быть загрузчиками классов System? Там есть особые соображения безопасности.   -  person Peter Kriens    schedule 29.03.2013
comment
@PeterKriens Сегодня, пока я на работе, мне придется поближе познакомиться с сервисом плетения байт-кода OSGi, но я чувствую, что он не сработает для меня просто потому, что цель состоит не в том, чтобы плести мой код, а в том, чтобы сплести код любого стороннего приложения. Также да, моя система кода определяет MyFancyProfiler в каждом загрузчике классов, который иначе не может его найти. Он использует код, аналогичный stackoverflow.com/questions/2063829/, чтобы он был единственной добавленной зависимостью. Неудачные загрузчики классов — это DelegatingClassLoader экземпляра.   -  person Dylan    schedule 29.03.2013
comment
@PeterKriens Я использовал все доступные символы в последнем комментарии. Я отредактировал свой вопрос, чтобы дать более подробную информацию о MyFancyProfiler и о том, почему я добавляю его в каждый ClassLoader.   -  person Dylan    schedule 29.03.2013


Ответы (2)


Вместо того, чтобы вводить код в ClassLoaders, попробуйте загрузить банку, содержащую MyFancyProfiler, в загрузчик классов начальной загрузки. Самый простой способ сделать это — добавить следующую строку в манифест вашего javaagent jar:

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

Это сделает все в этой банке доступным для всех загрузчиков классов, включая, я думаю, OSGi и его друзей.

person adaf    schedule 17.04.2013
comment
Наконец-то я вернулся и попробовал это, и это сработало! Огромное спасибо! - person Dylan; 02.05.2013

Вы получаете NullPointerException, потому что this в этом случае равно нулю. Если вы хотите загрузить класс с помощью загрузчика классов начальной загрузки (то есть null), вам необходимо обойти проверки безопасности, используя метод sun.misc.Unsafe.defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); с null в качестве ClassLoader и null в качестве ProtectionDomain.

person bedrin    schedule 16.10.2020