Компиляция и декомпиляция try-with-resources, или рассказ о том, как я фиксил баг и что из этого вышло.
Какое-то время назад backlog рабочего проекта почти опустел, и вверх всплыли различного рода исследовательские задачи. Одна из них звучала весьма интригующе: прикрутить к проекту мутационное тестирования используя PITest. На Хабре уже есть весьма подробный обзор этой библиотеки (с примерами и картинками). Пересказывать эту статью своими словами я не буду, но все же рекомендую с ней предварительно ознакомиться.
Признаюсь, что идеей мутационного тестирования я загорелся. Почти без дополнительных усилий получить инструмент поиска потенциально опасных мест кода — оно того стоит! Я без промедления взялся за дело. На тот момент библиотека была относительно молодой, как следствие — весьма сырой: здесь нужно немного пошаманить с конфигурацией maven’а, там — пропатчить плагин для Sonar’а. Однако через некоторое время я все же смог проверить проект целиком. Результат: сотни выживших мутаций! Эволюция в масштабе на нашем build-сервере.
Засучив рукава я погрузился в работу. В одних тестах не хватает верификаций заглушек, в других вместо логики вообще непонятно что тестируется. Правим, улучшаем, переписываем. В общем, процесс пошел, но число выживших мутаций убывало не так стремительно, как хотелось. Причина была проста: PIT давал огромное количество ложных срабатываний на блоке try-with-resources. Недолгие поиски показали, что баг известен, но до сих пор не исправлен. Что ж, код библиотеки открыт. От чего бы не склонировать его и не посмотреть, в чем же дело?
Я накидал простейший пример, юнит-тест к нему и запустил PITest. Результат перед вами: вместо одной — одиннадцать выживших мутаций, десять из которых указывают на строку с символом “}”. Вызовы методов close и addSupressed наводят на мысль, что к этой строке относится сгенерированный для блока try-with-resources код. Чтобы подтвердить эту догадку, я решил декомпилировать class-файл. Для этого я воспользовался JD-GUI, хотя сейчас рекомендовал бы встроенный декомпилятор IntelliJ IDEA 14.
Догадка подтвердилась, но остался вопрос: как две строчки try-with-resources превратились в десяток строк try-catch-finally? gvsmirnov завещал нам в любой непонятной ситуации качать исходники OpenJDK. Это я и сделал.
Весь код, относящийся к задаче компиляции try-with-resources, разместился между строками 1428 и 1580 класса Lower. Javadoc подсказывает нам, что этот класс предназначен для трансляции синтаксического сахара: никакой магии, только простейшие модификации синтаксического дерева. Все в соответсвии с JLS 14.20.3.
С поведением компилятора разобрались. Осталось понять, почему библиотека пытается мутировать сгенерированный компилятором код и как она устроена. Покопавшись в исходниках, я выяснил следующее. PITest манипулирует исключительно байткодом, загруженным в оперативную память. Он заменяет инструкции по определенным правилам, после чего запускает юнит-тесты. Для работы с байткодом используется ASM.
Первой идеей было перехватить номер строки из метода visitGeneratedTryCatchBlock класса MethodVisitor, а потом просто сообщить библиотеке, какую строку нужно проигнорировать. Подобная функциональность уже была реализована для finally-блока. Однако я был удивлен узнав, что метода visitGeneratedTryCatchBlock не существует. ASM никак не различает сгенерированный компилятором код от сгенерированного программистом. Засада. Пришлось заглянуть в байткод, вывод и форматирование которого любезно предоставил Textifier.
Наивное предположение, что блок try-catch-finally реализован на уровне JVM, не подтвердилось. Никакой специальной инструкции для него нет, только таблица исключений и goto между метками. Получается, стандартными средствами распознать сгенерированный блок не получится. Нужно искать другое решение.
Перед тем, как начать гадать на кофейной гуще, я решил нанести метки байткода на декомпилированный класс. Вот что из этого получилось.
Отчетливо вырисовываются два основных пути выполнения программы:
Друг под другом находятся метки, блоки кода которых совпадают или почти совпадают. В круглых скобках находится код, который будет выполнен в случае, когда метод close бросит исключение. Аналогично в квадратных — когда метод flush. Два пути получилось из-за того, что блок finally был подставлен компилятором дважды. Ну а теперь, чтобы окончательно сломать ваш визуальный парсер: метки в фигурных скобках относятся к строке 11. На эту же строку ссылаются ложные срабатывания PITest.
Вот оно решение! Необходимо выделить минимально повторяющийся набор инструкций. Если такой набор встретится в проверяемом байткоде, да еще и на одной строке — налицо сгенерированный код для блока try-with-resources. Звучит не очень железно, но я решил попробовать. Ниже список инструкций, на котором я в итоге остановился.
Примерно так его можно сопоставить коду в finally-блоке.
“Не так уж и сложно”, — подумал я после нескольких дней напряженной работы. Накидал еще несколько примеров; написал тесты, которые их используют. Все отлично, все работает. Попытался собрать PITest, чтобы запустить его на живом коде: упали тесты. Не те, что я написал; другие.
Итак, код перешел из стадии “не компилируется” в стадию “не работает”. Упал один из существовавших до этого тестов. Откатился — работает. Внутри теста проверяется файл Java7TryWithResources.class.bin, который уже был в проекте. Распечатав байткод, я не поверил своим глазам: для компиляции try-with-resources использован совершенно другой порядок инструкций!
Стараясь не поддаваться панике, я начал проверять все находившиеся под рукой компиляторы. С javac из Oracle JDK я работал, javac из OpenJDK ожидаемо дал аналогичный результат. Попробовал разные версии: безрезультатно. Настал черед компиляторов, которых под рукой не было. Eclipse Compiler for Java, ECJ. Скомпилировал, распечатал байткод — на первый взгляд похож на тот, что я ищу.
После этого я решил декомпилировать полученный class-файл. Результат работы декомпилятора обратно компилироваться отказался. Ну ничего, с этим уже можно работать. Руками приведя программный код в соответствие с байткодом, я получил следующее.
ECJ использует совершенно другой подход для компиляции try-with-resources. Меток заметно меньше, блоки кода заметно больше. Вместо раздутой таблицы, исключения просто пробрасываются на уровень выше. В примерах посложнее можно заметить, что получается этакая матрешка.
Что же под капотом? Я снова пошел качать исходники, на этот раз ECJ. Компиляция оператора try прячется в файле TryStatement. На этот раз никаких деревьев, только opcodes, только хардкор. Байткод, отвечающий за try-with-resources, генерируется между строками 500 и 604. По истории коммитов хорошо видно, что тело блока try просто обрамили цепочкой вызовов создания и закрытия ресурсов.
Т.к. нет подстановки finally-блока, то нет и дублирования кода. Однако из-за вложенности, одинаковые действия повторяются для разных исключений. Этим я и воспользовался. Набор инструкций для ECJ выглядит следующим образом.
А так выглядит соответствующий им java-код.
Что же с остальными компиляторами? Оказалось, что AspectJ генерирует почти такой же байткод, что и ECJ. Для него отдельную последовательность придумывать не пришлось. Компилятор от IBM я так и не смог скачать (да и не особо хотелось). Остальные компиляторы были проигнорированы в следствие малой распространенности.
Внимательный читатель уже заметил, что набор инструкций для javac не учитывает один нюанс. Для вызова методов класса и интерфейса на самом деле используются разные инструкции: INVOKEVIRTUAL и INVOKEINTERFACE соответственно. Описанная выше реализация учитывает только первый случай и не учитывает второй. Ну ничего, это не сложно исправить.
Итак, что же получилось в итоге?
Во-первых, основным результатом работы стал патч, исправляющий упомянутый в начале статьи баг. Почти весь код уместился в одном классе (не считая тестов), который на текущий момент выглядит следующим образом: TryWithResourcesMethodVisitor. Призываю всех критиковать и предлагать свои оптимальные варианты решения данной задачи.
Во-вторых, я узнал, какие бывают способы компиляции блока try-with-resources. Как следствие, я разобрался с тем, как выглядит try-catch-finally на уровне байткода. Ну а побочным продуктом стал перевод статьи, которую я уже упоминал выше по тексту.
В-третьих, я написал эту статью, где обо всем вам рассказал. Возможно, теперь кто-то из вас сможет увеличить фундаментальный коэффициент растаращивания используя полученные знания.
А где же польза и мораль, спросите вы? Оставляю их поиск читателю. Замечу только, что я получил удовольствие, пока писал эту статью. Надеюсь, вы получили его от чтения. До новых встреч!
P.S. В качестве бонуса предлагаю посмотреть на ранние предложения к реализации try-with-resources от Joshua Bloch.
Выглядит забавно.
Введение
Какое-то время назад backlog рабочего проекта почти опустел, и вверх всплыли различного рода исследовательские задачи. Одна из них звучала весьма интригующе: прикрутить к проекту мутационное тестирования используя PITest. На Хабре уже есть весьма подробный обзор этой библиотеки (с примерами и картинками). Пересказывать эту статью своими словами я не буду, но все же рекомендую с ней предварительно ознакомиться.
Признаюсь, что идеей мутационного тестирования я загорелся. Почти без дополнительных усилий получить инструмент поиска потенциально опасных мест кода — оно того стоит! Я без промедления взялся за дело. На тот момент библиотека была относительно молодой, как следствие — весьма сырой: здесь нужно немного пошаманить с конфигурацией maven’а, там — пропатчить плагин для Sonar’а. Однако через некоторое время я все же смог проверить проект целиком. Результат: сотни выживших мутаций! Эволюция в масштабе на нашем build-сервере.
Засучив рукава я погрузился в работу. В одних тестах не хватает верификаций заглушек, в других вместо логики вообще непонятно что тестируется. Правим, улучшаем, переписываем. В общем, процесс пошел, но число выживших мутаций убывало не так стремительно, как хотелось. Причина была проста: PIT давал огромное количество ложных срабатываний на блоке try-with-resources. Недолгие поиски показали, что баг известен, но до сих пор не исправлен. Что ж, код библиотеки открыт. От чего бы не склонировать его и не посмотреть, в чем же дело?
Разбираемся в причинах
Я накидал простейший пример, юнит-тест к нему и запустил PITest. Результат перед вами: вместо одной — одиннадцать выживших мутаций, десять из которых указывают на строку с символом “}”. Вызовы методов close и addSupressed наводят на мысль, что к этой строке относится сгенерированный для блока try-with-resources код. Чтобы подтвердить эту догадку, я решил декомпилировать class-файл. Для этого я воспользовался JD-GUI, хотя сейчас рекомендовал бы встроенный декомпилятор IntelliJ IDEA 14.
public static void main(String[] args) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Throwable var2 = null;
try {
baos.flush();
} catch (Throwable var11) {
var2 = var11;
throw var11;
} finally {
if (baos != null) {
if (var2 != null) {
try {
baos.close();
} catch (Throwable var10) {
var2.addSuppressed(var10);
}
} else {
baos.close();
}
}
}
}
Догадка подтвердилась, но остался вопрос: как две строчки try-with-resources превратились в десяток строк try-catch-finally? gvsmirnov завещал нам в любой непонятной ситуации качать исходники OpenJDK. Это я и сделал.
Весь код, относящийся к задаче компиляции try-with-resources, разместился между строками 1428 и 1580 класса Lower. Javadoc подсказывает нам, что этот класс предназначен для трансляции синтаксического сахара: никакой магии, только простейшие модификации синтаксического дерева. Все в соответсвии с JLS 14.20.3.
С поведением компилятора разобрались. Осталось понять, почему библиотека пытается мутировать сгенерированный компилятором код и как она устроена. Покопавшись в исходниках, я выяснил следующее. PITest манипулирует исключительно байткодом, загруженным в оперативную память. Он заменяет инструкции по определенным правилам, после чего запускает юнит-тесты. Для работы с байткодом используется ASM.
Первой идеей было перехватить номер строки из метода visitGeneratedTryCatchBlock класса MethodVisitor, а потом просто сообщить библиотеке, какую строку нужно проигнорировать. Подобная функциональность уже была реализована для finally-блока. Однако я был удивлен узнав, что метода visitGeneratedTryCatchBlock не существует. ASM никак не различает сгенерированный компилятором код от сгенерированного программистом. Засада. Пришлось заглянуть в байткод, вывод и форматирование которого любезно предоставил Textifier.
Байткод метода main класса TryExample
// access flags 0x9
public static main([Ljava/lang/String;)V throws java/io/IOException
TRYCATCHBLOCK L0 L1 L2 java/lang/Throwable
TRYCATCHBLOCK L3 L4 L5 java/lang/Throwable
TRYCATCHBLOCK L3 L4 L6 null
TRYCATCHBLOCK L7 L8 L9 java/lang/Throwable
TRYCATCHBLOCK L5 L10 L6 null
L11
LINENUMBER 12 L11
NEW java/io/ByteArrayOutputStream
DUP
INVOKESPECIAL java/io/ByteArrayOutputStream.<init> ()V
ASTORE 1
L12
ACONST_NULL
ASTORE 2
L3
LINENUMBER 13 L3
ALOAD 1
INVOKEVIRTUAL java/io/ByteArrayOutputStream.flush ()V
L4
LINENUMBER 14 L4
ALOAD 1
IFNULL L13
ALOAD 2
IFNULL L14
L0
ALOAD 1
INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V
L1
GOTO L13
L2
FRAME FULL [[Ljava/lang/String; java/io/ByteArrayOutputStream java/lang/Throwable] [java/lang/Throwable]
ASTORE 3
L15
ALOAD 2
ALOAD 3
INVOKEVIRTUAL java/lang/Throwable.addSuppressed (Ljava/lang/Throwable;)V
L16
GOTO L13
L14
FRAME SAME
ALOAD 1
INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V
GOTO L13
L5
LINENUMBER 12 L5
FRAME SAME1 java/lang/Throwable
ASTORE 3
ALOAD 3
ASTORE 2
ALOAD 3
ATHROW
L6
LINENUMBER 14 L6
FRAME SAME1 java/lang/Throwable
ASTORE 4
L10
ALOAD 1
IFNULL L17
ALOAD 2
IFNULL L18
L7
ALOAD 1
INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V
L8
GOTO L17
L9
FRAME FULL [[Ljava/lang/String; java/io/ByteArrayOutputStream java/lang/Throwable T java/lang/Throwable] [java/lang/Throwable]
ASTORE 5
L19
ALOAD 2
ALOAD 5
INVOKEVIRTUAL java/lang/Throwable.addSuppressed (Ljava/lang/Throwable;)V
L20
GOTO L17
L18
FRAME SAME
ALOAD 1
INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V
L17
FRAME SAME
ALOAD 4
ATHROW
L13
LINENUMBER 15 L13
FRAME FULL [[Ljava/lang/String;] []
RETURN
L21
LOCALVARIABLE x2 Ljava/lang/Throwable; L15 L16 3
LOCALVARIABLE x2 Ljava/lang/Throwable; L19 L20 5
LOCALVARIABLE baos Ljava/io/ByteArrayOutputStream; L12 L13 1
LOCALVARIABLE args [Ljava/lang/String; L11 L21 0
MAXSTACK = 2
MAXLOCALS = 6
Наивное предположение, что блок try-catch-finally реализован на уровне JVM, не подтвердилось. Никакой специальной инструкции для него нет, только таблица исключений и goto между метками. Получается, стандартными средствами распознать сгенерированный блок не получится. Нужно искать другое решение.
А что, если…
Перед тем, как начать гадать на кофейной гуще, я решил нанести метки байткода на декомпилированный класс. Вот что из этого получилось.
public static void main(String[] args) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // L11
Throwable primaryExc = null; // L12
try {
baos.flush(); // L3
} catch (Throwable t) { // L5
primaryExc = t;
throw t;
} finally { // L6
if (baos != null) { // L4 L10
if (primaryExc != null) {
try {
baos.close(); // L0 L7
} catch (Throwable suppressedExc) { // L2 L9
primaryExc.addSuppressed(suppressedExc); // L15 L19
} // L1 L16 L8 L20
} else {
baos.close(); // L14 L18
}
} // L17
} // L13
}
Отчетливо вырисовываются два основных пути выполнения программы:
L11 L12 L3 {L4 [L0 (L2 L15 L16) L1] L14} L13 L11 L12 L3 [L5 {L6] L10 [L7 (L9 L19 L20) L8] L18 L17}
Друг под другом находятся метки, блоки кода которых совпадают или почти совпадают. В круглых скобках находится код, который будет выполнен в случае, когда метод close бросит исключение. Аналогично в квадратных — когда метод flush. Два пути получилось из-за того, что блок finally был подставлен компилятором дважды. Ну а теперь, чтобы окончательно сломать ваш визуальный парсер: метки в фигурных скобках относятся к строке 11. На эту же строку ссылаются ложные срабатывания PITest.
Вот оно решение! Необходимо выделить минимально повторяющийся набор инструкций. Если такой набор встретится в проверяемом байткоде, да еще и на одной строке — налицо сгенерированный код для блока try-with-resources. Звучит не очень железно, но я решил попробовать. Ниже список инструкций, на котором я в итоге остановился.
private static final List<Integer> JAVAC_CLASS_INS_SEQUENCE = Arrays.asList(
ASTORE, // store throwable
ALOAD, IFNULL, // closeable != null
ALOAD, IFNULL, // localThrowable2 != null
ALOAD, INVOKEVIRTUAL, GOTO, // closeable.close()
ASTORE, // Throwable x2
ALOAD, ALOAD, INVOKEVIRTUAL, GOTO, // localThrowable2.addSuppressed(x2)
ALOAD, INVOKEVIRTUAL, // closeable.close()
ALOAD, ATHROW); // throw throwable
Примерно так его можно сопоставить коду в finally-блоке.
} finally {
if (closeable != null) { // IFNULL
if (localThrowable2 != null) { // IFNULL
try {
closeable.close(); // INVOKEVIRTUAL or INVOKEINTERFACE
} catch (Throwable x2) {
localThrowable2.addSuppressed(x2); // INVOKEVIRTUAL
}
} else {
closeable.close(); // INVOKEVIRTUAL or INVOKEINTERFACE
}
}
} // ATHROW
“Не так уж и сложно”, — подумал я после нескольких дней напряженной работы. Накидал еще несколько примеров; написал тесты, которые их используют. Все отлично, все работает. Попытался собрать PITest, чтобы запустить его на живом коде: упали тесты. Не те, что я написал; другие.
Компиляторы бывают разные
Итак, код перешел из стадии “не компилируется” в стадию “не работает”. Упал один из существовавших до этого тестов. Откатился — работает. Внутри теста проверяется файл Java7TryWithResources.class.bin, который уже был в проекте. Распечатав байткод, я не поверил своим глазам: для компиляции try-with-resources использован совершенно другой порядок инструкций!
Стараясь не поддаваться панике, я начал проверять все находившиеся под рукой компиляторы. С javac из Oracle JDK я работал, javac из OpenJDK ожидаемо дал аналогичный результат. Попробовал разные версии: безрезультатно. Настал черед компиляторов, которых под рукой не было. Eclipse Compiler for Java, ECJ. Скомпилировал, распечатал байткод — на первый взгляд похож на тот, что я ищу.
Байткод метода main класса TryExample by ECJ
// access flags 0x9
public static main([Ljava/lang/String;)V throws java/io/IOException
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L3 L4 L4 null
L5
LINENUMBER 12 L5
ACONST_NULL
ASTORE 1
ACONST_NULL
ASTORE 2
L3
NEW java/io/ByteArrayOutputStream
DUP
INVOKESPECIAL java/io/ByteArrayOutputStream.<init> ()V
ASTORE 3
L0
LINENUMBER 13 L0
ALOAD 3
INVOKEVIRTUAL java/io/ByteArrayOutputStream.flush ()V
L1
LINENUMBER 14 L1
ALOAD 3
IFNULL L6
ALOAD 3
INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V
GOTO L6
L2
FRAME FULL [[Ljava/lang/String; java/lang/Throwable java/lang/Throwable java/io/ByteArrayOutputStream] [java/lang/Throwable]
ASTORE 1
ALOAD 3
IFNULL L7
ALOAD 3
INVOKEVIRTUAL java/io/ByteArrayOutputStream.close ()V
L7
FRAME CHOP 1
ALOAD 1
ATHROW
L4
FRAME SAME1 java/lang/Throwable
ASTORE 2
ALOAD 1
IFNONNULL L8
ALOAD 2
ASTORE 1
GOTO L9
L8
FRAME SAME
ALOAD 1
ALOAD 2
IF_ACMPEQ L9
ALOAD 1
ALOAD 2
INVOKEVIRTUAL java/lang/Throwable.addSuppressed (Ljava/lang/Throwable;)V
L9
FRAME SAME
ALOAD 1
ATHROW
L6
LINENUMBER 15 L6
FRAME CHOP 2
RETURN
MAXSTACK = 2
MAXLOCALS = 4
После этого я решил декомпилировать полученный class-файл. Результат работы декомпилятора обратно компилироваться отказался. Ну ничего, с этим уже можно работать. Руками приведя программный код в соответствие с байткодом, я получил следующее.
public static void main(String[] paramArrayOfString) throws Throwable {
Throwable primaryExceptionVariable = null; // L5
Throwable caughtThrowableVariable = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // L3
try {
baos.flush(); // L0
} catch (Throwable t) {
primaryExceptionVariable = t; // L2
throw primaryExceptionVariable; // L7
} finally {
if (baos != null) { // L1
baos.close();
}
}
} catch (Throwable t) {
caughtThrowableVariable = t; // L4
if (primaryExceptionVariable == null) {
primaryExceptionVariable = caughtThrowableVariable;
} else if (primaryExceptionVariable != caughtThrowableVariable) { // L8
primaryExceptionVariable.addSuppressed(caughtThrowableVariable);
}
throw primaryExceptionVariable; // L9
} // L6
}
ECJ использует совершенно другой подход для компиляции try-with-resources. Меток заметно меньше, блоки кода заметно больше. Вместо раздутой таблицы, исключения просто пробрасываются на уровень выше. В примерах посложнее можно заметить, что получается этакая матрешка.
Что же под капотом? Я снова пошел качать исходники, на этот раз ECJ. Компиляция оператора try прячется в файле TryStatement. На этот раз никаких деревьев, только opcodes, только хардкор. Байткод, отвечающий за try-with-resources, генерируется между строками 500 и 604. По истории коммитов хорошо видно, что тело блока try просто обрамили цепочкой вызовов создания и закрытия ресурсов.
Т.к. нет подстановки finally-блока, то нет и дублирования кода. Однако из-за вложенности, одинаковые действия повторяются для разных исключений. Этим я и воспользовался. Набор инструкций для ECJ выглядит следующим образом.
private static final List<Integer> ECJ_INS_SEQUENCE = Arrays.asList(
ASTORE, // store throwable2
ALOAD, IFNONNULL, // if (throwable1 == null)
ALOAD, ASTORE, GOTO, // throwable1 = throwable2;
ALOAD, ALOAD, IF_ACMPEQ, // if (throwable1 != throwable2) {
ALOAD, ALOAD, INVOKEVIRTUAL, // throwable1.addSuppressed(throwable2)
ALOAD, ATHROW); // throw throwable1
А так выглядит соответствующий им java-код.
if (throwable1 == null) { // IFNONNULL
throwable1 = throwable2;
} else {
if (throwable1 != throwable2) { // IF_ACMPEQ
throwable1.addSuppressed(throwable2); // INVOKEVIRTUAL
}
} // ATHROW
Что же с остальными компиляторами? Оказалось, что AspectJ генерирует почти такой же байткод, что и ECJ. Для него отдельную последовательность придумывать не пришлось. Компилятор от IBM я так и не смог скачать (да и не особо хотелось). Остальные компиляторы были проигнорированы в следствие малой распространенности.
Результаты
Внимательный читатель уже заметил, что набор инструкций для javac не учитывает один нюанс. Для вызова методов класса и интерфейса на самом деле используются разные инструкции: INVOKEVIRTUAL и INVOKEINTERFACE соответственно. Описанная выше реализация учитывает только первый случай и не учитывает второй. Ну ничего, это не сложно исправить.
Итак, что же получилось в итоге?
Во-первых, основным результатом работы стал патч, исправляющий упомянутый в начале статьи баг. Почти весь код уместился в одном классе (не считая тестов), который на текущий момент выглядит следующим образом: TryWithResourcesMethodVisitor. Призываю всех критиковать и предлагать свои оптимальные варианты решения данной задачи.
Во-вторых, я узнал, какие бывают способы компиляции блока try-with-resources. Как следствие, я разобрался с тем, как выглядит try-catch-finally на уровне байткода. Ну а побочным продуктом стал перевод статьи, которую я уже упоминал выше по тексту.
В-третьих, я написал эту статью, где обо всем вам рассказал. Возможно, теперь кто-то из вас сможет увеличить фундаментальный коэффициент растаращивания используя полученные знания.
А где же польза и мораль, спросите вы? Оставляю их поиск читателю. Замечу только, что я получил удовольствие, пока писал эту статью. Надеюсь, вы получили его от чтения. До новых встреч!
P.S. В качестве бонуса предлагаю посмотреть на ранние предложения к реализации try-with-resources от Joshua Bloch.
Stumbled on original ARM block (try-with-resources) proposals, if anyone's curious. V1: https://t.co/Qngv2STN1W, V2: https://t.co/YiR1RvyZWg
— Joshua Bloch (@joshbloch) 13 июня 2015
Выглядит забавно.
{
final LocalVariableDeclaration ;
boolean #suppressSecondaryException = false;
try Block catch (final Throwable #t) {
#suppressSecondaryException = true;
throw #t;
} finally {
if (#suppressSecondaryException)
try { localVar.close(); } catch(Exception #ignore) { }
else
localVar.close();
}
}
Комментарии (6)
leventov
16.06.2015 15:39Тоже недавно имел дело с ECJ, та еще забагованная шняга.
Интересно, а чем компилирует IntelliJ? Им же тоже надо исходники в AST переводить. Или это не имеет оформления библиотеки, а просто набор классов в их проекте?grossws
16.06.2015 16:04IDEA использует javac или ecj, по выбору. По умолчанию у меня javac, но я мог когда-то переключаться.
artspb Автор
16.06.2015 16:10В настройках IDEA можно выбрать те же три, что упомянуты в статье, и Groovy-Eclipse.
ekapinos
Вот те на, всего лишь синтаксический сахар. А столько шума эта фитча наделала. Спасибо за Ваш труд.
grossws
Да и так было понятно, что сахар. И плюс ввести AutoClosable и использовать его в rt.jar