Главная

С прозрачностью скриптов сборки к идеальным зависимостям.

На примере Maven и Gradle.

Поделиться:
 

0
Image for post

Скрипты сборки являются неотъемлимой часть рабочего процесса разработчиков. Они включают функционал от вывода информации для отладки до ключевых команд вашего пайплайна. В Java стэке основными инструментами являются Maven и Gradle.

Часто ли вы были довольны теми pom.xml и build.gralde файлами, которые вы написали длительное время назад или которые достались вам в поддержку?

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

Проблемы

Любой элемент инструмента сборки рано или поздно создает проблему, иногда даже без изменений файлов. Как для maven, так и Gradle в основном — это зависимости и конфигурация плагинов.

Основные виды можно заметить по количеству вопросов на stackoverflow. Например, запрос Gradle could not resolve возвращает 2333 вопроса, а Gradle cannot find symbol — 643. Первый запрос оперирует к проблеме отсутствия артефакта в известных репозиториях, а второй же к проблеме компиляции, когде Gradle все-таки нашел артефакт, но код в нем не подходящий.

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

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

Анализ

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

Именно информации и инструментов не хватает людям, когда они отправляются с вопросом в интернет. Здесь все достаточно легко, если вы работаете в публичном репозитории и имеете зависимости только со всеми известными библиотеками. Но большинство людей работают в компаниях, чей код недоступен, а библиотеки используются внутренние и публикуются в частные репозитории. Здесь наступает особая потребность в прозрачности для анализа.

Разработчик начинает анализировать ошибку, отталкиваясь от сообщения об ошибке, а также от своего опыта, чтобы устранить проблему. Итого, в сумме для анализа имеем:

  • Опыт человека
  • Google
  • [Опционально] Опыт других участников проекта
  • Содержимое репозитория

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

Не стоит требовать от пользователя глубоких знаний — сделайте сборку прозрачной для анализа

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

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

Остается содержимое репозитория, которое хотелось бы, чтобы имело ответы на большинство вопросов.

Как и новичок, так и человек с опытом будет рад предупреждению проблемы. Для начала стоит повысить прозрачность конфигурации проекта, чтобы появилась дополнительная информация и проще было анализировать. Также это дает возможность легче контролировать текущую ситуации, например, периодически удаляя лишнее из скрипта сборки. Прозрачность скриптов также одобрят на code review, когда при чтении кода возникнет вопрос, зачем это было добавлено. Или наоборот, на code review можно обратить внимание человека, чтобы он сделал код более информативным.

Основная документация

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

Поэтому воспользуемся практикой, которая часто попадается на глаза в opensource проектах, но порой не хватает промышленным репозиториям — README файл проекта. Здесь желательно указать основные команды, которые будут ожидаемое запускаться из проекта:

  • Как собрать
  • Как релизить
  • Куда публикуется артефакт: URL репозитория и с каким координатами (group-id, artifact-id, version)
  • Как запустить

Не нужно больших описаний, небольшое описание основного функционала принесет больше выгоды, самое необходимое будет:

  • Консольная команда
  • Ссылки
  • Условия

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

README файл избавит от основных вопросов к проекту, но не поможет в анализе отдельных проблем.

Зависимости

Основное изменение большинства скриптов сборки, это зависимости, но они же сулят основное количество проблем.

Проблемы зависимостей отсутствуют тогда, когда их нет

Это крайне очевидная истина, но именно она поможет разработчику критически посмотреть на свои зависимости, а именно задать вопрос: зачем каждая из них нужна?

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

Оставляйте комментарии к зависимостям проекта

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

Image for post
Пример комментария перед зависимостью

Например, с таким комментарием однажды можно будет быстро удалить библиотеку, так как можно проверить используется ли она в этом классе, таким образом предупредить проблемы.

В ходе анализа эти комментарии помогут определить причину использования зависимости и принять решение.

Группируйте зависимости по общему признаку и подписывайте их.

Image for post
Пример разнородного списка зависимостей

В данном примере зависимости со scope test перемешаны с другими. Также можно разместить, например, зависимости одного фрейморка рядом друг с другом, так как вероятно они вместе будут обновлены и взглядом можно будет быстро их найти. В maven особенностей версии удобно оставить комментарием прям над тегом version. Даже если разделить однородные группы зависимостей пустой строкой, то уже проще будет читать.

