Статьи

Как сгенерировать исходный код?

В этой статье я расскажу о различных этапах разработки программного обеспечения, когда исходный код может быть сгенерирован программно, и я сравню различные подходы. Я также опишу архитектуру и идеи (тип момента эврики) конкретного инструмента, который генерирует код на определенной фазе.

Вручную

Это ответ на вопрос, заданный в заголовке. Если для этого есть возможность, вы должны сгенерировать код вручную. Год назад я уже писал статью о генерации кода и не передумал.

Вы не должны генерировать код, если вам действительно не нужно.

Странное утверждение, особенно когда я продвигаю инструмент FOSS, который точно нацелен на генерацию кода Java. Я знаю, и все же, утверждение состоит в том, что вы должны написать весь код, который вы можете вручную. К сожалению, или ради моего маленького инструмента, достаточно случаев, когда ручная генерация кода невозможна, или, по крайней мере, автоматическая генерация кода кажется более подходящей.

Зачем генерировать вручную

Я обсуждал это уже в ссылочной статье, но здесь мы идем снова. Когда лучшим вариантом является создание исходного кода, тогда в системе что-то не так или, по крайней мере, неоптимально.

  • разработчик, создающий код, не соответствует требованиям,
  • язык программирования ниже среднего, или
  • окружающая среда, некоторые рамки подпункт.

Не обижайтесь. Когда я говорю о «суб-разработчике», я не имею в виду Вас. Наконец, вы значительно выше среднего разработчика, но не в последнюю очередь потому, что вы открыты и заинтересованы в новых вещах, доказанных тем, что вы читаете эту статью. Тем не менее, когда вы пишете код, вы также должны учитывать среднего разработчика Джо или Джейн, который в будущем будет поддерживать вашу программу. И есть одна особенность обычных разработчиков: они не очень хороши. Они тоже неплохие, но, как следует из названия, средние.

Легенда о разработчике sub-par

С тобой может случиться то, что случилось со мной несколько лет назад. Это пошло как следующее.

Решая проблему, я создал мини-фреймворк. Не совсем фреймворк, такой как Spring или Hibernate, потому что один разработчик не может ничего подобного разработать. (Это не останавливает, хотя некоторые из них пытаются даже в профессиональной среде, что противоречиво, поскольку это не профессионально.) Вам нужна команда. То, что я создал, было единственным классом, который делал некоторые «магические» отражения, преобразовывая объекты в карты и обратно. До этого у нас были toMap() и fromMap() во всех классах, которым требовалась эта функциональность. Они были созданы и поддерживаются вручную.

К счастью, я был не один. У меня была команда. Они сказали мне отказаться от кода, который я написал, и продолжать создавать toMap() и fromMap() вручную. Причина в том, что код должен быть поддержан разработчиками, которые следуют за нами. И мы их не знаем, так как они даже не отобраны. Они все еще могут учиться в университете или даже не родиться. Мы знаем одно: они будут средними разработчиками, и код, который я создал, требует чуть больше, чем средние навыки. С другой стороны, для обслуживания toMap() вручную toMap() и fromMap() не требуется больше, чем средний навык, хотя обслуживание подвержено ошибкам. Но это только вопрос стоимости, который требует немного больше инвестиций в QA и значительно меньше, чем найм старших разработчиков.

Вы можете представить мои амбивалентные чувства, поскольку мой блестящий кодекс был отклонен, но с подушкой, которая восхваляла мое эго. Я должен сказать, что они были правы.

Sub-par framework

Ну, многие рамки в этом смысле не соответствуют. Может быть, выражение «sub-par» на самом деле не самое лучшее. Например, вы генерируете код Java из файла WSDL. Почему среда генерирует исходный код вместо байт-кода Java? Есть веская причина.

Генерация байт-кода сложна и требует специальных знаний. Это связано с расходами. Ему нужна библиотека для генерации байт-кода, например, Byte Buddy, более сложная для отладки для программиста, использующего код, и она немного зависит от версии JVM. В случае, если код сгенерирован как исходный код Java, даже если это для какой-то более поздней версии Java, и проект использует некоторую запаздывающую версию, шансы лучше, что проект может каким-то образом понизить сгенерированный код в случае, если это источник Java чем если бы это был байт-код.

Не соответствует языку

Очевидно, что в данном случае мы не говорим о Java, потому что Java — лучшая в мире, и нет ничего лучше. Или это? Если кто-то заявляет о каком-либо языке программирования, что этот язык идеален, игнорируйте этого человека. У каждого языка есть свои сильные и слабые стороны. Ява ничем не отличается. Если вы подумаете о том, что язык был разработан более 20 лет назад, и в соответствии с философией разработки он строго соблюдал обратную совместимость, то это просто подразумевает, что должны быть некоторые области, которые лучше в других языках.

Подумайте о методах equals() и hashCode() , которые определены в классе Object и могут быть переопределены в любом классе. Существует не так много изобретений, отменяющих любой из них. Переопределенные реализации являются довольно стандартными. На самом деле они настолько стандартны, что каждая интегрированная среда разработки поддерживает генерацию кода для них. Почему мы должны генерировать код для них? Почему они не являются частью языка каким-то декларативным способом? На эти вопросы должны быть очень хорошие ответы, потому что внедрение таких вещей в язык не представляет особой проблемы, и все же это не так. Должна быть веская причина, по которой я не лучший человек, о котором можно писать.

Как итог этой части: если вы не можете полагаться на сгенерированный вручную код, вы можете быть уверены, что что-то не соответствует. Это не позор. Такова наша профессия. Так идет природа. Идеального решения не существует, мы должны идти на компромиссы.

Тогда следующий вопрос,

Когда генерировать код?

Генерация кода принципиально может произойти:

  • (BC) до компиляции
  • (DC) во время компиляции
  • (DT) во время фазы испытаний
  • (DCL) во время загрузки класса
  • (DRT) во время выполнения

Далее мы обсудим эти разные случаи.

(BC) Перед компиляцией

Обычный этап — перед компиляцией. В этом случае генератор кода считывает некоторую конфигурацию или, возможно, исходный код и генерирует код Java, как правило, в определенный каталог, отделенный от исходного кода вручную.

В этом случае сгенерированный исходный код не является частью кода, который попадает в систему контроля версий. Ведение кода должно иметь дело с генерацией кода, и вряд ли есть возможность исключить генератор кода из процесса и продолжать поддерживать код вручную.

Генератор кода не имеет легкого доступа к структуре кода Java. Если сгенерированный код должен использовать, расширять или дополнять каким-либо образом уже существующий ручной код, он должен проанализировать исходный код Java. Это можно сделать построчно или с помощью какого-то парсера. В любом случае, это задача, которая будет выполнена снова компилятором Java позже, и также есть небольшая вероятность того, что компилятор Java и инструмент, используемый для анализа кода для генератора кода, могут быть не на 100% совместимы.

(DC) во время компиляции

Java позволяет создавать так называемые процессоры аннотаций, которые вызываются компилятором. Они могут генерировать код на этапе компиляции, и компилятор скомпилирует сгенерированные классы. Таким образом, генерация кода является частью фазы компиляции.

Генераторы кода, работающие на этом этапе, не могут получить доступ к скомпилированному коду, но они могут получить доступ к скомпилированной структуре через API, предоставляемый компилятором Java для процессоров аннотаций.

Можно генерировать новые классы, но невозможно изменить существующий исходный код.

(DT) во время фазы испытаний

Во-первых, это, кажется, немного не так. Зачем кому-то хотеть выполнять генерацию кода на этапе тестирования? Тем не менее, FOSS, который я пытаюсь «продать» здесь, делает именно это, и я подробно опишу возможность, преимущества и, честно говоря, недостатки генерации кода на этом этапе.

(DCL) во время загрузки класса

Также возможно изменить код во время загрузки класса. Программы, которые делают это, называются агентами Java. Они не являются реальными генераторами кода. Они работают на уровне байтового кода и модифицируют уже скомпилированный код.

(DRT) во время выполнения

Некоторые генераторы кода работают во время выполнения. Многие из этих приложений генерируют Java-байт-код напрямую и загружают код в работающее приложение. Также возможно генерировать исходный код Java, компилировать код и загружать полученные байты в JVM.

Генерация кода на этапе тестирования

Это фаза, когда и где Java :: Geci (Java GEnerate Code Inline) генерирует код. Чтобы помочь вам понять, как возникает странная идея выполнить генерацию кода во время модульного тестирования (когда уже слишком поздно: код уже скомпилирован), позвольте мне рассказать вам другую историю. История придумана, этого никогда не было, но она не затмевает объясняющую силу.

У нас был код с несколькими классами данных, каждый с несколькими полями. Мы должны были создать методы equals() и hashCode() для каждого из этих классов. В конечном итоге это означало избыточность кода. Когда класс изменился, поле было добавлено или удалено, тогда методы также должны были быть изменены. Удаление поля не было проблемой: компилятор не компилирует метод equal() или hashCode() который ссылается на несуществующее поле. С другой стороны, компилятор не возражает против такого метода, который НЕ ссылается на новое существующее поле.

Время от времени мы забывали обновлять эти методы и пытались изобрести все более сложные и лучшие способы противодействия подверженному ошибкам человеческому кодированию. Самой странной идеей было создать значение MD5 для имен полей и вставить его в качестве комментария в методы equals() и hashCode() . В случае изменения в полях, тест может проверить, что значение в исходном коде отличается от значения, рассчитанного по именам полей, и затем сообщить об ошибке: модульный тест не пройден. Мы никогда не реализовывали это.

Еще более странная идея, которая оказалась не такой уж странной и, наконец, привела к Java :: Geci, заключается в том, чтобы на самом деле создать ожидаемый тест метода equals() и hashCode() во время теста из полей, доступных через отражение, и сравнить его с тем, который был уже в коде. Если они не совпадают, то они должны быть восстановлены. Тем не менее, код на этом этапе уже восстановлен. Единственная проблема заключается в том, что он находится в памяти JVM, а не в файле, который содержит исходный код. Зачем просто сигнализировать об ошибке и сказать программисту перегенерировать код? Почему тест не записывает изменения? Ведь мы, люди, должны говорить компьютеру, что делать, а не наоборот!

И это было прозрение, которое привело к Java :: Geci.

Java :: Geci Architecture

Java :: Geci генерирует код в середине жизненного цикла компиляции, развертывания и выполнения. Java :: Geci запускается, когда модульные тесты выполняются на этапе сборки.

Это означает, что руководство и ранее сгенерированный код уже скомпилированы и доступны для генератора кода через отражение.

Выполнение генерации кода на этапе тестирования имеет еще одно преимущество. Любое генерирование кода, которое запускается позже, должно генерировать только код, который ортогонален функциональности кода вручную. Что это означает? Он должен быть ортогональным в том смысле, что сгенерированный код не должен каким-либо образом изменять или создавать помехи существующему коду, созданному вручную, который может быть обнаружен модульными тестами. Причина этого заключается в том, что генерация кода, происходящая на любой более поздней стадии, уже после выполнения модульного теста, и, таким образом, нет возможности проверить, влияет ли сгенерированный код каким-либо нежелательным образом на поведение кода.

Генерация кода во время теста позволяет протестировать весь код как с учетом руководства, так и сгенерированного кода. Сам по себе сгенерированный код не должен тестироваться как таковой (это задача теста проекта генератора кода), но поведение ручного кода, написанного программистами, может зависеть от сгенерированного кода и, следовательно, от выполнения тестов. может зависеть от сгенерированного кода.

