Руководство по Java Instrumentation
1. Вступление
В этом руководстве мы поговорим оJava Instrumentation API.. Он предоставляет возможность добавлять байтовый код в существующие скомпилированные классы Java.
Мы также поговорим о Java-агентах и о том, как мы используем их для инструментария нашего кода.
2. Настроить
В этой статье мы будем создавать приложение с помощью инструментов.
Наше приложение будет состоять из двух модулей:
-
Приложение для банкомата, которое позволяет нам снимать деньги
-
И Java-агент, который позволит нам измерять производительность нашего банкомата, измеряя затраченное время и деньги
Агент Java изменит байт-код банкомата, что позволит нам измерить время снятия средств без необходимости изменять приложение банкомата.
Наш проект будет иметь следующую структуру:
com.example.instrumentation
base
1.0.0
pom
agent
application
Прежде чем углубляться в детали инструментария, давайте посмотрим, что такое Java-агент.
3. Что такое Java-агент
В общем, java-агент - это просто специально созданный jar-файл. It utilizes the Instrumentation API that the JVM provides to alter existing byte-code that is loaded in a JVM.с
Чтобы агент работал, нам нужно определить два метода:
-
premain - статически загружает агент с помощью параметра -javaagent при запуске JVM
-
agentmain - динамически загружает агент в JVM, используяJava Attach API
Следует иметь в виду, что реализация JVM, такая как Oracle, OpenJDK и другие, может предоставить механизм для динамического запуска агентов, но это не является обязательным требованием.
Во-первых, давайте посмотрим, как мы будем использовать существующий агент Java.
После этого мы рассмотрим, как создать его с нуля, чтобы добавить необходимые функции в наш байт-код.
4. Загрузка Java-агента
Чтобы иметь возможность использовать агент Java, мы должны сначала загрузить его.
У нас есть два типа нагрузки:
-
static - используетpremain для загрузки агента с помощью параметра -javaagent
-
динамический - используетagentmain для загрузки агента в JVM с помощьюJava Attach API
Далее мы рассмотрим каждый тип нагрузки и объясним, как это работает.
4.1. Статическая нагрузка
Загрузка агента Java при запуске приложения называется статической загрузкой. Static load modifies the byte-code at startup time before any code is executed.с
Имейте в виду, что статическая загрузка использует методpremain, который запускается до запуска любого кода приложения, чтобы запустить его, мы можем выполнить:
java -javaagent:agent.jar -jar application.jar
Важно отметить, что мы всегда должны ставить спараметр -javaagent перед спараметром -jar .
Ниже приведены журналы для нашей команды:
22:24:39.296 [main] INFO - [Agent] In premain method
22:24:39.300 [main] INFO - [Agent] Transforming class MyAtm
22:24:39.407 [main] INFO - [Application] Starting ATM application
22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units!
22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
Мы можем видеть, когда запускался методpremain и когда был преобразован классMyAtm . Мы также видим два журнала транзакций снятия наличных в банкоматах, в которых указано время, необходимое для завершения каждой операции.
Помните, что в нашем исходном приложении у нас не было этого времени завершения транзакции, оно было добавлено нашим Java-агентом.
4.2. Динамическая нагрузка
The procedure of loading a Java agent into an already running JVM is called dynamic load. Агент прикрепляется с помощьюJava Attach API.
Более сложный сценарий - это когда у нас уже работает наше приложение ATM, и мы хотим динамически добавлять общее время транзакций без простоев нашего приложения.
Для этого напишем небольшой фрагмент кода и назовем этот классAgentLoader. . Для простоты поместим этот класс в файл jar приложения. Таким образом, файл jar нашего приложения может как запустить наше приложение, так и присоединить нашего агента к приложению ATM:
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
Теперь, когда у нас естьAgentLoader, мы запускаем наше приложение, следя за тем, чтобы в течение десятисекундной паузы между транзакциями мы динамически подключили наш Java-агент с помощьюAgentLoader.
Давайте также добавим клей, который позволит нам либо запустить приложение, либо загрузить агент.
Мы назовем этот классLauncher, и он будет нашим основным классом файла jar:
public class Launcher {
public static void main(String[] args) throws Exception {
if(args[0].equals("StartMyAtmApplication")) {
new MyAtmApplication().run(args);
} else if(args[0].equals("LoadAgent")) {
new AgentLoader().run(args);
}
}
}
Запуск приложения
java -jar application.jar StartMyAtmApplication
22:44:21.154 [main] INFO - [Application] Starting ATM application
22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!
Присоединение Java-агента
После первой операции мы присоединяем Java-агент к нашей JVM:
java -jar application.jar LoadAgent
22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575
22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully
Проверить журналы приложений
Теперь, когда мы подключили нашего агента к JVM, мы увидим, что у нас есть общее время завершения для второй операции снятия средств через банкомат.
Это означает, что мы добавили нашу функциональность на лету, когда наше приложение работало:
22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method
22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm
22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
5. Создание Java-агента
Узнав, как использовать агента, давайте посмотрим, как мы можем его создать. Мы посмотрим наhow to use Javassist, чтобы изменить байт-код, и объединим это с некоторыми методами API инструментария.
Поскольку java-агент используетJava Instrumentation API, прежде чем углубляться в создание нашего агента, давайте рассмотрим некоторые из наиболее часто используемых методов в этом API и краткое описание того, что они делают:
-
addTransformer - добавляет трансформатор в инструментальный движок
-
getAllLoadedClasses - возвращает массив всех классов, загруженных в данный момент JVM
-
retransformClasses - упрощает оснащение уже загруженных классов путем добавления байт-кода
-
removeTransformer - отменяет регистрацию поставленного трансформатора
-
redefineClasses - переопределить предоставленный набор классов, используя предоставленные файлы классов, что означает, что класс будет полностью заменен, а не изменен, как сretransformClasses
5.1. Создайте методыPremain иAgentmain
Мы знаем, что каждому Java-агенту нужен хотя бы один из методовpremain илиagentmain. Последний используется для динамической загрузки, тогда как первый используется для статической загрузки Java-агента в JVM.
Давайте определим их обоих в нашем агенте, чтобы мы могли загружать этот агент как статически, так и динамически:
public static void premain(
String agentArgs, Instrumentation inst) {
LOGGER.info("[Agent] In premain method");
String className = "com.example.instrumentation.application.MyAtm";
transformClass(className,inst);
}
public static void agentmain(
String agentArgs, Instrumentation inst) {
LOGGER.info("[Agent] In agentmain method");
String className = "com.example.instrumentation.application.MyAtm";
transformClass(className,inst);
}
В каждом методе мы объявляем класс, который хотим изменить, а затем копаемся, чтобы преобразовать этот класс с помощью методаtransformClass.
Ниже приведен код методаtransformClass, который мы определили, чтобы помочь нам преобразовать классMyAtm.
В этом методе мы находим класс, который хотим преобразовать, и используем методtransform . Кроме того, мы добавляем трансформатор в инструментальный движок:
private static void transformClass(
String className, Instrumentation instrumentation) {
Class> targetCls = null;
ClassLoader targetClassLoader = null;
// see if we can get the class using forName
try {
targetCls = Class.forName(className);
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
} catch (Exception ex) {
LOGGER.error("Class [{}] not found with Class.forName");
}
// otherwise iterate all loaded classes and find what we want
for(Class> clazz: instrumentation.getAllLoadedClasses()) {
if(clazz.getName().equals(className)) {
targetCls = clazz;
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
}
}
throw new RuntimeException(
"Failed to find class [" + className + "]");
}
private static void transform(
Class> clazz,
ClassLoader classLoader,
Instrumentation instrumentation) {
AtmTransformer dt = new AtmTransformer(
clazz.getName(), classLoader);
instrumentation.addTransformer(dt, true);
try {
instrumentation.retransformClasses(clazz);
} catch (Exception ex) {
throw new RuntimeException(
"Transform failed for: [" + clazz.getName() + "]", ex);
}
}
Разобравшись с этим, давайте определим трансформатор для классаMyAtm.
5.2. Определение нашегоTransformer
Преобразователь класса должен реализоватьClassFileTransformer и реализовать метод преобразования.
Мы будем использоватьJavassist, чтобы добавить байт-код в классMyAtm и добавить журнал с общим временем транзакции вывода ATW:
public class AtmTransformer implements ClassFileTransformer {
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
byte[] byteCode = classfileBuffer;
String finalTargetClassName = this.targetClassName
.replaceAll("\\.", "/");
if (!className.equals(finalTargetClassName)) {
return byteCode;
}
if (className.equals(finalTargetClassName)
&& loader.equals(targetClassLoader)) {
LOGGER.info("[Agent] Transforming class MyAtm");
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(targetClassName);
CtMethod m = cc.getDeclaredMethod(
WITHDRAW_MONEY_METHOD);
m.addLocalVariable(
"startTime", CtClass.longType);
m.insertBefore(
"startTime = System.currentTimeMillis();");
StringBuilder endBlock = new StringBuilder();
m.addLocalVariable("endTime", CtClass.longType);
m.addLocalVariable("opTime", CtClass.longType);
endBlock.append(
"endTime = System.currentTimeMillis();");
endBlock.append(
"opTime = (endTime-startTime)/1000;");
endBlock.append(
"LOGGER.info(\"[Application] Withdrawal operation completed in:" +
"\" + opTime + \" seconds!\");");
m.insertAfter(endBlock.toString());
byteCode = cc.toBytecode();
cc.detach();
} catch (NotFoundException | CannotCompileException | IOException e) {
LOGGER.error("Exception", e);
}
}
return byteCode;
}
}
5.3. Создание файла манифеста агента
Наконец, чтобы получить работающий Java-агент, нам понадобится файл манифеста с парой атрибутов.
Следовательно, мы можем найти полный список атрибутов manifest в официальной документацииInstrumentation Package.
В окончательном файле jar агента Java мы добавим следующие строки в файл манифеста:
Agent-Class: com.example.instrumentation.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.example.instrumentation.agent.MyInstrumentationAgent
Наш инструментарий Java завершен. Чтобы запустить его, обратитесь к разделуLoading a Java Agent этой статьи.
6. Заключение
В этой статье мы поговорили об API инструментария Java. Мы рассмотрели, как загрузить агент Java в JVM как статически, так и динамически.
Мы также рассмотрели, как мы будем создавать собственный агент Java с нуля.
Как всегда, полную реализацию примера можно найти вover on Github.