Image for post
Пример зависимостей, размещённых в одном месте

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

Такая группировка помогает в анализе быстро сократить область поиска проблемных зависимостей.

Проверяйте дублирующиеся зависимости

Image for post
Пример объявления лишней зависимости org.harmcrest:harmcrest

Часто встречается, что явно объявляется зависимость, которая является транзитивной для других.

Image for post
testCompileClasspath зависимости для примера выше

Например, зависимости объявлены таким образом. То есть тот, кто их добавил говорил, что хотел бы обе библиотеки в compile и runtime classpath в тестах. Но если выполнить команду Gradle dependencies, то результат говорит о том, что harmcrest-core уже включает harmcrest. Значит это лишнее объявление. Оно не меняет многое, но добавляет лишних строк, и читающий может быть озадачен в будущем.

В практике можно наткнуться на более сложные примеры лишних зависимостей. Когда большая библиотека тянет по цепочке что-то версии 2.0, а вы объявили эту библиотеку явно с версия 1.0. Крайний случай проблемы здесь в том, что возможна ошибка рефлексии обнаруженная лишь в runtime, во время работы вашего приложения, если ваши тесты не покрыли какого-то случая.

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

Транзитивные зависимости

Отдельные категория проблем приносится с неподходящим использованием скоупа зависимостей. Например, выше упомянутые ошибки рефлексии, когда не найден класс или метод при запуске тестов или сборки.

Как и зависимости бывают двух видов, так и здесь существует два источника проблемы:

  • Прямые зависимости: ваши зависимости
  • Транзитивные зависимости: зависимости ваших зависимостей

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

С транзитивными зависимостями сложнее. Так как их декларация зафиксирована в pom файле в удаленном репозитории, но хотелось бы обладать всей информацией для быстрого анализа ситуации. Независимо от инструмента сборки, которым вы пользуетесь, вам нужно будет ответить на вопросы:

  • На какую часть приложения влияют транзитивные зависимостей библиотеки?
  • Как изменить скоуп влияния транзитивной зависимости?

Связь Maven и Gradle

Независимо от того библиотека написана в вашей компании или opensource, и независимо инструмента для ее публикации применяются общие правила.

Maven или Gradle определенной командой отправляют библиотеку в удаленный репозиторий в большинстве случаев это Maven репозиторий, также gralde поддерживает Ivy репозитории, но мы рассмотрим лишь популярный. Каждая зависимость должна иметь уникальную координату, которой является ключ: group-id + artifact-id + version и хранится он в pom файле артефакта. Опубликованная зависимость это завершенный шаг, и библиотека после публикации не связана с Gradle.

Например, здесь опубликован Gradle плагин, это jar файл, но в Maven репозитории он имеет pom файл и это обязательное правило.

В Gradle проектах, особенно больших, не всегда очевидно с какими координатами будет опубликована библиотека, потому что в Gradle нет декларации Maven координат, а используются другие наименования, которые укажу ниже. Говоря про opensource проекты, зачастую разработчики оставляют информацию о Maven координатах проекта в README, таким образом можно быстро скопировать и объявить зависимость у себя в проектах. Во внутренних библиотеках компании не всегда же принято делать развернутым README файл, поэтому приходится тратить время, чтобы построить связь с каким координатами будет опубликована библиотека из этого репозитория. В помощь хочу оставить таблицу связей и команду для быстрого получения координат.

Например, вы укажете в Gradle зависимость таким образом:

Image for post
Пример объявления координат проекта gradle

В этом случае в pom файл этой библиотеки после публикации попадут следующие координаты:

Image for post
Пример maven координат для gradle-библиотеки

Для быстрого вывода в консоль Maven координат можно пользоваться следующей таской. Это будет полезно, когда для текущего проекта будет необходимо найти библиотеку в Maven репозитории артефактов.

Image for post
gradle task для вывода в консоль maven координат

Скоуп зависимостей

Кроме координат, для всех транзитивных зависимостей обозначается скоуп — область воздействия библиотеки.

Для начала определимся с общим списком для скоупов. На текущую версию для Gradle это:

  • implementation, api, compile, compileOnly, runtimeOnly, providedCompile, providedRuntime, testImplementation, testCompile, testCompileOnly, testRuntimeOnly

