Любопытно об отладке
Когда я впервые изучил Java, мне было очень интересно узнать об отладке IDEA, она не только может просматривать контекст точек останова, но, что еще более удивительно, я могу использовать ее функцию Evaluate для непосредственного выполнения определенных команд в точках останова, выполнения некоторых вычислений или изменения текущей переменной. .
В начале я был незнаком с синтаксисом и часто писал неправильный код, на перепаковку и деплой код уходило много времени, поэтому я разрабатывал сразу под Debug. Поставьте точку останова в начале записываемого метода, снова и снова выполняйте функцию метода в поле Evaluate и постоянно корректируйте код.Если проблем нет, скопируйте код и поместите его в IDEA, а затем напишите следующий метод, так что Так же, как и при написании интерпретируемого языка, похожего на PHP, его очень удобно писать и выполнять.
Но Java — это статический язык, и его необходимо скомпилировать перед запуском.Может ли код, который я написал, компилироваться в режиме реального времени и «внедряться» в отлаживаемую службу?
По мере того, как я лучше знакомился с Java, я также узнал об отражении, байт-коде и других технологиях.Пока я не поделился этим на еженедельном собрании несколько дней назад, коллега поделился использованием и реализацией Btrace и упомянул фреймворк Java ASM и JVM TI. интерфейс. . Реализация способности Btrace изменять код имеет много общего с Debug's Evaluate, что меня очень привлекло. Совместное использование — это как введение, и то, что вы из него узнаете, — это только кожа, чтобы понять это, вам все равно нужно изучить это самому. Поэтому я проверил информацию и написал код, чтобы узнать его конкретную реализацию.
Перепечатка по желанию, статья будет дорабатываться, просьба указывать адрес источника:zhenbianshu.github.io.
ASM
Первая проблема, которую необходимо решить при реализации Evaluate, — как изменить поведение исходного кода.Ее реализация называется технологией динамического байт-кода в Java.
Генерировать байт-код динамически
Мы знаем, что код Java, который мы пишем, должен быть скомпилирован в байт-код, прежде чем его можно будет выполнить в JVM, и как только байт-код загружен в виртуальную машину, его можно будет интерпретировать и выполнить.
Файл байт-кода (.class) — это обычный двоичный файл, генерируемый компилятором Java. И пока это файл, его можно изменить.Если мы проанализируем исходный файл байт-кода с определенными правилами, изменим его или просто переопределим, не может ли это изменить поведение кода.
В экосистеме Java существует множество технологий, которые могут динамически генерировать байт-код, например BCEL, Javassist, ASM, CGLib и т. д., каждая из которых имеет свои преимущества. Некоторые из них сложны в использовании, но эффективны, а некоторые просты и плохо работают.
Структура ASM
ASM является самым мощным из них.С его помощью вы можете динамически изменять классы, методы и даже переопределять классы.Даже нижний слой CGLib реализован с помощью ASM.
Конечно, порог его использования также очень высок, для его использования требуется некоторое понимание файлов байт-кода Java и знакомство с инструкциями по компиляции JVM. Хотя я не знаком с синтаксисом байт-кода JVM, некоторые великие боги разработали плагины, которые могут просматривать байт-коды в IDEA:ASM Bytecode Outline, щелкните правой кнопкой мыши файл класса, который хотите просмотреть, и выберитеShow bytecode OutlineТо есть вы можете просмотреть байт-код, который мы хотим сгенерировать, на панели инструментов справа. В отличие от примера, мы можем легко написать код Java, который манипулирует байт-кодом.
и сократить доASMifiedНа панели вкладок мы можем даже напрямую получить код использования ASM.
Общий метод
В кодовой реализации ASM наиболее очевидным является режим посетителя, и ASM упакован в средство доступа к коду, когда вызывается код сегмента, загруженный JVM.
ClassReader — это точка входа кода ASM. Он анализирует двоичный байт-код. При его создании нам нужно передать ClassVisitor. В этом посетителе мы можем реализоватьvisitMethod()/visitAnnotation()и другие методы для определения методов доступа к структурам классов (таким как методы, поля, аннотации).
Интерфейс ClassWriter наследует интерфейс ClassVisitor.Когда мы создаем экземпляр класса доступа, мы «внедряем» в него ClassWriter, чтобы реализовать объявление записи класса.
Instrument
вводить
Байт-код изменен, но JVM будет использовать свой собственный загрузчик классов для загрузки файла байт-кода во время выполнения.После загрузки она не будет обращать внимания на вносимые нами изменения.Для реализации модификаций существующих классов нам также потребуется Другая библиотека идти с Javainstrument.
Instrument — это библиотека классов, предоставляемая JVM, которая может изменять загруженные файлы классов. До версии 1.6 инструмент может вступить в силу только тогда, когда JVM начинает загружать класс, после чего инструмент поддерживает модификацию определения класса во время выполнения.
использовать
Чтобы использовать возможности модификации класса инструмента, нам необходимо реализовать егоClassFileTransformerИнтерфейс определяет конвертер файлов классов. это только одинtransform()Метод будет вызываться при загрузке файла класса.В методе преобразования мы можем переписать или заменить входящий двоичный байт-код, сгенерировать новый массив байт-кода и вернуть его, а JVM будет использовать байт-код, возвращенный методом преобразования.Данные загружается в классы.
JVM TI
Методы модификации и переопределения байт-кода определены, но как заставить JVM вызывать предоставленный нами преобразователь классов? Вот еще одно введение в JVM TI.
вводить
JVM TI (Интерфейс инструментов JVM) Интерфейс инструментов JVM — это очень мощный инструментальный интерфейс, предоставляемый JVM для операций JVM.Через этот интерфейс мы можем реализовывать операции над различными компонентами JVM, отJVMTM Tool InterfaceЗдесь мы признаем мощь JVM TI, которая включает в себя интерфейсы управления памятью кучи виртуальной машины, классами, потоками и другими аспектами.
С помощью механизма событий JVM TI регистрирует различные перехватчики событий через интерфейс и запускает предопределенные перехватчики одновременно с запуском события JVM, чтобы реализовать восприятие и реакцию на каждое событие JVM.
Agent
Агент — это способ реализации JVM TI. Мы связываем статическую библиотеку в скомпилированном проекте C и внедряем функции статической библиотеки в проект, чтобы на функции в библиотеке можно было ссылаться в проекте. Мы можем сравнить агент со статической библиотекой на C, мы также можем реализовать его на C или C++, скомпилировать его в файл dll или около того и запустить его при запуске JVM.
В это время давайте подумаем о реализации Debug.Когда мы запускаем отлаживаемую JVM, мы должны добавить параметры-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333, а параметр -agentlib указывает агента Java, который мы хотим загрузить. jdwp — это имя агента. В системе Linux мы можем найти файл библиотеки jdwp.so в каталоге jre.
Система отладки Java состоит из jdpa, от высокого к низкому, соответственно.jdi->jdwp->jvmti, мы отправляем инструкции по отладке через интерфейс JDI, а jdwp эквивалентен каналу, помогающему нам транслировать инструкции JDI в JVM TI, а нижний JVM TI, наконец, реализует работу JVM.
использовать
Использование агента JVM TI очень просто: при запуске агента добавьте параметр -agent, чтобы указать пакет jar агента, который мы хотим загрузить.
Чтобы реализовать модификацию кода, нам нужно реализовать инструментальный агент, который можно добавить в класс, добавивpremain()илиagentmain()метод достижения. Чтобы реализовать функцию динамического инструмента выше 1.6, реализуйте метод agentmain.
В методе agentmain мы вызываемInstrumentation.retransformClasses()Метод реализует переопределение целевого класса.
Кроме того, для динамического добавления агента к работающей JVM также необходимо использовать функцию присоединения JVM, которая включена в пакет Sun tools.jar.VirtualMachineКласс предоставляет функцию подключения локальной JVM, которая требует от нас передачи pid локальной JVM.tools.jar можно найти в каталоге jre.
генерация агента
Кроме того, мы также должны обратить внимание на упаковку агента.В нем необходимо указать параметр Agent-Class, чтобы указать наш класс, включая метод agentmain, который можно рассматривать как назначенный класс входа.
Кроме того, необходимо настроитьMANIFEST.MFНекоторые параметры файла позволяют нам переопределить класс. Если ваша реализация агента также должна ссылаться на некоторые другие библиотеки классов, вам также необходимо упаковать эти библиотеки классов в этот пакет jar.Ниже приведена моя конфигурация файла pom.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>asm.TestAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Manifest-Version>1.0</Manifest-Version>
<Permissions>all-permissions</Permissions>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
Кроме того, необходимо использоватьmvn assembly:assemblКоманда для создания jar-файлов с зависимостями в качестве агента.
Код
Во время тестирования я написал демонстрацию, которая использует описанные выше методы для реализации простой динамической модификации байт-кода.
Модифицированный класс
TransformTarget — это целевой класс, который нужно изменить. При нормальном выполнении он будет выводить «hello» каждые три секунды.
public class TransformTarget {
public static void main(String[] args) {
while (true) {
try {
Thread.sleep(3000L);
} catch (Exception e) {
break;
}
printSomething();
}
}
public static void printSomething() {
System.out.println("hello");
}
}
Agent
Агент — это тело класса, выполняющего модификацию, который использует ASM для модификации методов класса TransformTarget и отправляет модификации в JVM с помощью пакета инструментов.
Класс входа также является классом агента агента.
public class TestAgent {
public static void agentmain(String args, Instrumentation inst) {
inst.addTransformer(new TestTransformer(), true);
try {
inst.retransformClasses(TransformTarget.class);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}
Класс, выполняющий модификацию и преобразование байт-кода.
public class TestTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("Transforming " + className);
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter);
reader.accept(classVisitor, ClassReader.SKIP_DEBUG);
return classWriter.toByteArray();
}
class TestClassVisitor extends ClassVisitor implements Opcodes {
TestClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("printSomething")) {
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(19, l0);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("bytecode replaced!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(20, l1);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 0);
mv.visitEnd();
TransformTarget.printSomething();
}
return mv;
}
}
}
Attacher
Классы, которые динамически загружают агент в целевую JVM, используя методы из tools.jar.
public class Attacher {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
VirtualMachine vm = VirtualMachine.attach("34242"); // 目标 JVM pid
vm.loadAgent("/path/to/agent.jar");
}
}
Таким образом, сначала запустите класс TransformTarget, получите pid и передайте его в Attacher, укажите банку агента и прикрепите агент к TransformTarget. Исходный вывод «привет» становится «байт-код заменен!», который мы хотим изменить. . . .
резюме
После освоения технологии динамической модификации байт-кода принцип Btrace станет яснее, когда мы оглянемся назад.Мы также можем реализовать упрощенную версию с небольшим исследованием. Кроме того, технологический стек различных инструментов анализа производительности Java, реализованных многими Даниэлем, ничем не отличается.Зная это, мы также можем написать инструменты, подходящие для себя в будущем, или, по крайней мере, модифицировать чужие инструменты~
Я должен сказать, что экология Java действительно процветает, она действительно широка и глубока, при просмотре информации о модуле всегда можно найти много новых концепций, и всегда будет что-то новое для изучения.
Если у вас есть какие-либо вопросы по поводу этой статьи, вы можете оставить сообщение ниже, чтобы связаться с ним. Если вы считаете, что эта статья полезна для вас, пожалуйста, подпишитесь на меня.ВейбоилиGitHub. Вы также можете найти в моемБлог РЕПОщелкните правой кнопкой мышиWatchи выберитеReleases onlyпункт订阅В моем блоге я сообщу вам, как только выйдет новая статья.
Ссылаться на: