LINUX.ORG.RU

История изменений

Исправление dimgel, (текущая версия) :

За долгие годы наработал я следующие варианты метапрограммирования.

  1. Самый мерзкий: кодогенерация в рантайме (в т.ч. спринг). Синтаксис идиотский, хрен увидишь что сгенерил, хрен отладишь. Разве что с точки зрения понимания работы стековой машины JVM забавно, если юзать низкоуровневые инструменты (я юзал https://asm.ow2.io/ и какую-то обёртку поверх него, автоматом расчитывающую размер фрейма под локальные переменные – вроде бы она называлась bytebucket, но чёт не гуглится).

// UPD1: Опять же, какого чёрта делать в рантайме то, что надо делать в билдтайме; из-за всей этой дичи JEE- и спринг-приложения и стартуют так долго.

Далее все варианты – про работу твоего кода в buildtime, что и является метапрограммированием здорового человека. В случае жавы, под каждую задачу рисуется отдельный maven-плагин – модуль приложения; этот модуль-плагин может собираться и тут же собранный запускаться при сборке другого модуля – т.е. всё в рамках одного запуска команды mvn.

  1. Самый тупой: кодогенерация. Парсишь что-то из src (и/или ещё откуда-нибудь), генеришь что-то в target/generated-sources. Под это дело даже отдельные фазы у maven есть. Этим кто только не занимается: например JPA и QueryDSL генерят свои мета-классы, сканируя и парся entity-классы. Я люблю классы сущностей по структуре базы генерить – здесь я, во-первых, не сорцы парсю, а лезу в information_schema (локальной временной базы, на которой предварительно прогоняю миграции другим maven-плагином); а во-вторых, я люблю кодогенерировать в src, а не в target/generated-sources: так удобнее и изменения отслеживать (в какой момент генератор поломали), и не только. Для парсинга категорически рекомендую JavaParser.org: редко я могу что-нибудь искренне похвалить, а манипуляция AST вообще занятие крайне занудное – но эта либа отличная (с учётом того, что в жаве нету pattern matching, с помощью которого в той же scala работать с AST на порядки проще).

// UPD2: «JPA … генерят свои мета-классы» – в смысле, Criteria API.

  1. Развитием идеи кодогенерации целого исходника с нуля является замена кусков исходника: ищем метки ///something-begin/// и ///something-end/// и генерируем код между ними. Тут опять же модифицируемый файл лежит в src. Класс, ищущий такие метки, разбивающий по ним исходник, предоставляющий API типа set(«something», generatedSource) и собирающий исходник из кусков обратно – тривиален.

  2. Ещё круче (хотя и гораздо гиморнее) парсить не метки (что имеет довольно ограниченное применение), а натравить JavaParser.org на исходник, подкрутить распарсенное AST и сериализовать его тем же JavaParser-ом назад в исходник. Слегка поломаются пустые строки и отступы, но терпимо (раньше по крайней мере ломались; например, каменты прижимаются к коду под ними). Я таким образом перегенерировал классы сущностей, сохраняя добавленные вручную программистом методы, не являющиеся вырожденными геттерами-сеттерами (которые добавлялись/удалялись плагином вместе с приватными полями класса при изменении структуры таблицы).

Всеми этими безобразиями с генерацией сорцов / изменениями их по месту можно сделать очень много, но не всё: нельзя нормально разворачивать DSL. Например, если в сорце есть статически-типизированный SQL-запрос, его нельзя удалить и заменить на сгенерированный по нему raw SQL, а генерить этот raw SQL в отдельный файл – настолько уродство, что даже не хочется думать, как до этого сгенерированного в отдельном файле запроса добраться из точки, где вставлен исходный DSL.

Для таких случаев:

  1. Ну и наконец, есть lombok. Это самое «честное» метапрограммирование на AST-макросах: сорцы не меняются, манипуляция AST выполняется в процессе работы javac, результаты манипуляций идут в class-файл. Посмотреть что нагенерилось можно, открыв этот class-файл в IDE (с автоматическим дизассемблированием). Ставить точки останова на сгенерированном коде у меня не выходило (не помню, пытался ли на class-файле), делал stepOver / stepInto вслепую + view variables на каждом шаге.

Самая большая проблема у lombok: чтобы добавить в него свой код, надо его форкать. Форков у него на одном только гитхабе – полторы тыщи, и всем пофиг. И авторы lombok как-то не чешутся делать поддержку расширяемости, и кстати авторы javac тоже не чешутся сделать классы AST public – сейчас lombok инстанциирует их через рефлексию (ну там с кешированием конструкторов, так что всё довольно шустро). Короче, на метапрограммирование в жаве всем насрать. С тоской вспоминаю скалу, где метапрограммирование встроенное, хотя AST в ней в разы более сложное, чем у жавы.

Собирается этот lombok отдельно от проекта, с помощью ant. Там у него свои заморочки.

Ну и кто с AST не работал, тем предстоит познать геморрой. Там ничего не делается в лоб. Простой пример – локальная переменная с инициализатором:

// boolean myVar = true;
mk.VarDef(
    mk.Modifiers(0),
    node.toName("myVar"),
    mk.TypeIdent(CTC_BOOLEAN),
    mk.Literal(CTC_BOOLEAN, 1)
)

В скале, помнится, константа задавалась то ли как Constant(Literal(true)), то ли как Literal(Constant(true)). Но там хотя бы квазицитирование есть, в отличие от. На каждом новом языке, на каждом новом компиляторе – свои заморочки. Хочешь сгенерировать try-catch – пишешь его в коде, навешиваешь на этот код lombok-аннотацию, при старте maven ставишь точку останова внутри lombok и когда до неё доходит дело, смотришь какое AST сгенерил компилятор; потом такое же делаешь в своём плагине, раза со 2-го … 5-го получится. Помнится, я пытался mk.Literal(CTC_BOOLEAN, true) в последней строчке – и оно не работало.

Что там с типизацией, не помню. У JavaParser вроде есть какие-то API для семантического анализа, но я с ними не разбирался. В lombok приезжает вроде уже типизированное дерево, но это не точно.

Можно подумать, что все варианты кроме lombok – для бедных. Но на lombok ты можешь работать только в пределах сорца (ну может, ещё анализировать импортированные типы), а с тупой кодогенерацией я и List<Migration> по списку файлов migration/*.java генерю, и, повторюсь, классы по базе. В общем, всё это можно совмещать.

Исправление dimgel, :

За долгие годы наработал я следующие варианты метапрограммирования.

  1. Самый мерзкий: кодогенерация в рантайме (в т.ч. спринг). Синтаксис идиотский, хрен увидишь что сгенерил, хрен отладишь. Разве что с точки зрения понимания работы стековой машины JVM забавно, если юзать низкоуровневые инструменты (я юзал https://asm.ow2.io/ и какую-то обёртку поверх него, автоматом расчитывающую размер фрейма под локальные переменные – вроде бы она называлась bytebucket, но чёт не гуглится).

// UPD1: Опять же, какого чёрта делать в рантайме то, что надо делать в билдтайме; из-за всей этой дичи JEE- и спринг-приложения и стартуют так долго.

Далее все варианты – про работу твоего кода в buildtime, что и является метапрограммированием здорового человека. В случае жавы, под каждую задачу рисуется отдельный maven-плагин – модуль приложения; этот модуль-плагин может собираться и тут же собранный запускаться при сборке другого модуля – т.е. всё в рамках одного запуска команды mvn.

  1. Самый тупой: кодогенерация. Парсишь что-то из src (и/или ещё откуда-нибудь), генеришь что-то в target/generated-sources. Под это дело даже отдельные фазы у maven есть. Этим кто только не занимается: например JPA и QueryDSL генерят свои мета-классы, сканируя и парся entity-классы. Я люблю классы сущностей по структуре базы генерить – здесь я, во-первых, не сорцы парсю, а лезу в information_schema (локальной временной базы, на которой предварительно прогоняю миграции другим maven-плагином); а во-вторых, я люблю кодогенерировать в src, а не в target/generated-sources: так удобнее и изменения отслеживать (в какой момент генератор поломали), и не только. Для парсинга категорически рекомендую JavaParser.org: редко я могу что-нибудь искренне похвалить, а манипуляция AST вообще занятие крайне занудное – но эта либа отличная (с учётом того, что в жаве нету pattern matching, с помощью которого в той же scala работать с AST на порядки проще).

// UPD2: «JPA … генерят свои мета-классы» – в смысле, Criteria API.

  1. Развитием идеи кодогенерации целого исходника с нуля является замена кусков исходника: ищем метки ///something-begin/// и ///something-end/// и генерируем код между ними. Тут опять же модифицируемый файл лежит в src. Класс, ищущий такие метки, разбивающий по ним исходник, предоставляющий API типа set(«something», generatedSource) и собирающий исходник из кусков обратно – тривиален.

  2. Ещё круче (хотя и гораздо гиморнее) парсить не метки (что имеет довольно ограниченное применение), а натравить JavaParser.org на исходник, подкрутить распарсенное AST и сериализовать его тем же JavaParser-ом назад в исходник. Слегка поломаются пустые строки и отступы, но терпимо (раньше по крайней мере ломались; например, каменты прижимаются к коду под ними). Я таким образом перегенерировал классы сущностей, сохраняя добавленные вручную программистом методы, не являющиеся вырожденными геттерами-сеттерами (которые добавлялись/удалялись плагином вместе с приватными полями класса при изменении структуры таблицы).

Всеми этими безобразиями с генерацией сорцов / изменениями их по месту можно сделать очень много, но не всё: нельзя нормально разворачивать DSL. Например, если в сорце есть статически-типизированный SQL-запрос, его нельзя удалить и заменить на сгенерированный по нему raw SQL, а генерить этот raw SQL в отдельный файл – настолько уродство, что даже не хочется думать, как до этого сгенерированного в отдельном файле запроса добраться из точки, где вставлен исходный DSL.

Для таких случаев:

  1. Ну и наконец, есть lombok. Это самое «честное» метапрограммирование на AST-макросах: сорцы не меняются, манипуляция AST выполняется в процессе работы javac, результаты манипуляций идут в class-файл. Посмотреть что нагенерилось можно, открыв этот class-файл в IDE (с автоматическим дизассемблированием). Ставить точки останова на сгенерированном коде у меня не выходило (не помню, пытался ли на class-файле), делал stepOver / stepInto вслепую + view variables на каждом шаге.

Самая большая проблема у lombok: чтобы добавить в него свой код, надо его форкать. Форков у него на одном только гитхабе – полторы тыщи, и всем пофиг. И авторы lombok как-то не чешутся делать поддержку расширяемости, и кстати авторы javac тоже не чешутся сделать классы AST public – сейчас lombok инстанциирует их через рефлексию (ну там с кешированием конструкторов, так что всё довольно шустро). Короче, на метапрограммирование в жаве всем насрать. С тоской вспоминаю скалу, где метапрограммирование встроенное, хотя AST в ней в разы более сложное, чем у жавы.

Собирается этот lombok отдельно от проекта, с помощью ant. Там у него свои заморочки.

Ну и кто с AST не работал, тем предстоит познать геморрой. Там ничего не делается в лоб. Простой пример – локальная переменная с инициализатором:

// boolean myVar = true;
mk.VarDef(
    mk.Modifiers(0),
    node.toName("myVar"),
    mk.TypeIdent(CTC_BOOLEAN),
    mk.Literal(CTC_BOOLEAN, 1)
)

В скале, помнится, константа задавалась то ли как Constant(Literal(true)), то ли как Literal(Constant(true)). Но там хотя бы квазицитирование есть, в отличие от. На каждом новом языке, на каждом новом компиляторе – свои заморочки. Хочешь сгенерировать try-catch – пишешь его в коде, навешиваешь на этот код lombok-аннотацию, при старте maven ставишь точку останова внутри lombok и когда до неё доходит дело, смотришь какое AST сгенерил компилятор; потом такое же делаешь в своём плагине, раза со 2-го … 5-го получится. Помнится, я пытался mk.Literal(CTC_BOOLEAN, true) в последней строчке – и оно не работало.

Что там с типизацией, не помню. У JavaParser вроде есть какие-то API для семантического анализа, но я с ними не разбирался. В lombok приезжает вроде уже типизированное дерево, но это не точно.

Можно подумать, что все варианты кроме lombok – для бедных. Но на lombok ты можешь работать только в пределах сорца (ну может, ещё анализировать импортированные типы), а с тупой кодогенерацией я и List по списку файлов migration/*.java генерю, и, повторюсь, классы по базе. В общем, всё это можно совмещать.

Исправление dimgel, :

За долгие годы наработал я следующие варианты метапрограммирования.

  1. Самый мерзкий: кодогенерация в рантайме (в т.ч. спринг). Синтаксис идиотский, хрен увидишь что сгенерил, хрен отладишь. Разве что с точки зрения понимания работы стековой машины JVM забавно, если юзать низкоуровневые инструменты (я юзал https://asm.ow2.io/ и какую-то обёртку поверх него, автоматом расчитывающую размер фрейма под локальные переменные – вроде бы она называлась bytebucket, но чёт не гуглится).

// UPD1: Опять же, какого чёрта делать в рантайме то, что надо делать в билдтайме; из-за всей этой дичи JEE- и спринг-приложения и стартуют так долго.

Далее все варианты – про работу твоего кода в buildtime, что и является метапрограммированием здорового человека. В случае жавы, под каждую задачу рисуется отдельный maven-плагин – модуль приложения; этот модуль-плагин может собираться и тут же собранный запускаться при сборке другого модуля – т.е. всё в рамках одного запуска команды mvn.

  1. Самый тупой: кодогенерация. Парсишь что-то из src (и/или ещё откуда-нибудь), генеришь что-то в target/generated-sources. Под это дело даже отдельные фазы у maven есть. Этим кто только не занимается: например JPA и QueryDSL генерят свои мета-классы, сканируя и парся entity-классы. Я люблю классы сущностей по структуре базы генерить – здесь я, во-первых, не сорцы парсю, а лезу в information_schema (локальной временной базы, на которой предварительно прогоняю миграции другим maven-плагином); а во-вторых, я люблю кодогенерировать в src, а не в target/generated-sources: так удобнее и изменения отслеживать (в какой момент генератор поломали), и не только. Для парсинга категорически рекомендую JavaParser.org: редко я могу что-нибудь искренне похвалить, а манипуляция AST вообще занятие крайне занудное – но эта либа отличная (с учётом того, что в жаве нету pattern matching, с помощью которого в той же scala работать с AST на порядки проще).

// UPD2: «JPA … генерят свои мета-классы» – в смысле, Criteria API.

  1. Развитием идеи кодогенерации целого исходника с нуля является замена кусков исходника: ищем метки ///something-begin/// и ///something-end/// и генерируем код между ними. Тут опять же генерируемый файл лежит в src. Класс, ищущий такие метки, разбивающий по ним исходник, предоставляющий API типа set(«something», generatedSource) и собирающий исходник из кусков обратно – тривиален.

  2. Ещё круче (хотя и гораздо гиморнее) парсить не метки (что имеет довольно ограниченное применение), а натравить JavaParser.org на исходник, подкрутить распарсенное AST и сериализовать его тем же JavaParser-ом назад в исходник. Слегка поломаются пустые строки и отступы, но терпимо (раньше по крайней мере ломались; например, каменты прижимаются к коду под ними). Я таким образом перегенерировал классы сущностей, сохраняя добавленные вручную программистом методы, не являющиеся вырожденными геттерами-сеттерами (которые добавлялись/удалялись плагином вместе с приватными полями класса при изменении структуры таблицы).

Всеми этими безобразиями с генерацией сорцов / изменениями их по месту можно сделать очень много, но не всё: нельзя нормально разворачивать DSL. Например, если в сорце есть статически-типизированный SQL-запрос, его нельзя удалить и заменить на сгенерированный по нему raw SQL, а генерить этот raw SQL в отдельный файл – настолько уродство, что даже не хочется думать, как до этого сгенерированного в отдельном файле запроса добраться из точки, где вставлен исходный DSL.

Для таких случаев:

  1. Ну и наконец, есть lombok. Это самое «честное» метапрограммирование на AST-макросах: сорцы не меняются, манипуляция AST выполняется в процессе работы javac, результаты манипуляций идут в class-файл. Посмотреть что нагенерилось можно, открыв этот class-файл в IDE (с автоматическим дизассемблированием). Ставить точки останова на сгенерированном коде у меня не выходило (не помню, пытался ли на class-файле), делал stepOver / stepInto вслепую + view variables на каждом шаге.

Самая большая проблема у lombok: чтобы добавить в него свой код, надо его форкать. Форков у него на одном только гитхабе – полторы тыщи, и всем пофиг. И авторы lombok как-то не чешутся делать поддержку расширяемости, и кстати авторы javac тоже не чешутся сделать классы AST public – сейчас lombok инстанциирует их через рефлексию (ну там с кешированием конструкторов, так что всё довольно шустро). Короче, на метапрограммирование в жаве всем насрать. С тоской вспоминаю скалу, где метапрограммирование встроенное, хотя AST в ней в разы более сложное, чем у жавы.

Собирается этот lombok отдельно от проекта, с помощью ant. Там у него свои заморочки.

Ну и кто с AST не работал, тем предстоит познать геморрой. Там ничего не делается в лоб. Простой пример – локальная переменная с инициализатором:

// boolean myVar = true;
mk.VarDef(
    mk.Modifiers(0),
    node.toName("myVar"),
    mk.TypeIdent(CTC_BOOLEAN),
    mk.Literal(CTC_BOOLEAN, 1)
)

В скале, помнится, константа задавалась то ли как Constant(Literal(true)), то ли как Literal(Constant(true)). Но там хотя бы квазицитирование есть, в отличие от. На каждом новом языке, на каждом новом компиляторе – свои заморочки. Хочешь сгенерировать try-catch – пишешь его в коде, навешиваешь на этот код lombok-аннотацию, при старте maven ставишь точку останова внутри lombok и когда до неё доходит дело, смотришь какое AST сгенерил компилятор; потом такое же делаешь в своём плагине, раза со 2-го … 5-го получится. Помнится, я пытался mk.Literal(CTC_BOOLEAN, true) в последней строчке – и оно не работало.

Что там с типизацией, не помню. У JavaParser вроде есть какие-то API для семантического анализа, но я с ними не разбирался. В lombok приезжает вроде уже типизированное дерево, но это не точно.

Можно подумать, что все варианты кроме lombok – для бедных. Но на lombok ты можешь работать только в пределах сорца (ну может, ещё анализировать импортированные типы), а с тупой кодогенерацией я и List по списку файлов migration/*.java генерю, и, повторюсь, классы по базе. В общем, всё это можно совмещать.

Исправление dimgel, :

За долгие годы наработал я следующие варианты метапрограммирования.

  1. Самый мерзкий: кодогенерация в рантайме (в т.ч. спринг). Синтаксис идиотский, хрен увидишь что сгенерил, хрен отладишь. Разве что с точки зрения понимания работы стековой машины JVM забавно, если юзать низкоуровневые инструменты (я юзал https://asm.ow2.io/ и какую-то обёртку поверх него, автоматом расчитывающую размер фрейма под локальные переменные – вроде бы она называлась bytebucket, но чёт не гуглится).

// UPD1: Опять же, какого чёрта делать в рантайме то, что надо делать в билдтайме; из-за всей этой дичи JEE- и спринг-приложения и стартуют так долго.

Далее все варианты – про работу твоего кода в buildtime, что и является метапрограммированием здорового человека. В случае жавы, под каждую задачу рисуется отдельный maven-плагин – модуль приложения; этот модуль-плагин может собираться и тут же собранный запускаться при сборке другого модуля – т.е. всё в рамках одного запуска команды mvn.

  1. Самый тупой: кодогенерация. Парсишь что-то из src (и/или ещё откуда-нибудь), генеришь что-то в target/generated-sources. Под это дело даже отдельные фазы у maven есть. Этим кто только не занимается: например JPA и QueryDSL генерят свои мета-классы, сканируя и парся entity-классы. Я люблю классы сущностей по структуре базы генерить – здесь я, во-первых, не сорцы парсю, а лезу в information_schema (локальной временной базы, на которой предварительно прогоняю миграции другим maven-плагином); а во-вторых, я люблю кодогенерировать в src, а не в target/generated-sources: так удобнее и изменения отслеживать (в какой момент генератор поломали), и не только. Для парсинга категорически рекомендую JavaParser.org: редко я могу что-нибудь искренне похвалить, а манипуляция AST вообще занятие крайне занудное – но эта либа отличная (с учётом того, что в жаве нету pattern matching, с помощью которого в той же scala работать с AST на порядки проще).

  2. Развитием идеи кодогенерации целого исходника с нуля является замена кусков исходника: ищем метки ///something-begin/// и ///something-end/// и генерируем код между ними. Тут опять же генерируемый файл лежит в src. Класс, ищущий такие метки, разбивающий по ним исходник, предоставляющий API типа set(«something», generatedSource) и собирающий исходник из кусков обратно – тривиален.

  3. Ещё круче (хотя и гораздо гиморнее) парсить не метки (что имеет довольно ограниченное применение), а натравить JavaParser.org на исходник, подкрутить распарсенное AST и сериализовать его тем же JavaParser-ом назад в исходник. Слегка поломаются пустые строки и отступы, но терпимо (раньше по крайней мере ломались; например, каменты прижимаются к коду под ними). Я таким образом перегенерировал классы сущностей, сохраняя добавленные вручную программистом методы, не являющиеся вырожденными геттерами-сеттерами (которые добавлялись/удалялись плагином вместе с приватными полями класса при изменении структуры таблицы).

Всеми этими безобразиями с генерацией сорцов / изменениями их по месту можно сделать очень много, но не всё: нельзя нормально разворачивать DSL. Например, если в сорце есть статически-типизированный SQL-запрос, его нельзя удалить и заменить на сгенерированный по нему raw SQL, а генерить этот raw SQL в отдельный файл – настолько уродство, что даже не хочется думать, как до этого сгенерированного в отдельном файле запроса добраться из точки, где вставлен исходный DSL.

Для таких случаев:

  1. Ну и наконец, есть lombok. Это самое «честное» метапрограммирование на AST-макросах: сорцы не меняются, манипуляция AST выполняется в процессе работы javac, результаты манипуляций идут в class-файл. Посмотреть что нагенерилось можно, открыв этот class-файл в IDE (с автоматическим дизассемблированием). Ставить точки останова на сгенерированном коде у меня не выходило (не помню, пытался ли на class-файле), делал stepOver / stepInto вслепую + view variables на каждом шаге.

Самая большая проблема у lombok: чтобы добавить в него свой код, надо его форкать. Форков у него на одном только гитхабе – полторы тыщи, и всем пофиг. И авторы lombok как-то не чешутся делать поддержку расширяемости, и кстати авторы javac тоже не чешутся сделать классы AST public – сейчас lombok инстанциирует их через рефлексию (ну там с кешированием конструкторов, так что всё довольно шустро). Короче, на метапрограммирование в жаве всем насрать. С тоской вспоминаю скалу, где метапрограммирование встроенное, хотя AST в ней в разы более сложное, чем у жавы.

Собирается этот lombok отдельно от проекта, с помощью ant. Там у него свои заморочки.

Ну и кто с AST не работал, тем предстоит познать геморрой. Там ничего не делается в лоб. Простой пример – локальная переменная с инициализатором:

// boolean myVar = true;
mk.VarDef(
    mk.Modifiers(0),
    node.toName("myVar"),
    mk.TypeIdent(CTC_BOOLEAN),
    mk.Literal(CTC_BOOLEAN, 1)
)

В скале, помнится, константа задавалась то ли как Constant(Literal(true)), то ли как Literal(Constant(true)). Но там хотя бы квазицитирование есть, в отличие от. На каждом новом языке, на каждом новом компиляторе – свои заморочки. Хочешь сгенерировать try-catch – пишешь его в коде, навешиваешь на этот код lombok-аннотацию, при старте maven ставишь точку останова внутри lombok и когда до неё доходит дело, смотришь какое AST сгенерил компилятор; потом такое же делаешь в своём плагине, раза со 2-го … 5-го получится. Помнится, я пытался mk.Literal(CTC_BOOLEAN, true) в последней строчке – и оно не работало.

Что там с типизацией, не помню. У JavaParser вроде есть какие-то API для семантического анализа, но я с ними не разбирался. В lombok приезжает вроде уже типизированное дерево, но это не точно.

Можно подумать, что все варианты кроме lombok – для бедных. Но на lombok ты можешь работать только в пределах сорца (ну может, ещё анализировать импортированные типы), а с тупой кодогенерацией я и List по списку файлов migration/*.java генерю, и, повторюсь, классы по базе. В общем, всё это можно совмещать.

Исправление dimgel, :

За долгие годы наработал я следующие варианты метапрограммирования.

  1. Самый мерзкий: кодогенерация в рантайме (в т.ч. спринг). (UPD1: Опять же, какого чёрта делать в рантайме то, что надо делать в билдтайме; из-за всей этой дичи JEE- и спринг-приложения и стартуют так долго.) Синтаксис идиотский, хрен увидишь что сгенерил, хрен отладишь. Разве что с точки зрения понимания работы стековой машины JVM забавно, если юзать низкоуровневые инструменты (я юзал https://asm.ow2.io/ и какую-то обёртку поверх него, автоматом расчитывающую размер фрейма под локальные переменные – вроде бы она называлась bytebucket, но чёт не гуглится).

Далее все варианты – про работу твоего кода в buildtime, что и является метапрограммированием здорового человека. В случае жавы, под каждую задачу рисуется отдельный maven-плагин – модуль приложения; этот модуль-плагин может собираться и тут же собранный запускаться при сборке другого модуля – т.е. всё в рамках одного запуска команды mvn.

  1. Самый тупой: кодогенерация. Парсишь что-то из src (и/или ещё откуда-нибудь), генеришь что-то в target/generated-sources. Под это дело даже отдельные фазы у maven есть. Этим кто только не занимается: например JPA и QueryDSL генерят свои мета-классы, сканируя и парся entity-классы. Я люблю классы сущностей по структуре базы генерить – здесь я, во-первых, не сорцы парсю, а лезу в information_schema (локальной временной базы, на которой предварительно прогоняю миграции другим maven-плагином); а во-вторых, я люблю кодогенерировать в src, а не в target/generated-sources: так удобнее и изменения отслеживать (в какой момент генератор поломали), и не только. Для парсинга категорически рекомендую JavaParser.org: редко я могу что-нибудь искренне похвалить, а манипуляция AST вообще занятие крайне занудное – но эта либа отличная (с учётом того, что в жаве нету pattern matching, с помощью которого в той же scala работать с AST на порядки проще).

  2. Развитием идеи кодогенерации целого исходника с нуля является замена кусков исходника: ищем метки ///something-begin/// и ///something-end/// и генерируем код между ними. Тут опять же генерируемый файл лежит в src. Класс, ищущий такие метки, разбивающий по ним исходник, предоставляющий API типа set(«something», generatedSource) и собирающий исходник из кусков обратно – тривиален.

  3. Ещё круче (хотя и гораздо гиморнее) парсить не метки (что имеет довольно ограниченное применение), а натравить JavaParser.org на исходник, подкрутить распарсенное AST и сериализовать его тем же JavaParser-ом назад в исходник. Слегка поломаются пустые строки и отступы, но терпимо (раньше по крайней мере ломались; например, каменты прижимаются к коду под ними). Я таким образом перегенерировал классы сущностей, сохраняя добавленные вручную программистом методы, не являющиеся вырожденными геттерами-сеттерами (которые добавлялись/удалялись плагином вместе с приватными полями класса при изменении структуры таблицы).

Всеми этими безобразиями с генерацией сорцов / изменениями их по месту можно сделать очень много, но не всё: нельзя нормально разворачивать DSL. Например, если в сорце есть статически-типизированный SQL-запрос, его нельзя удалить и заменить на сгенерированный по нему raw SQL, а генерить этот raw SQL в отдельный файл – настолько уродство, что даже не хочется думать, как до этого сгенерированного в отдельном файле запроса добраться из точки, где вставлен исходный DSL.

Для таких случаев:

  1. Ну и наконец, есть lombok. Это самое «честное» метапрограммирование на AST-макросах: сорцы не меняются, манипуляция AST выполняется в процессе работы javac, результаты манипуляций идут в class-файл. Посмотреть что нагенерилось можно, открыв этот class-файл в IDE (с автоматическим дизассемблированием). Ставить точки останова на сгенерированном коде у меня не выходило (не помню, пытался ли на class-файле), делал stepOver / stepInto вслепую + view variables на каждом шаге.

Самая большая проблема у lombok: чтобы добавить в него свой код, надо его форкать. Форков у него на одном только гитхабе – полторы тыщи, и всем пофиг. И авторы lombok как-то не чешутся делать поддержку расширяемости, и кстати авторы javac тоже не чешутся сделать классы AST public – сейчас lombok инстанциирует их через рефлексию (ну там с кешированием конструкторов, так что всё довольно шустро). Короче, на метапрограммирование в жаве всем насрать. С тоской вспоминаю скалу, где метапрограммирование встроенное, хотя AST в ней в разы более сложное, чем у жавы.

Собирается этот lombok отдельно от проекта, с помощью ant. Там у него свои заморочки.

Ну и кто с AST не работал, тем предстоит познать геморрой. Там ничего не делается в лоб. Простой пример – локальная переменная с инициализатором:

// boolean myVar = true;
mk.VarDef(
    mk.Modifiers(0),
    node.toName("myVar"),
    mk.TypeIdent(CTC_BOOLEAN),
    mk.Literal(CTC_BOOLEAN, 1)
)

В скале, помнится, константа задавалась то ли как Constant(Literal(true)), то ли как Literal(Constant(true)). Но там хотя бы квазицитирование есть, в отличие от. На каждом новом языке, на каждом новом компиляторе – свои заморочки. Хочешь сгенерировать try-catch – пишешь его в коде, навешиваешь на этот код lombok-аннотацию, при старте maven ставишь точку останова внутри lombok и когда до неё доходит дело, смотришь какое AST сгенерил компилятор; потом такое же делаешь в своём плагине, раза со 2-го … 5-го получится. Помнится, я пытался mk.Literal(CTC_BOOLEAN, true) в последней строчке – и оно не работало.

Что там с типизацией, не помню. У JavaParser вроде есть какие-то API для семантического анализа, но я с ними не разбирался. В lombok приезжает вроде уже типизированное дерево, но это не точно.

Можно подумать, что все варианты кроме lombok – для бедных. Но на lombok ты можешь работать только в пределах сорца (ну может, ещё анализировать импортированные типы), а с тупой кодогенерацией я и List по списку файлов migration/*.java генерю, и, повторюсь, классы по базе. В общем, всё это можно совмещать.

Исходная версия dimgel, :

За долгие годы наработал я следующие варианты метапрограммирования.

  1. Самый мерзкий: кодогенерация в рантайме (в т.ч. спринг). Синтаксис идиотский, хрен увидишь что сгенерил, хрен отладишь. Разве что с точки зрения понимания работы стековой машины JVM забавно, если юзать низкоуровневые инструменты (я юзал https://asm.ow2.io/ и какую-то обёртку поверх него, автоматом расчитывающую размер фрейма под локальные переменные – вроде бы она называлась bytebucket, но чёт не гуглится).

Далее все варианты – про работу твоего кода в buildtime, что и является метапрограммированием здорового человека. В случае жавы, под каждую задачу рисуется отдельный maven-плагин – модуль приложения; этот модуль-плагин может собираться и тут же собранный запускаться при сборке другого модуля – т.е. всё в рамках одного запуска команды mvn.

  1. Самый тупой: кодогенерация. Парсишь что-то из src (и/или ещё откуда-нибудь), генеришь что-то в target/generated-sources. Под это дело даже отдельные фазы у maven есть. Этим кто только не занимается: например JPA и QueryDSL генерят свои мета-классы, сканируя и парся entity-классы. Я люблю классы сущностей по структуре базы генерить – здесь я, во-первых, не сорцы парсю, а лезу в information_schema (локальной временной базы, на которой предварительно прогоняю миграции другим maven-плагином); а во-вторых, я люблю кодогенерировать в src, а не в target/generated-sources: так удобнее и изменения отслеживать (в какой момент генератор поломали), и не только. Для парсинга категорически рекомендую JavaParser.org: редко я могу что-нибудь искренне похвалить, а манипуляция AST вообще занятие крайне занудное – но эта либа отличная (с учётом того, что в жаве нету pattern matching, с помощью которого в той же scala работать с AST на порядки проще).

  2. Развитием идеи кодогенерации целого исходника с нуля является замена кусков исходника: ищем метки ///something-begin/// и ///something-end/// и генерируем код между ними. Тут опять же генерируемый файл лежит в src. Класс, ищущий такие метки, разбивающий по ним исходник, предоставляющий API типа set(«something», generatedSource) и собирающий исходник из кусков обратно – тривиален.

  3. Ещё круче (хотя и гораздо гиморнее) парсить не метки (что имеет довольно ограниченное применение), а натравить JavaParser.org на исходник, подкрутить распарсенное AST и сериализовать его тем же JavaParser-ом назад в исходник. Слегка поломаются пустые строки и отступы, но терпимо (раньше по крайней мере ломались; например, каменты прижимаются к коду под ними). Я таким образом перегенерировал классы сущностей, сохраняя добавленные вручную программистом методы, не являющиеся вырожденными геттерами-сеттерами (которые добавлялись/удалялись плагином вместе с приватными полями класса при изменении структуры таблицы).

Всеми этими безобразиями с генерацией сорцов / изменениями их по месту можно сделать очень много, но не всё: нельзя нормально разворачивать DSL. Например, если в сорце есть статически-типизированный SQL-запрос, его нельзя удалить и заменить на сгенерированный по нему raw SQL, а генерить этот raw SQL в отдельный файл – настолько уродство, что даже не хочется думать, как до этого сгенерированного в отдельном файле запроса добраться из точки, где вставлен исходный DSL.

Для таких случаев:

  1. Ну и наконец, есть lombok. Это самое «честное» метапрограммирование на AST-макросах: сорцы не меняются, манипуляция AST выполняется в процессе работы javac, результаты манипуляций идут в class-файл. Посмотреть что нагенерилось можно, открыв этот class-файл в IDE (с автоматическим дизассемблированием). Ставить точки останова на сгенерированном коде у меня не выходило (не помню, пытался ли на class-файле), делал stepOver / stepInto вслепую + view variables на каждом шаге.

Самая большая проблема у lombok: чтобы добавить в него свой код, надо его форкать. Форков у него на одном только гитхабе – полторы тыщи, и всем пофиг. И авторы lombok как-то не чешутся делать поддержку расширяемости, и кстати авторы javac тоже не чешутся сделать классы AST public – сейчас lombok инстанциирует их через рефлексию (ну там с кешированием конструкторов, так что всё довольно шустро). Короче, на метапрограммирование в жаве всем насрать. С тоской вспоминаю скалу, где метапрограммирование встроенное, хотя AST в ней в разы более сложное, чем у жавы.

Собирается этот lombok отдельно от проекта, с помощью ant. Там у него свои заморочки.

Ну и кто с AST не работал, тем предстоит познать геморрой. Там ничего не делается в лоб. Простой пример – локальная переменная с инициализатором:

// boolean myVar = true;
mk.VarDef(
    mk.Modifiers(0),
    node.toName("myVar"),
    mk.TypeIdent(CTC_BOOLEAN),
    mk.Literal(CTC_BOOLEAN, 1)
)

В скале, помнится, константа задавалась то ли как Constant(Literal(true)), то ли как Literal(Constant(true)). Но там хотя бы квазицитирование есть, в отличие от. На каждом новом языке, на каждом новом компиляторе – свои заморочки. Хочешь сгенерировать try-catch – пишешь его в коде, навешиваешь на этот код lombok-аннотацию, при старте maven ставишь точку останова внутри lombok и когда до неё доходит дело, смотришь какое AST сгенерил компилятор; потом такое же делаешь в своём плагине, раза со 2-го … 5-го получится. Помнится, я пытался mk.Literal(CTC_BOOLEAN, true) в последней строчке – и оно не работало.

Что там с типизацией, не помню. У JavaParser вроде есть какие-то API для семантического анализа, но я с ними не разбирался. В lombok приезжает вроде уже типизированное дерево, но это не точно.

Можно подумать, что все варианты кроме lombok – для бедных. Но на lombok ты можешь работать только в пределах сорца (ну может, ещё анализировать импортированные типы), а с тупой кодогенерацией я и List по списку файлов migration/*.java генерю, и, повторюсь, классы по базе. В общем, всё это можно совмещать.