Для Maven это:

  • compile, runtime, provided, test

Кроме того, для составления связей определимся, на какой classpath могут влиять зависимости :

  • compile classpath, runtime classpath, test compile classpath, test runtime classpath

Самое важное, что стоит помнить в вопросе транзитивных зависимостей и cвязи между Gradle и Maven это:

Только pom файл в Maven репозитории влияет на то, какие зависимости будут использованы и в каком classpath.

Таким образом, Gradle при скачивании зависимости полностью опирается на информацию в pom файле, потому что другой у него и нет. И именно это поможет вам в анализе проблемы, когда вы не уверены с каким scope была опубликована транзитивная зависимости — смотрите конкретный pom файл в репозитории артефактов.

В помощь к анализу проблем связи конфигурации зависимостей Gradle и их влияние на фактический pom файл можно пользоваться таблицей ниже, которая является актуальной на текущий день и создана через проверку на реальном проекте, доступном на github. Также к ней есть отдельная поясняющая статья на dev.to

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

Image for post
Схема связи текущего проекта с зависимостями

Например, прямая зависимость B к вашему проекту, окажет влияние на различные classpath, как указано в соответствующих столбцах. Транзитивная зависимость C повлияет на ваш проект A в соотствествии с столбцами справа.

Рекомендую практический совет. В Intellij Idea можно “упасть” в класс библиотеки, быстро увидеть библиотеку откуда этот класс к вам попал, а также посмотреть весь список библиотек, которые каким-то образом использует проект. Все библиотеки объявленные в Maven или Gradle файлах, а также через транзитивные зависимости будут в этом списке.

Image for post
Навигация по связи класса с его библиотекой

Изменение транзитивных зависимостей

Каждая зависимость проекта — отдельная библиотека, pom файл зависимостей которой не изменить

Способы, которым вы можете повлиять на транзитивные зависимости библиотеки:

  • Исключить из списка транзитивных
  • Переопределить библиотеку другой версии на своей стороне
  • Изменить версию вашей зависимости
Image for post
Пример исключения зависимости

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

Именно поэтому так важно при публикации своих библиотек в репозиторий вашей компании следить за скоупом библиотек, с которым вы их публикуете. В любых относительно крупных opensource проектах обычного происходит ревью зависимостей, так как транзитивные зависимости это часть продукта — артефакта, который вы релизите, как и pom файл.

Плагины

В случае плагинов анализ ситуации и поиск связи между Gradle и Maven репозиторием может оказаться сложнее.

Предположим, вы хотите изучить или изменить что-то в репозитории какого-то плагина, но в README отсутствует информация о том, как его использовать.

Image for post
Пример конфигурации плагина через plugins DSL
Image for post
Пример конфигурации плагина legacy методами

С последних версий gradle плагин конфигурируется через plugins DSL, ранее же для этого требовалось больше строк, но если раньше явно объявлялась зависимость, которую можно найти в Maven репозитории, то теперь только plugin.id, как уникальный идентификатор.

Фактически же Gradle ищет плагин по наличию внутри артефакта файла по формату META-INF/gradle-plugins/{plugin.id}.properties в соответствии с этой статьей документации.

Image for post
Пример конфигурации кастомного плагина id=’my-plugin’

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

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

Для плагинов актуальны те же правила для повышения прозрачности: подписывать зависимость, оставлять комментарии о причине использования чего-либо и уделять внимание скриптам сборки на code review. В помощь к анализу оставлю полезные команды:

Image for post
полезные таски для анализа

В заключение

Код скриптов сборки также нуждается в качестве, как и код решения. Вернувшись в проект, всем хочется быстро актуализировать зависимости и избежать однотипных проблем. Даже в тяжелом случае, всегда возможно оставить объяснение зачем был нужен этот “костыль” в скрипте или странная зависимось, это даст шанс другому человеку разобраться. Если проигнорировать это, то остаются проблемы на будущее другим людям или себе, когда захочется сделать этот проект лучше.

Резюмируя, предлагаю пользоваться следующими практиками, которые помогут вашему проекту:

  • Написание README
  • Комментарии в файле сборка
  • Проверка на дублирующие зависимости
  • Проверка транзитивных зависимостей

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

Надеюсь это статья дала вам пищи для размышления и повышения осознанности в работе с файлами сборки проекта.

0