Чтобы убедиться, что все тесты в порядке с сгенерированным кодом, компиляция и тесты должны быть выполнены снова в случае, если был сгенерирован новый код. Чтобы гарантировать это, генерация кода вызывается из теста, и тест завершается неудачей в случае генерации нового кода.

Чтобы это исправить, генерация кода в Java :: Geci обычно вызывается
из трехстрочного модульного теста, который имеет структуру:

1
Assertions.assertFalse(...generate(...),"code has changed, recompile!");

Вызов для generate(...) представляет собой цепочку вызовов методов, конфигурирующих платформу и генераторы, и при выполнении каркас решает, отличается ли сгенерированный код от уже существующего кода. Он записывает Java-код обратно в исходный код, если код изменился, но оставляет код без изменений, если сгенерированный код не изменился.

Метод generate() который является последним вызовом в цепочке к коду
генерация возвращает true если какой-либо код был изменен и записан обратно
исходный код. Это провалит тест, но если мы запустим тест снова
с уже измененными источниками, тогда тест должен работать нормально.

Эта структура имеет некоторые ограничения на генераторы:

  • Генераторы должны генерировать точно такой же код, если они выполняются на том же источнике и классах. Это обычно не является строгим требованием, генераторы кода не склонны генерировать случайный источник. Некоторые генераторы кода могут захотеть вставить метки времени в качестве комментария в коде: они не должны.
  • Сгенерированный код становится частью исходного кода, и они не являются артефактами времени компиляции. Обычно это относится ко всем генераторам кода, которые генерируют код в уже существующие исходные классы. Java :: Geci может генерировать отдельные файлы, но он был разработан главным образом для генерации встроенного кода (отсюда и название).
  • Сгенерированный код должен быть сохранен в хранилище, а ручной источник вместе с сгенерированным кодом должен находиться в состоянии, которое не требует дальнейшей генерации кода. Это гарантирует, что CI-сервер в разработке может работать с исходным рабочим процессом: извлекать — компилировать — тестировать — вносить артефакты в репозиторий. Генерация кода уже была выполнена на компьютере разработчика, и генератор кода на CI только гарантирует, что это действительно было сделано (иначе тест не пройден).

Обратите внимание, что тот факт, что код генерируется на компьютере разработчика
не нарушает правило, что сборка должна быть машинно-независимой.
В случае какой-либо машинной зависимости генерация кода будет
В результате на CI-сервере будет разный код, и сборка будет нарушена.

API генерации кода

Приложения генератора кода должны быть простыми. Фреймворк должен выполнять все задачи, которые одинаковы для большинства генераторов кода, и должен обеспечивать поддержку, или какова обязанность фреймворка?

Java :: Geci делает много вещей для генераторов кода:

  • он обрабатывает конфигурацию наборов файлов, чтобы найти исходные файлы
  • сканирует исходные каталоги и находит файлы исходного кода
  • читает файлы, и если файлы являются исходными кодами Java, это помогает найти класс, соответствующий исходному коду
  • поддерживает рефлексию, чтобы помочь детерминированной генерации кода
  • унифицированная обработка конфигурации
  • Генерация исходного кода Java различными способами
  • изменяет исходные файлы только при изменении и записывает изменения
  • предоставить полностью функциональные примеры генераторов кода. Одним из них является полноценный генератор Fluent API, который сам по себе может быть целым проектом.
  • поддерживает шаблоны Jamal и генерацию кода.

Резюме

Прочитав эту статью, вы получили представление о том, как работает Java :: Geci. Вы можете начать использовать его, посетив домашнюю страницу GitHub Java :: Geci . Я также выступлю с докладом на эту тему в Майнце на конференции JAX. Среда, 8 мая 2019 года. 18:15 — 19:15

В ближайшие недели я планирую написать больше статей о соображениях дизайна и реальных решениях, которым я следовал в Java :: Geci.

Вам предлагается связаться со мной, для кода, создания билетов следуйте в Twitter, Linked-in whatnot. Это весело.

Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Как генерировать исходный код?

Мнения, высказанные участниками Java Code Geeks, являются их собственными.