Такая техника широко применяется для реализации AOP, создания тестовых фреймворков, ORM. Особенно хочется отметить terracota, продукт с красивой идеей кластеризации jvm и на всю катушку использующей модификации байт-кода. Эта заметка будет посвящена обзору структуры байт-кода, первой части этой сильной связки.
Каждому классу в java соответствует один откомпилированный файл. Это справедливо даже для подклассов или анонимным классов. Такой файл содержит информацию об имени класса, его родители, список интерфейсов которые он реализует, перечисление его полей и методов. Важно отметить, что после компиляции информации которая содержит директива import теряется и все классы именуются теперь через полный путь. Например в место String будет записано java/lang/String.
Самое интересное как будут выглядеть методы класса в байт-коде. Будем наблюдать во, что трансформируется следующий класс:
package org;
class Test {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Метод getName будет описываться в байт-коде следующим образом.
public getName()Ljava/lang/String;
ALOAD 0
GETFIELD org/Test name
ARETURN
Начнем с заголовка. В нем содержится информация о название метода то, что метод вызывается без параметров и тип возвращаемого аргумента.
Байт-кода стеко-ориентированный язык, похожий по своей структуре на ассемблер. Что бы произвести операции с данными их сначала нужно положит на стек. Мы хотим взять поле у объект. Что бы это сделять нужно его положить в стек. В байт-коде нет имен переменных, у них есть номера. Нулевой номер у ссылки на текущий объект или у переменой this. Потом идут параметры исполняемого метода. Затем остальные переменные.
Команда ALOAD 0 кладет переменную this на стек. Что бы на стек положить тип данных, отличный от ссылки нужно воспользоваться другой командой. Для long будет LLOAD, а для doubles[] будет DALOAD.
Следующая команда GETFIELD, убирает со стека ссылку на объект и кладет примитивный тип или ссылку на поле данного объекта. У нее есть два параметра. Первый имя класса, второй имя переменной. Если же переменная статическая, то предварительно класть на стек ничего не нужно, а команду нужно заменить на GETSTATIC с теме же параметрами.
Последняя команда говорит, что метод завершен и возвращает значения типа ссылки со стека.
Сеттер имеет немного более сложную структуру.
public setName(Ljava/lang/String;)V
ALOAD 0
ALOAD 1
PUTFIELD org/Test name
RETURN
Данный метод ничего не возвращает. Первые две команды кладут на стек переменную this и параметр исполняемого метода. Затем вызывается команда PUTFIELD (PUTSTATIC для статического поля) которая установит значение поля объекта и уберет со стека последние два значения. Последняя команда выход из метода.
Добавим к нашему объект еще пару методов и посмотрим какой байт-код им соответствует.
public void forTest(Boolean b){
System.out.prinln(b);
}
public Long testMethods(Collection<Long> testInterface){
Long a = System.curretM();
forTest(testInterface.contains(a));
return a;
}
testMethod имеет следующие представление.
INVOKESTATIC java/lang/System currentTimeMillis ()J
LSTORE 2
ALOAD 0
ALOAD 1
LLOAD 2
INVOKESTATIC java/lang/Long valueOf (J)Ljava/lang/Long;
INVOKEINTERFACE java/util/Collection contains (Ljava/lang/Object;)Z
INVOKESTATIC java/lang/Boolean valueOf (Z)Ljava/lang/Boolean;
INVOKEVIRTUAL org/Test forTest (Ljava/lang/Boolean;)V
LLOAD 2
INVOKESTATIC java/lang/Long valueOf (J)Ljava/lang/Long;
ARETURN
Первая команда вызывает статический метод у класса System. Вторая запоминает результат вызова метода currentTimeMillis в переменной со вторым номером. Затем мы кладем переменную this, параметр метода и переменную с номером 2 на стек. Преобразую переменную к типу java/lang/Long. И проверяем, что она у нас содержится в коллекции, вызывая метод у параметра исполняемого. У нас параметр интерфейс, поэтому применяется команда INVOKEINTERFACE. Для метода класса необходимо использовать INVOKEVIRTUAL. Что бы вызвать метод у объект или интерфейса необходимо, что бы на стеке лежал объект, затем параметры вызываемого метода. В результате вызова метода они заменяться на результат или просто уберутся со стека, если метод ничего возвращает. Последняя три команды кладут перемену на стек, превращают ее в объект и возвращают ее как значение метода.
Что бы завершить наш экскурс в байт-код добавим последний метод и посмотрим на циклы и условные операторы.
public void testAriphmentics(){
int i = -17;
while(i < 10){
if(i < 0){
i = i + 7;
}
i = i*13;
}
}
Он в байт-коде будет выглядить так
ACC_FINAL -17
ISTORE 1
Label:L1466604866
ILOAD 1
ACC_FINAL 10
IF_ICMPGE L329949514
ILOAD 1
IFGE L658705244
ILOAD 1
ACC_FINAL 7
IADD
ISTORE 1
Label:L658705244
ILOAD 1
ACC_FINAL 13
IMUL
ISTORE 1
GOTO L1466604866
Label:L329949514
RETURN
Первые две команды инициализирую переменную i (с номером 1) значением -17.
Дальше у нас начинается тело цикла. Для реализации которого потребуются метки,
команда перехода и условный оператор. В теле нашего метода аж целых три меток. Первая метка означает начало цикла. Вторая нужна для условного оператора, а последняя знаменует конец цикла. Уловный оператор имеет один параметр метку перехода. Прежде чем его вызвать сравниваемы значения должны лежать на стеке. Для сравнения с нулем используется отдельная команда. Для каждого типа есть свой оператор сравнения. Для int он IF_ICMPGE. После сравнения сравниваемы значения убираются со стека. Для арифметических действий с двумя переменным их так же как и для условного оператора нужно предварительно положить на стек. После выполнения они снимаются со стека, а на их место кладется результат.
На это краткий экскурс в байт-код закончен, некоторые вопросы такие как исключения, синхронизация не были затронуты. Я надеюсь, что имея представление о байт коде читатель без труда справиться с ними. В следующей части мы рассмотрим инструменты которые применяются для модификации байт-кода.
блин, ты б хоть на хабре выложил без ошибок. читать же трудно.
ReplyDeleteПривет классная статья. У меня вопрос: как из дизассемблированного байт кода получить тот который выполнят JVM?
ReplyDeleteНаверно есть какое-то либа, которая конвертирует.
ReplyDelete???
ВСЕ ПРОЧИТАЙТЕ НАСТОЯЩЕЕ ОТЗЫВ О том, КАК Я ПОЛУЧИЛ СВОЙ КРЕДИТ ОТ КОМПАНИИ LEGIT И ДОВЕРЕННОЙ КРЕДИТНОЙ СРЕДИ Меня зовут Kjerstin Lis, я искал кредит для погашения своих долгов, все, кого я встречал, мошенничали и брали свои деньги, пока я наконец не встретил мистера Бенджамина Брейл Ли Он смог дать мне кредит в размере 450 000 рублей. Он также помог другим моим коллегам. Я говорю как самый счастливый человек во всем мире сегодня, и я сказал себе, что любой кредитор, который спасает мою семью от нашей бедной ситуации, я скажу имя всему миру, и я так счастлив сказать, что моя семья вернулся навсегда, потому что я нуждался в кредите, чтобы начать свою жизнь заново, потому что я одинокая мама с 3 детьми, и весь мир, казалось, висел на мне, пока я не имел в виду, что БОГ послал ссудодателя, который изменил мою жизнь и член моей семьи, БОЖИЙ кредитор, мистер Бенджамин, он был Спасителем БОГом, посланным для спасения моей семьи, и сначала я подумал, что это будет невозможно, пока я не получу кредит, я пригласил его к себе в семью -все вечеринка, от которой он не отказался, и я посоветую всем, кто действительно нуждается в кредите, связаться с г-ном Бенджамином Брейлом Ли по электронной почте (lfdsloans@outlook.com), потому что он самый понимающий и добрый кредитор. когда-либо встречал с заботливым сердцем. Он не знает, что я делаю это, распространяя свою добрую волю ко мне, но я чувствую, что должен поделиться этим со всеми вами, чтобы освободить себя от мошенников, пожалуйста, остерегайтесь подделок и свяжитесь с правильной кредитной компанией. com или whatsapp + 1-989-394-3740. ,
ReplyDelete