- PVSM.RU - https://www.pvsm.ru -
Без сомнения каждый, кто в своем резюме указывает опыт разработки на Java, хоть раз в жизни писал строки
public static void main(String[] args)
компилировал их и запускал на выполнение командой наподобие java HelloWorld
.
Но многие ли знают, что происходит внутри JVM от момента выполнения этой команды до того как управление передается методу main, как Java находит и загружает необходимые пользователю классы? Возникшая однажды производственная задача заставила автора разобраться в этом вопросе. Результаты изысканий под катом. Сразу стоит оговориться, что статья не претендует на полноту охвата всех существующих JVM, тестирование проводилось только на Sun HotSpot JVM.
В один прекрасный день заказчику потребовалось выяснить, какие классы использует его приложение. Приложение было уже хорошо знакомо автору и представляло собой гремучую смесь из кода различной расовой принадлежности, реализующего (к чести разработчиков системы, по большей части грамотно и к месту) механизмы наследования, позднего связывания и динамической компиляции. Поэтому информация о действительно используемых классах могла существенно помочь в рефакторинге приложения.
Задача поставлена следующим образом: в процессе работы приложения должен формироваться файл, содержащий имена всех классов, непосредственно использованных приложением. Оно, к слову, состоит из двух основных частей: сервера приложений, на котором размещен веб-интерфейс приложения, и сервера обработки (отдельный сервер, на котором различные периодические задачи запускаются с помощью скриптов Ant). Разумеется, информацию о классах необходимо собирать с обеих частей приложения.
Приступим к поиску решения поставленной задачи и заодно разберемся с механизмами загрузки классов в Java.
Первым направлением, которое пришло в голову при решении данной задачи, было воспользоваться возможностями расширения механизма загрузки классов в Java. На данную тему написано достаточно много статей, том числе и на русском языке (ссылки в конце статьи).
Суть данного механизма в следующем:
java.lang.ClassLoader
используются для непосредственной загрузки классов, о чем красноречиво свидетельствует сигнатура метода Class loadClass(String name)
. Данный метод должен найти массив байт, являющийся байт-кодом искомого класса и передать его методу protected Class defineClass(String name, byte[] b, int off, int len)
, который превратит его в экземпляр класса java.lang.Class
. Таким образом, реализуя свои загрузчики, разработчики могут загружать из любого места, откуда можно получить массив байт;getParent
класса ClassLoader
. При старте JVM создается вершина этой иерархии из трех основных загрузчиков: базового (Bootstrap Classloader, отвечает за загрузку базовых классов фреймворка), загрузчика расширений (Extension Classloader, отвечает за загрузку классов из lib/ext) и системного загрузчика (System Classloader, отвечает за загрузку пользовательских классов). Далее разработчики вольны продолжать эту иерархию от системного загрузчика и ниже. По умолчанию в HotSpot JVM в качестве системного загрузчика используется класс sun.misc.Launcher$AppClassLoader
однако его можно легко переопределить с помощью системного свойства java.system.class.loader
ключа командной строки java -Djava.system.class.loader=имя.класса.загрузчика
;Однако на данном этапе уже появилась первая концепция решения поставленной задачи:
Для реализации второго пункта необходимо решить следующие задачи:
java.system.class.loader
— из командной строки это можно сделать так: java -Djava.system.class.loader=имя.класса.загрузчика HelloWolrd
-Djava.system.class.loader
. Как оказалось, для этого тоже существует изящное решение — нужно использовать специальную переменную окружения, значение которой автоматически добавляется к параметрам запуска любой JVM. В процессе поиска было найдено две переменные, которые могли бы отвечать за данную возможность: JAVA_OPTS и JAVA_TOOL_OPTIONS. Однако ни одна из статей не давала четкого ответа на вопрос в чем же отличие этих двух переменных? Ответ на данный вопрос было решено найти опытным путем. В ходе эксперимента было установлено, что по настоящему «волшебной» является переменная JAVA_TOOL_OPTIONS, значение которой автоматически добавляется к параметрам запуска любой запускаемой HotSpot JVM. А JAVA_OPTS — это результат негласного соглашения разработчиков различных Java приложений. Данную переменную в явном виде используют многие скрипты (например, startup.sh/startup.bat для запуска Apache Tomcat), однако никто не гарантирует, что данную переменную будут использовать все разработчики скриптов.
Итак, дело сделано, загрузчик скомпилирован и помещен в lib/ext, значение переменной окружения JAVA_TOOL_OPTIONS
задано, запускаем приложение, работаем, открываем лог и видим… скудный список из десятка классов, включая системные и еще несколько сторонних классов. Вот тут-то и пришлось вспомнить о необязательности выполнения правила делегирования загрузки, а так же заглянуть в исходный код Apache Ant и Tomcat. Как оказалось, в этих приложениях используются собственные загрузчики классов. Это, с одной стороны, отчасти и позволило им обрести свой мощный функционал. Однако по тем или иным причинам разработчики этих продуктов решили не придерживаться рекомендованного правила делегирования загрузки и написанные ими загрузчики далеко не всегда обращаются к своим родителям, перед тем как загрузить очередной класс. Именно поэтому наш системный загрузчик почти ничего не знает о классах, загружаемых Tomcat-ом и Ant-ом.
Таким образом, описанный способ не позволяет отловить все требуемые классы, особенно учитывая разнообразие используемых серверов приложений — кто знает, как отнеслись к правилу делегирования загрузки разработчики используемого заказчиком сервера приложений.
Порой для решения задачи не достаточно одних знаний или умений. Иногда для достижения цели необходима интуиция и чуточку везения. Сейчас автор уже и не вспомнит, в ответ на какой поисковый запрос о загрузчиках классов, поисковый гигант выдал ссылку на статью о механизме инструментации классов. Как оказалось, данный инструмент предназначен для изменения байт кода Java классов во время их загрузки (к примеру, JProfiler с помощью данного механизма встраивается в классы для замеров производительности). Стоп, что значит во время их загрузки? То есть данный механизм знает о каждом загруженном классе? Да знает, и, как оказалось, даже лучше чем загрузчики классов — метод byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
интерфейса ClassFileTransformer
вызывается у реализующего его класса-трансформатора при загрузке любого класса. Этот метод и оказался тем бутылочным горлышком, через которое проходит любой загружаемый класс, за исключением совсем небольшого количества системных.
Теперь задача сводится к следующему:
ClassFileTransformer.transform
, который, правда, не будет осуществлять никакой трансформации, а будет всего лишь записывать имя загруженного класса в файл.Исходный код класса-трансформатора представлен ниже:
package com.test;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.lang.instrument.IllegalClassFormatException;
public class LoggingClassFileTransformer implements ClassFileTransformer {
public static void premain(String agentArguments, Instrumentation instrumentation) {
instrumentation.addTransformer(new LoggingClassFileTransformer());
}
/**
* Данный метод вызывается для любого загруженного класса
* @param className имя загружаемого класса для записи в лог
* @return неизмененный classfileBuffer содержащий байт-код класса
*/
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException
{
log(className);
return classfileBuffer;
}
// сохраняем лог в папку lib/ext
private static final File logFile = new File(System.getProperty("java.ext.dirs").split(Character.toString(File.pathSeparatorChar))[0]+"/LoggingClassFileTransformer.log");
public static synchronized void log(String text)
{
// запись в файл
// ...
}
}
Здесь необходимо пояснить механизм использования классов-трансформаторов. Чтобы подключить такой класс к приложению нам понадобиться так называемый premain класс, т.е. класс, содержащий метод public static void premain(String paramString, Instrumentation paramInstrumentation)
. Из названия метода понятно, что он вызывается до вызова метода main. В этот момент можно подключить к приложению классы-трансформаторы с помощью метода addTransformer
интерфейса java.lang.instrument.Instrumentation
. Таким образом, приведенный выше класс одновременно является и классом-трансформатором и premain-классом. Чтобы данный класс можно было использовать, его необходимо поместить в JAR файл, манифест которого (файл META-INF/MANIFEST.MF) содержит параметр Premain-Class, указывающий на полное имя premain-класса, в нашем случае Premain-Class: com.test.LoggingClassFileTransformer
. Затем необходимо указать полный путь к данному архиву с помощью параметра -javaagent
при запуске JVM. Тут нам на помощь снова приходит переменная JAVA_TOOL_OPTIONS.
Итак, класс написан, скомпилирован, упакован вместе с манифестом в JAR, переменная окружения JAVA_TOOL_OPTIONS=-javaagent:"путь к LoggingClassFileTransformer.jar"
задана, приложение запущено, лог собран, PROFIT!
Итак, какие выводы можно сделать после окончания работы над проектом:
Статьи о загрузчиках классов в Java:
Другие полезные ссылки:
Автор: Askell
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/5179
Ссылки в тексте:
[1] habrahabr.ru/post/103830/: http://habrahabr.ru/post/103830/
[2] voituk.kiev.ua/2008/01/14/java-plugins/: http://voituk.kiev.ua/2008/01/14/java-plugins/
[3] blogs.oracle.com/vmrobot/entry/%D0%BE%D1%81%D0%BD%D0%BE%D0%B2%D1%8B_%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9_%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B8_%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%B2_%D0%B2: https://blogs.oracle.com/vmrobot/entry/%D0%BE%D1%81%D0%BD%D0%BE%D0%B2%D1%8B_%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9_%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B8_%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%B2_%D0%B2
[4] stackoverflow.com/questions/3933300/difference-between-java-opts-and-java-tool-options: http://stackoverflow.com/questions/3933300/difference-between-java-opts-and-java-tool-options
[5] docs.oracle.com/javase/1.4.2/docs/api/java/lang/ClassLoader.html#getSystemClassLoader: http://docs.oracle.com/javase/1.4.2/docs/api/java/lang/ClassLoader.html#getSystemClassLoader
[6] www.sql.ru/forum/actualthread.aspx?tid=858652: http://www.sql.ru/forum/actualthread.aspx?tid=858652
[7] www.exampledepot.com/egs/java.lang/PropCmdLine.html: http://www.exampledepot.com/egs/java.lang/PropCmdLine.html
[8] docs.oracle.com/javase/1.4.2/docs/guide/extensions/spec.html: http://docs.oracle.com/javase/1.4.2/docs/guide/extensions/spec.html
[9] www.javalobby.org/java/forums/t19309.html: http://www.javalobby.org/java/forums/t19309.html
[10] today.java.net/pub/a/today/2008/04/24/add-logging-at-class-load-time-with-instrumentation.html: http://today.java.net/pub/a/today/2008/04/24/add-logging-at-class-load-time-with-instrumentation.html
[11] java.decompiler.free.fr: http://java.decompiler.free.fr
Нажмите здесь для печати.