1. Байт-код
1.1 Что такое байт-код?
Причина, по которой Java может «компилировать один раз, запускаться везде», заключается в том, что JVM настраивается для различных операционных систем и платформ, а во-вторых, в том, что независимо от того, на какой платформе она находится, она может компилировать и генерировать байт-код фиксированного формата (.class file) ) для использования JVM. Таким образом, мы также можем видеть важность байт-кода для экосистемы Java. Он называется байт-кодом, потому что файл байт-кода состоит из шестнадцатеричных значений, а JVM считывает набор из двух шестнадцатеричных значений, то есть в байтах. В Java команда javac обычно используется для компиляции исходного кода в файл байт-кода Пример файла .java от компиляции до запуска показан на рисунке 1.
Для разработчиков понимание байт-кода может более точно и интуитивно понимать более глубокие вещи в языке Java.Например, с помощью байт-кода очень интуитивно понятно, как ключевое слово Volatile работает в байт-коде. Кроме того, применение технологии улучшения байт-кода в Spring AOP, различных ORM-фреймворках и горячем развертывании не редкость, и глубокое понимание ее принципов приносит нам большую пользу. Кроме того, из-за существования спецификации JVM, поскольку байт-код, соответствующий спецификации, наконец может быть сгенерирован, его можно запустить на JVM, так что это дает множество языков (таких как Scala, Groovy , Kotlin), которые работают на JVM, это возможность расширить возможности, которых нет в Java, или реализовать различные синтаксические сахара. После понимания байт-кода и последующего изучения этих языков вы можете пойти «вверх по течению» и посмотреть на его идеи дизайна с точки зрения байт-кода, и его «легко изучить».
В этой статье основное внимание уделяется технологии улучшения байт-кода, начиная с байт-кода слой за слоем, от коллекции операций байт-кода JVM до среды операций с байт-кодом Java, а затем к различным принципам и приложениям, с которыми мы знакомы. Представьте их одно за другим.
1.2 Структура байт-кода
После компиляции файла .java с помощью javac будет получен файл .class, такой как написание простого класса ByteCodeDemo, как показано в левой части рисунка 2 ниже:
После компиляции генерируется файл ByteCodeDemo.class, который после открытия представляет собой набор шестнадцатеричных чисел, разделенных по байтам и отображаемых, как показано в правой части рисунка 2. Как упоминалось выше, у JVM есть требования спецификации для байт-кода, так какой же структуре соответствует кажущаяся беспорядочной шестнадцатеричная система счисления? Спецификация JVM требует, чтобы каждый файл байт-кода состоял из десяти частей в фиксированном порядке, а общая структура показана на рисунке 3. Далее мы представим эти десять частей одну за другой:
(1) Магическое число
Первые четыре байта всех файлов .class представляют собой магические числа, а фиксированное значение магического числа: 0xCAFEBABE. Магический номер помещается в начало файла, и JVM может определить, может ли файл быть файлом .class, в соответствии с началом файла, и если это так, она продолжит последующие операции.
Интересно, что фиксированное значение магического числа сформулировано Джеймсом Гослингом, отцом Java, как CafeBabe (кофейный малыш), а иконкой Java является чашка кофе.
(2) Номер версии
Номер версии находится через 4 байта после магического числа.Первые два байта представляют собой дополнительный номер версии (Minor Version), а последние два байта представляют собой основной номер версии (Major Version). Номер версии на рис. 2 выше — «00 00 00 34», дополнительный номер версии, преобразованный в десятичный, равен 0, основной номер версии, преобразованный в десятичный, — 52, а основной номер версии соответствует серийному номеру 52 в официальном документе Oracle. веб-сайт 1.8, поэтому скомпилируйте его. Номер версии файла Java — 1.8.0.
(3) Постоянный пул
Байт сразу после основного номера версии является константной записью пула. В пуле констант хранятся два типа констант: литералы и символические ссылки. Литералы — это постоянные значения, объявленные в коде как Final, и символические ссылки, такие как глобально квалифицированные имена классов и интерфейсов, имена и дескрипторы полей, а также имена и дескрипторы методов. Пул констант в целом разделен на две части: счетчик пула констант и область данных пула констант, как показано на рис. 4 ниже.
- Счетчик постоянного пула (constant_pool_count): поскольку количество констант не является фиксированным, сначала необходимо разместить два байта, чтобы представить значение счетчика емкости постоянного пула. Первые 10 байт байт-кода примера кода на рисунке 2 показаны ниже на рисунке 5. Преобразуйте шестнадцатеричное 24 в десятичное значение 36, исключая нижний индекс «0», то есть в этом файле класса имеется 35 констант в целом.
- Область данных пула констант: область данных состоит из (constant_pool_count-1) структур cp_info, одна структура cp_info соответствует одной константе. В байт-коде имеется 14 типов cp_info (как показано на рис. 6 ниже), и структура каждого типа фиксирована.
Взяв в качестве примера CONSTANT_utf8_info, его структура показана в левой части рисунка 7 ниже. Во-первых, байт «тег», его значение берется из тега соответствующего элемента на рисунке 6. Поскольку его тип — utf8_info, значение равно «01». Следующие два байта определяют длину строки, Length, а затем байты длины представляют собой конкретное значение строки. Извлеките структуру cp_info из байт-кода на рис. 2, как показано справа на рис. 7 ниже. После перевода его значение таково: тип константы — строка utf8, длина — один байт, а данные — «a».
Другие типы структур cp_info не будут повторяться в этой статье.Общая структура аналогична.Тип сначала идентифицируется тегом, а затем следуют n байтов для описания длины и/или данных. Prophet, вы можете использовать команду javap -verbose ByteCodeDemo для просмотра полного пула констант после декомпиляции JVM, как показано на рисунке 8 ниже. Видно, что результаты декомпиляции ясно показывают тип и значение каждой структуры cp_info.
(4) Флаг доступа
Два байта после конца пула констант описывают, является ли класс классом или интерфейсом и модифицируется ли он модификаторами, такими как Public, Abstract и Final. Спецификация JVM определяет флаг доступа (Access_Flag) на рис. 9 ниже. Следует отметить, что JVM не исчерпывает все флаги доступа, а использует для их описания операции побитового ИЛИ.Например, если модификатор класса Public Final, значение соответствующего модификатора доступа равно ACC_PUBLIC|ACC_FINAL, то есть 0x0001 |0x0010=0x0011.
(5) Текущее имя класса
Два байта после флага доступа описывают полное имя текущего класса. Значение, хранящееся в этих двух байтах, является значением индекса в пуле констант.По значению индекса полное имя этого класса можно найти в пуле констант.
(6) Имя родительского класса
Два байта после имени текущего класса описывают полное имя родительского класса, как указано выше, а также сохраняют значение индекса в пуле констант.
(7) Информация об интерфейсе
После имени родительского класса следует двухбайтовый счетчик интерфейса, описывающий количество интерфейсов, реализованных классом или родительским классом. Следующие n байтов — значения индексов строковых констант для всех имен интерфейсов.
(8) Полевой стол
Таблицы полей используются для описания переменных, объявленных в классах и интерфейсах, включая переменные уровня класса и переменные экземпляра, но не локальные переменные, объявленные внутри методов. Таблица полей также разделена на две части, первая часть — это два байта, описывающие количество полей, вторая — подробная информация fields_info каждого поля. Структура таблицы полей показана на следующем рисунке:
В качестве примера возьмем таблицу полей байт-кода на рис. 2, как показано на рис. 11 ниже. Флаг доступа к полю показан на рисунке 9, а 0002 соответствует Private. Имя поля — «a», а дескриптор — «I» (представляющий int) в пуле констант на рис. 8 с помощью подписки индекса. Таким образом, переменная private int a, объявленная в классе, может быть однозначно определена.
(9) Таблица методов
После завершения таблицы полей таблица методов состоит из двух частей: первая часть представляет собой два байта, описывающих количество методов, а вторая часть представляет собой подробную информацию о каждом методе. Детали метода более сложны, включая флаг доступа к методу, имя метода, дескриптор метода и свойства метода, как показано на следующем рисунке:
Модификатор разрешения метода по-прежнему можно получить с помощью запроса значения на рисунке 9. Имя метода и дескриптор метода являются значениями индекса в пуле констант, которые можно найти в пуле констант через значение индекса. Часть «атрибуты метода» более сложна, и она напрямую декомпилируется с помощью javap -verbose для интерпретации в удобочитаемую информацию, как показано на рисунке 13. Вы можете видеть, что свойства включают в себя следующие три части:
- «Кодовая область»: код операции инструкции JVM, соответствующий исходному коду, а «Кодовая область» является частью ключевой операции при выполнении расширения байт-кода.
- «LineNumberTable»: таблица номеров строк, которая соответствует кодам операций в области «Код» номерам строк в исходном коде, которые будут играть роль в отладке (исходный код проходит строку, сколько кодов операций инструкции JVM нужно пройти) .
- «LocalVariableTable»: таблица локальных переменных, включая This и локальные переменные. Причина, по которой This можно вызывать внутри каждого метода, заключается в том, что JVM неявно передает This в качестве первого параметра каждого метода. Конечно, это для нестатических методов.
(10) Дополнительная таблица атрибутов
Последняя часть байт-кода, этот элемент хранит основную информацию о свойствах, определенных классом или интерфейсом в файле.
1.3 Сбор операций с байт-кодом
На рис. 13 выше красные числа от 0 до 17 в области кода — это коды операций, которые JVM фактически выполняет после компиляции исходного кода метода в .java. Чтобы помочь людям понять, что вы видите после декомпиляции, это мнемоника, соответствующая шестнадцатеричному коду операции, соответствие между шестнадцатеричным значением кода операции и мнемоникой, а также использование каждого кода операции, вы можете проверить официальные документы Oracle, чтобы понять, вы можете обращайтесь к нему, когда вам это нужно. Например, первая мнемоника на приведенном выше рисунке — icont_2, что соответствует байт-коду на рисунке 2 — 0x05, который используется для помещения значения int 2 в стек операндов. По аналогии, после понимания мнемоники 0~17, это реализация полного метода add().
1.4 Стек операндов и байт-код
Набор инструкций JVM основан на стеке, а не на регистре.Основываясь на стеке, он может иметь хорошую кросс-платформенную производительность (поскольку набор инструкций регистра часто связан с оборудованием), но недостатком является то, что для завершения та же самая операция, реализация на основе стека требует больше инструкций.Для завершения (поскольку стек - это просто структура FILO, его нужно часто извлекать и извлекать). Кроме того, поскольку стек реализован в памяти, а регистры находятся в области кэша ЦП, скорость на основе стека значительно ниже, что также является жертвой кроссплатформенности.
Коды операций или наборы операций, о которых мы упоминали выше, фактически управляют стеком операндов этой JVM. Чтобы более интуитивно почувствовать, как код операции управляет стеком операндов, и понять роль пула констант и таблицы переменных, операция метода add() над стеком операндов выполнена в виде GIF, как показано на рисунке 14 ниже. Перехватывается упомянутая часть в пуле констант, начиная с инструкции icont_2 и заканчивая ireturn, что соответствует инструкциям в области кода 0~17 на рисунке 13:
1.5 Просмотр инструментов байт-кода
Если вы используете команду javap каждый раз, когда просматриваете декомпилированный байт-код, это очень утомительно. Вот рекомендуемый плагин Idea:jclasslib. Эффект использования показан на рис. 15. После компиляции кода выберите «Показать байт-код с помощью jclasslib» в строке меню «Вид», и вы сможете интуитивно увидеть информацию о классе, константном пуле, области методов и другую информацию о текущем файл с байт-кодом.
2. Усовершенствования байт-кода
Выше мы сосредоточились на структуре байт-кода, которая закладывает основу для понимания реализации технологии улучшения байт-кода. Технология улучшения байт-кода — это тип технологии, которая изменяет существующие байт-коды или динамически создает новые файлы байт-кода. Далее мы углубимся, начав с реализации, которая наиболее непосредственно манипулирует байт-кодом.
2.1 ASM
Для требований, требующих ручного управления байт-кодом, можно использовать ASM, который может напрямую создавать файлы байт-кода .class или динамически изменять поведение класса перед его загрузкой в JVM (как показано на рис. 17 ниже). Сценарии применения ASM включают AOP (Cglib основан на ASM), горячее развертывание и модификацию классов в других пакетах jar. Конечно, с такими низкоуровневыми шагами реализовать их также сложнее. Далее в этой статье будут представлены два API ASM и использование ASM для реализации относительно грубого АОП. Но перед этим, чтобы все могли быстрее понять поток обработки ASM, настоятельно рекомендуется, чтобы читатели сначалашаблон посетителяпонять. Проще говоря, шаблон посетителя в основном используется для изменения или манипулирования некоторыми данными с относительно стабильными структурами данных, и из главы 1 мы знаем, что структура файла байт-кода фиксируется JVM, поэтому очень удобно использовать шаблон посетителя для изменения файла кода раздела.
2.1.1 ASM API
2.1.1.1 Основной API
ASM Core API может быть аналогичен методу SAX для анализа файлов XML.Вы можете использовать метод потоковой передачи для обработки файлов байт-кода без чтения всей структуры этого класса. Преимущество в том, что он очень экономит память, но его сложнее программировать. Однако из соображений производительности при программировании обычно используется Core API. В Core API есть следующие ключевые классы:
- ClassReader: используется для чтения скомпилированных файлов .class.
- ClassWriter: используется для перестроения скомпилированных классов, таких как изменение имен классов, свойств и методов, а также может создавать новые файлы байт-кода классов.
- Различные классы посетителей: как упоминалось выше, CoreAPI обрабатывает байт-код последовательно сверху вниз.Существуют разные посетители для разных областей в файле байт-кода, такие как MethodVisitor для доступа к методам и FieldVisitor для доступа к переменным класса, AnnotationVisitor для доступа к аннотациям и т. д. . Для достижения АОП ключом к использованию является MethodVisitor.
2.1.1.2 API дерева
ASM Tree API может быть аналогом парсинга метода DOM в файле XML и чтения всей структуры класса в память.Недостаток в том, что он потребляет много памяти, но программирование относительно простое. TreeApi отличается от CoreAPI. TreeAPI отображает различные области байт-кода через различные классы Node. По аналогии с DOM-узлами этот метод программирования можно хорошо понять.
2.1.2 Реализация АОП напрямую с помощью ASM
Используйте CoreAPI ASM для улучшения класса. Здесь мы не запутываемся с профессиональными терминами АОП, такими как слайс и уведомление, а только добавляем логику до и после вызова метода, которую легко понять и понять. Сначала определите базовый класс, который необходимо улучшить: он содержит только один метод process(), который выводит строку «процесс». После улучшения мы ожидаем, что выведем «start» до выполнения метода и «end» после.
public class Base {
public void process(){
System.out.println("process");
}
}
Чтобы использовать ASM для реализации АОП, необходимо определить два класса: класс MyClassVisitor, который используется для посещения и изменения байт-кода, и класс Generator, в котором определены ClassReader и ClassWriter. который читает classReader Возьмите байт-код и передайте его классу MyClassVisitor для обработки.После завершения обработки ClassWriter записывает байт-код и заменяет старый байт-код. Класс Generator относительно прост, давайте рассмотрим его реализацию, как показано ниже, а затем сосредоточимся на объяснении класса MyClassVisitor.
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
public class Generator {
public static void main(String[] args) throws Exception {
//读取
ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//输出
File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("now generator cc success!!!!!");
}
}
MyClassVisitor наследуется от ClassVisitor и используется для наблюдения за байт-кодом. Он также содержит внутренний класс MyMethodVisitor, который наследуется от MethodVisitor для наблюдения за методами в классе.Его общий код выглядит следующим образом:
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
//Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
if (!name.equals("<init>") && mv != null) {
mv = new MyMethodVisitor(mv);
}
return mv;
}
class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}
Этот класс можно использовать для изменения байт-кода. Чтобы подробно интерпретировать код, шаги по изменению байт-кода:
- Во-первых, с помощью метода visitMethod в классе MyClassVisitor определите, какой метод был прочитан текущим байт-кодом. После пропуска конструктора "" метод, который необходимо улучшить, передается внутреннему классу MyMethodVisitor для обработки.
- Далее введите метод visitCode во внутренний класс MyMethodVisitor, он будет вызываться, когда ASM начнет обращаться к области кода метода, перепишите метод visitCode и поместите сюда пре-логику в АОП.
- MyMethodVisitor продолжает читать инструкции байт-кода. Всякий раз, когда ASM обращается к инструкции без параметров, он вызывает метод visitInsn в MyMethodVisitor. Судим, является ли текущая инструкция безпараметрической инструкцией «возврата», если да, добавляем какие-то инструкции перед ней, то есть закладываем в этот метод логику постобработки АОП.
- Подводя итог, можно сказать, что переписывание двух методов в MyMethodVisitor может реализовать АОП, а при переписывании метода вам необходимо использовать метод записи ASM, чтобы вручную написать или изменить байт-код. Вставку байт-кода можно выполнить, вызвав метод visitXXXXInsn() в methodVisitor. XXXX соответствует соответствующему мнемоническому типу кода операции. Например, код операции, соответствующий mv.visitLdcInsn("end"), представляет собой ldc "end", то есть строку "конец" помещается в стек.
После завершения двух классов посетителей запустите основной метод в генераторе, чтобы завершить расширение байт-кода базового класса.Улучшенный результат можно просмотреть в файле Base.class в скомпилированной целевой папке, и вы можете увидеть декомпиляцию кода сзади был изменен (показан слева на рис. 18). Затем напишите тестовый класс MyTest, в нем new Base() и вызовите метод base.process(), вы можете увидеть эффект реализации АОП, показанный в правой части следующего рисунка:
2.1.3 Инструменты АСМ
При использовании ASM для записи байт-кодов необходимо использовать серию методов visitXXXXInsn() для написания соответствующих мнемоник, поэтому каждую строку исходного кода необходимо преобразовать в мнемоники одну за другой, а затем преобразовать в visitXXXXInsn() таким образом. письмо. Первым шагом является преобразование исходного кода в мнемонику.Это достаточно хлопотно.Если вы не знакомы с набором операций байт-кода, вам нужно скомпилировать код, а затем декомпилировать его, чтобы получить мнемонику, соответствующую исходному коду. На втором этапе, при использовании ASM для написания байт-кода, как передать параметры — тоже головная боль. Сообщество ASM также знает об этих двух проблемах, поэтому инструменты предоставляютсяASM ByteCode Outline.
После установки щелкните правой кнопкой мыши и выберите «Show Bytecode Outline», в новой вкладке выберите вкладку «ASMified», как показано на рисунке 19, вы можете увидеть метод записи ASM, соответствующий коду в этом классе. Верхний и нижний красные прямоугольники на рисунке соответствуют пре-логике и пост-логике в АОП соответственно.Скопируйте эти два блока непосредственно в методы visitMethod() и visitInsn() в посетителе, и все.
2.2 Javassist
ASM работает с байт-кодом на уровне инструкций.После прочтения вышесказанного мы интуитивно почувствовали, что структура для работы с байт-кодом на уровне инструкций довольно неясна для реализации. Поэтому в дополнение к этому мы кратко представим другой тип фреймворка: Javassist, фреймворк, который делает упор на работу с байт-кодом на уровне исходного кода.
При использовании Javassist для улучшения байт-кода не нужно обращать внимание на жесткую структуру байт-кода, а его преимущество заключается в простоте программирования. Непосредственно используйте форму java-кодирования, не зная инструкций виртуальной машины, вы можете динамически изменять структуру класса или динамически генерировать класс. Наиболее важными из них являются четыре класса ClassPool, CtClass, CtMethod и CtField:
- CtClass (класс времени компиляции): информация о классе времени компиляции, которая является абстрактным представлением файла класса в коде.Вы можете получить объект CtClass через полное имя класса для представления файла класса.
- ClassPool: с точки зрения разработки ClassPool — это HashTable, в которой хранится информация CtClass.Ключом является имя класса, а значением является объект CtClass, соответствующий имени класса. Когда нам нужно изменить класс, мы получаем соответствующий CtClass из пула с помощью метода pool.getCtClass("className").
- CtMethod, CtField: эти два параметра легко понять, они соответствуют методам и свойствам класса.
Разобравшись с этими четырьмя классами, мы можем написать небольшую демонстрацию, чтобы показать простые и быстрые функции Javassist. Мы все еще улучшаем метод process() в Base и выводим "start" и "end" до и после вызова метода. Код реализации выглядит следующим образом. Все, что нам нужно сделать, это получить соответствующий объект CtClass и его методы из пула, а затем выполнить методы method.insertBefore и insertAfter, параметры — это код Java, который необходимо вставить, а затем передать в виде строки. Чрезвычайно просто.
import com.meituan.mtrace.agent.javassist.*;
public class JavassistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("meituan.bytecode.javassist.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
Class c = cc.toClass();
cc.writeFile("/Users/zen/projects");
Base h = (Base)c.newInstance();
h.process();
}
}
3. Перегрузка класса во время выполнения
3.1 проблема проблема
Предыдущая глава была посвящена двум различным типам фреймворков для манипулирования байт-кодом, оба из которых реализуют грубые реализации АОП. На самом деле, чтобы всем было проще понять технологию улучшения байт-кода, в вышеизложенном мы избежали важного и разделили процесс ASM на два основных метода: первый — использовать MyClassVisitor для модификации скомпилированного файла класса, а второй — использовать MyClassVisitor для модификации скомпилированного файла класса, нового объекта и вызова. Этот период не предполагает перезагрузки класса во время выполнения JVM, но в первом основном методе байт-код скомпилированного класса заменяется на ASM, а во втором основном методе используется непосредственно замененный класс. Информация. Кроме того, при реализации Javassist мы загружали базовый класс только один раз и не требовали перезагрузки класса во время выполнения.
Что произойдет, если мы в JVM сначала загрузим класс, затем байт-код улучшит его и перезагрузит? Чтобы смоделировать эту ситуацию, нам нужно всего лишь добавить Base b=new Base() в первую строку метода main() в демонстрационной версии Javassist выше, то есть позволить JVM загрузить класс Base перед расширением, а затем выполнить на c. Метод toClass() выдает ошибку, как показано на рисунке 20 ниже. Следуя за методом c.toClass(), мы обнаружим, что он сообщает об ошибке при вызове собственного метода defineClass() загрузчика классов в конце. То есть JVM не допускает динамической перегрузки класса во время выполнения.
Очевидно, что если класс может быть улучшен только до того, как класс будет загружен, сценарии использования технологии расширения байт-кода становятся очень узкими. Желаемый эффект заключается в использовании методов улучшения байт-кода для замены и перезагрузки поведения класса в работающей JVM, в которой загружены все классы. Чтобы смоделировать эту ситуацию, мы переписываем класс Base, пишем в нем метод main, вызываем метод process() каждые пять секунд и выводим строку «process» в методе process().
Наша цель — заменить метод process() во время работы JVM и вывести «start» и «end» до и после него. То есть при запуске содержимое, печатаемое каждые пять секунд, меняется с «процесс» на «начало процесса, конец». Итак, как решить проблему, из-за которой JVM не позволяет перезагружать информацию о классе во время выполнения? Для достижения этой цели мы представим библиотеки классов Java, которые необходимо использовать одну за другой.
import java.lang.management.ManagementFactory;
public class Base {
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
//打印当前Pid
System.out.println("pid:"+s);
while (true) {
try {
Thread.sleep(5000L);
} catch (Exception e) {
break;
}
process();
}
}
public static void process() {
System.out.println("process");
}
}
3.2 Instrument
Instrument — это библиотека классов, предоставляемая JVM, которая может изменять загруженные классы и обеспечивает поддержку инструментальных сервисов, написанных на языке Java. Он должен полагаться на реализацию механизма Attach API JVMTI.Мы представим эту часть JVMTI в следующем разделе. До JDK 1.6 инструмент может вступить в силу только тогда, когда JVM начинает загружать класс, но после JDK 1.6 инструмент поддерживает изменение определения класса во время выполнения. Чтобы использовать функцию модификации класса инструмента, нам нужно реализовать предоставляемый им интерфейс ClassFileTransformer и определить преобразователь файла класса. Метод transform() в интерфейсе будет вызываться при загрузке файла класса, а в методе преобразования мы можем использовать ASM или Javassist выше, чтобы переписать или заменить входящий байт-код, чтобы позже сгенерировать новый массив байт-кода.
Мы определяем класс TestTransformer, реализующий интерфейс ClassFileTransformer, и по-прежнему используем Javassist для улучшения метода process() в базовом классе и печатаем «начало» и «конец» до и после соответственно. Код выглядит следующим образом:
import java.lang.instrument.ClassFileTransformer;
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
System.out.println("Transforming " + className);
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("meituan.bytecode.jvmti.Base");
CtMethod m = cc.getDeclaredMethod("process");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Теперь, когда у нас есть Transformer, как его внедрить в работающую JVM? Вам также необходимо определить агента для внедрения инструмента в JVM с помощью возможностей агента. Мы познакомим вас с агентом в следующем разделе, а сейчас мы познакомим вас с другим классом Instrumentation, который используется в агенте. После JDK 1.6 инструментарий можно использовать как инструмент после запуска, как инструмент для собственного кода, а также динамически изменять путь к классам и т. д. Мы можем добавить Transformer, определенный выше, в Instrumentation и указать класс для перезагрузки, как показано ниже. Таким образом, когда агент подключен к JVM, он будет выполнять замену и повторную загрузку байт-кода в JVM.
import java.lang.instrument.Instrumentation;
public class TestAgent {
public static void agentmain(String args, Instrumentation inst) {
//指定我们自己定义的Transformer,在其中利用Javassist做字节码替换
inst.addTransformer(new TestTransformer(), true);
try {
//重定义类并载入新的字节码
inst.retransformClasses(Base.class);
System.out.println("Agent Load Done.");
} catch (Exception e) {
System.out.println("agent load failed!");
}
}
}
3.3 JVMTI & Agent & Attach API
В предыдущем разделе мы привели код класса агента, чтобы проследить источник, нам нужно ввести JPDA (архитектура отладчика платформы Java). Классы разрешено перезагружать, если JVM запускается с включенным JPDA. В этом случае старую версию информации о классе, которая была загружена, можно выгрузить, а затем перезагрузить новую версию класса. Точно так же, как отладчик в названии JDPA, JDPA на самом деле представляет собой набор стандартов для отладки Java-программ, и любой JDK должен реализовывать этот стандарт.
JPDA определяет полный набор системы, он делит систему отладки на три части и определяет интерфейс связи между ними. Три части от младшего к старшему — это Java Virtual Machine Tool Interface (JVMTI), Java Debugging Protocol (JDWP) и Java Debugging Interface (JDI).Взаимосвязь между ними показана на следующем рисунке:
Теперь вернемся к теме. Мы можем использовать некоторые возможности JVMTI для динамической перезагрузки информации о классе. JVM TI (JVM TOOL INTERFACE, инструментальный интерфейс JVM) — это набор инструментальных интерфейсов, предоставляемых JVM для работы с JVM. Через JVMTI могут быть реализованы различные операции с JVM. Он регистрирует различные обработчики событий через интерфейс. Когда событие JVM запускается, одновременно срабатывает предопределенный обработчик, чтобы реализовать ответ на каждое событие JVM, включая загрузку файла класса. ., генерация и захват исключений, начало и конец потока, вход и выход из критической секции, изменение переменных-членов, начало и конец GC, вход и выход вызова метода, конкуренция и ожидание критической секции, запуск и выход виртуальной машины и т. д.
Агент является реализацией JVMTI. Существует два метода запуска агента: один — запустить процесс Java, который часто встречается в java-agentlib, другой — загрузить модуль во время выполнения через API подключения (jar package) Динамическое присоединение к процессу Java с указанным идентификатором процесса.
Роль Attach API заключается в обеспечении возможности взаимодействия между процессами JVM.Например, чтобы позволить другому процессу JVM выгрузить поток онлайн-сервиса, он запустит процесс jstack или jmap и передаст параметр pid в скажите ему, какой процесс делает дамп потока, что и делает API-интерфейс Attach. Далее мы будем динамически присоединять упакованный пакет jar агента к целевой JVM с помощью метода loadAgent() интерфейса Attach API. Конкретные шаги заключаются в следующем:
- Определите Agent и реализуйте в нем метод AgentMain, как в классе TestAgent в кодовом блоке 7, определенном в предыдущем разделе;
- Затем превратите класс TestAgent в пакет jar, содержащий MANIFEST.MF, где атрибут Agent-Class указан как полное имя TestAgent в файле MANIFEST.MF, как показано на следующем рисунке;
- Наконец, используйте Attach API, чтобы прикрепить наш упакованный пакет jar к указанному идентификатору JVM Код выглядит следующим образом:
import com.sun.tools.attach.VirtualMachine;
public class Attacher {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
// 传入目标 JVM pid
VirtualMachine vm = VirtualMachine.attach("39333");
vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");
}
}
- Поскольку класс агента указан в MANIFEST.MF, после подключения целевая JVM перейдет к методу agentmain(), определенному в классе TestAgent во время выполнения, и в этом методе мы используем Instrumentation для указания слова класса. код раздела выполняет замену байт-кода базового класса (через javassist) через определенный преобразователь класса TestTransformer и завершает перезагрузку класса. Таким образом, мы достигаем цели «изменения байт-кода класса и перезагрузки информации о классе во время работы JVM».
Ниже приведен эффект перезагрузки класса во время выполнения: сначала запустите метод main() в Base, запустите JVM, и вы сможете видеть вывод «процесса» каждые пять секунд в консоли. Затем выполните метод main() в Attacher и передайте pid предыдущей JVM. В этот момент вернитесь к консоли предыдущего метода main().Вы можете видеть, что «начало» и «конец» будут выводиться до и после вывода «процесса» каждые пять секунд, что означает, что улучшение байт-кода во время выполнения завершается. и перезагружает класс.
3.4 Сценарии использования
На данный момент область применения методов улучшения байт-кода больше не ограничивается классами загрузки JVM. С помощью вышеуказанных библиотек классов мы можем изменять и перезагружать классы в JVM во время выполнения. Таким образом можно сделать многое:
- Горячее развертывание: изменение онлайн-сервиса без развертывания сервиса, вы можете управлять им, добавлять журналы и т. д.
- Mock: Моделирование некоторых сервисов во время тестирования.
- Инструменты диагностики производительности. Например, bTrace использует Instrument для неинвазивного отслеживания работающей JVM и мониторинга информации о состоянии на уровне классов и методов.
4. Резюме
Технология улучшения байт-кода эквивалентна ключу для открытия JVM во время выполнения, с его помощью можно динамически модифицировать работающую программу и отслеживать состояние работающей программы JVM. Кроме того, динамический прокси и АОП, которые мы обычно используем, также тесно связаны с улучшением байт-кода, поскольку они используют различные средства для создания файлов байт-кода, соответствующих спецификации. Таким образом, освоив усовершенствование байт-кода, вы сможете эффективно находить и быстро устранять некоторые сложные проблемы (такие как проблемы с производительностью в сети, неконтролируемый доступ и параметры в методах, требующих экстренного ведения журнала и т. д.), а также их можно уменьшить в процессе разработки. код значительно повышает эффективность разработки.
5. Ссылки
- "ASM4-Руководство"
- Oracle:The class File Format
- Oracle:The Java Virtual Machine Instruction Set
- javassist tutorial
- JVM Tool Interface - Version 1.2
об авторе
Зин, инженер отдела исследований и разработок Meituan Dianping.
Предложения о работе
Команда отдела исследований и разработок Meituan, занимающаяся размещением в магазинах, отвечает за построение основной бизнес-системы отеля Meituan и стремится выполнить миссию «помогать всем жить лучше» с помощью технологий. Meituan Hotels неоднократно устанавливала отраслевые рекорды: за последние 12 месяцев количество забронированных ночей в отелях достигло 300 миллионов, а пиковое количество ночей в сутки превысило 2,8 миллиона. Видение команды: построить первоклассную техническую структуру в индустрии туризма и размещения и обеспечить быстрое развитие системы с точки зрения качества, безопасности, эффективности и производительности. Группа по исследованиям и разработкам в сфере гостиничного бизнеса Meituan Arrival Business Group в настоящее время ищет инженеров-разработчиков / технических экспертов по бэк-энду.