Инициализация Gradle-проекта — Java: Настройка окружения
Ручная компиляции кода довольно утомительный процесс даже во время обучения. В реальных приложениях такой подход просто не применим, слишком много действий придется делать руками. Для компиляции используются специальные системы сборки, такие как Maven или Gradle. Последний стал стандартом де-факто для новых приложений, поэтому рассмотрим работу именно с ним. Принцип у всех таких систем один и тот же, поэтому зная один, несложно разобраться и в других.
Gradle — это не просто автоматизатор компиляции. Это навороченная система сборки, где компиляция это всего лишь один из этапов. Сборка проекта – довольно широкое понятие. Она включает в себя компиляцию исходного кода, упаковку в jar, запуск тестов и другие шаги, необходимые для создания рабочего приложения. Ключевые возможности Gradle:
- Автоматическая сборка проекта. Gradle сам знает какие файлы и как надо собирать. Сам компилирует, сам упаковывает в JAR
- Быстрая инкрементальная сборка. Компилируется только то, что изменилось
- Управление зависимостями. Gradle сам качает и подключает библиотеки. И заодно умеет их обновлять
Полный список возможностей огромен . Большая часть из них работает автоматически, об этом не придется думать.
В повседневной работе Java-программист пользуется Gradle через редактор, но во время обучения нужно потратить немного времени на то, чтобы разобраться с тем как он работает. Иначе потом будет сложно, когда что-то пойдет не так и возникнет ошибка. Поэтому здесь мы проделаем все операции через консоль, а дальше подключим редактор.
Начнем с установки. Если Gradle у вас не установлен, то посмотрите инструкцию . Проверить установку можно так:
-v ------------------------------------------------------------ Gradle 8.4 ------------------------------------------------------------
Теперь инициализируем новый Gradle-проект:
# Создаем директорию для проекта mkdir hexlet-gradle-project cd hexlet-gradle-project # Запускаем инициализацию gradle init
Дальше Gradle задаст несколько вопросов, на базе которых сформируется правильная структура. Если во время создания вы ошиблись и выбрали не тот вариант, то ничего страшного. Просто дойдите до конца и пересоздайте директорию с проектом. Потом запустите все заново. Разбираем вопросы:
type of project to generate: 1: basic 2: application 3: library 4: Gradle plugin Enter selection (default: basic) [1..4]
Выбираем тип basic, который Gradle предлагает по умолчанию. Этот тип представляет собой базовую структуру проекта Gradle, которую дальше можно настроить и дополнить по мере необходимости
(default: Kotlin) [1..2]
Выбираем язык для описания Gradle файлов. Kotlin DSL является выбором по умолчанию. Мы тоже будем использовать Kotlin
(default: hexlet-gradle-project):
Просто жмем Enter. Текущее имя директории и есть имя проекта.
(some features may change in the next minor release)? (default: no) [yes, no]
Выбираем yes. После этого появляется радостная надпись:
in 8m 46s
Посмотрим на получившуюся структуру:
-a . . ├── .gitattributes ├── .gitignore ├── build.gradle.kts ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts 2 directories, 9 files
Много всего, начнем по порядку.
Gradle сразу подготавливает проект к использованию через git добавив два файла .gitignore и .gitattributes. Игнорируются файлы сборки, которые попадут в директорию build и .gradle, это служебные файлы Gradle, которые он сам себе сгенерирует во время работы.
Файлы gradlew и gradlew.bat нужны для установки самого Gradle. Концепция здесь такая, Gradle во время создания проекта делает так, чтобы проект не использовал глобально установленный Gradle. Он скачивает сам себя в директорию gradle. Все команды будут запускаться через ./gradlew (в Windows ./gradlew.bat). Зачем так сделано? Так Gradle фиксирует версию. Если поменяется глобально установленная версия, то проект продолжит работать с той, с которой он работал. Меньше шансов что-то сломать, но сложнее в обновлении.
Файл settings.gradle.kts содержит различные настройки, например, там задается имя проекта. Остальное добавляется по мере развития и требований со стороны кода.
Файл build.gradle.kts – это основной файл Gradle, в котором на языке Kotlin описано то, как будет работать система сборки. Пока этот файл пустой, чуть позже мы его наполним
Далее нужно создать место, в котором будет располагаться исходный код нашего проекта. Внутри директории проекта создайте следующую файловую структуру: src/main/java/io/hexlet/example. Назначение директорий здесь следующее. Директория src (source) – место, в котором лежит весь исходный код проекта. Директория main отвечает за код проекта и дополнительные ресурсы (например, картинки). Внутри находится java, то есть тут лежит Java-код. Но подразумевается, что бывает и по-другому. И вот только внутри java начинается структура, соответствующая пакету проекта.
В директории example создайте новый Java-класс с именем App.java и добавьте туда код:
// Имя пакета // Соответствует структуре директорий внутри директории java package io.hexlet.example; public class App public static void main(String[] args) System.out.println("Hello, world!"); > >
Само приложение готово. Теперь перейдем к настройке системы сборки. Откройте файл build.gradle.kts. Именно с этим файлом придется работать больше всего, настраивая Gradle для подключения новых библиотек и их конфигурации. Добавьте туда следующий код:
plugins // Поддержка запуска из командной строки application > repositories // Подключаем автоматическая работа с репозиторием Maven Central mavenCentral() > // Для плагина application указываем главный класс приложения application // Входная точка mainClass.set("io.hexlet.example.App") >
Теперь все готово. Попробуем запустить проект, а в следующем уроке поговорим о том, как конкретно работать с Gradle:
> Task :run Hello, world! # Вот он вывод нашей программы
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Apache Maven
Сегодня речь пойдёт о Maven. Это в первую очередь утилита для сборки Java проектов, но по факту является системой управления проекта в целом, определяет его структуру и, во многом, жизненный цикл. (Apache Maven is a software project management and comprehension tool. Project object model (POM)) Работая с базовыми сценариями в хорошо настроенном окружении, программисту не требуется глубоко вникать в тонкости этой технологии. Но стоит проекту Maven перестать собираться, как легко можно попасть в тупик, для выхода из которого требуется опыт и понимание определённых нюансов. Почему не находится плагин? Зачем он лезет в интернет? Почему падает деплой артефакта? Откуда взялась библиотека log4j первой версии? Почему сборка работает локально, а на Jenkins падает? У меня в IDE всё компилируется, а Maven что-то тупит. Вспоминать и фантазировать можно долго.
Многие скажут, что я утрирую, но Вы можете поверить, что люди переходили с Eclipse на IDEA после трёх и более лет разработки потому, что сложный maven проект отказывался нормально импортироваться в Eclipse, но коллеги пользовались IDEA и никто не мог помочь настроить его в Eclipse?
Но даже учитывая всё это, использование Maven — это необходимость и меньшее из зол. Maven заставляет всех разработчиков делать одинаково и более менее правильно. Это касается структуры проектов, расположения и имён определённых файлов, управлениями модулями, зависимостями и в определённой степени процессом релизов.
Как развивались некоторые проекты, пока их не мигрировали на maven? Все известные мне проекты собирались с помощью утилиты Ant. Не знаю, смог ли кто-то настроить компиляцию и сборку с использованием непосредственно утилиты javac, поставляемой вместе JDK а также shell или bat скриптов, но это было бы уж слишком безумно. Большинство вполне успешно обходилось утилитой Ant.
Давайте пофантазируем, как это было. Куда положить исходный код? Ну, допустим ./src/Main.java или ./src/com/example/Main.java, если с пакетами. Куда проперти файлы? Давайте туда же ./src/log4j.xml. Но не все, что-то положим в ./conf. Куда будем складывать скомпилированные файлы? Мне нравится ./dist. А тесты? В ./test/src, а данные для теста в ./test/*, то есть рядом с исходниками. Честно говоря, я подсматриваю в реальном проекте. Если вы уже имеете опыт разработки, то уже чувствуете отсутствие стандарта — вроде всё логично, но в то же время наобум. И на каждом новом проекте будет похоже, но по-другому. Кстати, а библиотеки мы куда положим? В ./lib, конечно же. Стоп! Что значит положим библиотеки, разве они не должны храниться централизованно и выкачиваться автоматически при необходимости? Нет, библиотеки скачивают, потом обычно из переименовывают из log4j-1.2.12.jar в log4j.jar, чтобы версию уже никак нельзя было определить, разве что её заботливо указали в MANIFEST файле внутри библиотеки. Естественно, библиотеки сохраняются и в системе контроля версий, и таким образом любой проект весит уже никак не меньше 20-50 мегабайт.
С библиотеками вообще очень неудобно — начинаешь новый проект и каждую библиотеку скачивать из интернета и сохранять в проекте неудобно, ещё и может понадобиться в IDE явно её добавлять. Но был найден удобный и эффективный подход, решающий все проблемы — мы будем копировать lib из проекта в проект. Со временем там окажутся все нужные нам библиотеки: логгеры, драйверы всех баз данных, spring, apache commons, junit, сервлеты.
Ну вот у нас есть исходные файлы, мы настроили IDE, указав пути к ним, добавили библиотеки. Пришло время показать наше приложение кому-то, а на местном наречии — «выложить». Выложить в тестовую среду или продакшен. В случае web приложения это означает, что нужно создать ZIP архив фиксированной структуры, состоящий из скомпиливанных классов, файлов настроек, библиотек и ещё пары специальных файлов. Этот архив называется WAR-архивом (или варником). Для всего этого и создаётся Ant сборка. Чаще всего это файл с названием build.xml, в довесок к которому идёт build.properties. Пример содержимого такого фэйла:
Команда завершается успешно, в папке ./target появился файл samples-maven-simple-1.0-SNAPSHOT.jar. В нём только сам pom.xml и MANIFEST.MF следующего содержания: Чаще всего это применяется к библиотекам логирования. Например, исключаем отовсюду log4j, а потом добавляем log4j-over-slf4j. Подробнее тут.
Добавлю один интересный сценарий связанный с тестированием, о котором не все знают. Все тесты располагаются в папке src/test, в все юнит тесты имеют постфикс «Test». По практикам разработки существуют ещё интеграционные тесты, которые могут использовать внешние сервисы и запускаются отдельно от юнит тестов. Для них иногда заводят отдельные модули и настройки surefire плагина. Можно поступить иначе — добавить к имени теста постфикс «IT» (полный список: **/IT*.java, **/*IT.java, **/*ITCase.java). Такие тесты запускаются командой «mvn verify», а отвечает за них специальный maven-failsafe-plugin. Он требует явного указания в pom.xml: То есть при настройках по умолчанию придётся ждать целый день, чтобы воспользоваться исправленной версией библиотеки! А если установить опцию ‘always’, то сборки начнут заметно тормозить при большом количестве snapshot зависимостей. Удобно при необходимости добавлять к аргументам команды сборки параметр «-U» (mvn clean package -U), тогда все снэпшоты будут принудительно обновлены. Процесс разработки можно выстроить следующим образом:
- Устанавливаем версию библиотеки 1.0-SNAPSHOT
- Устанавливаем версию зависимости в основном приложении в 1.0-SNAPSHOT
- Вносим изменения в библиотеку, выполняем deploy
- Пересобираем основное приложение, при необходимости с аргументом -U, тестируем
- Повторяем шаги 3-4 до тех пор, пока код не стабилизируется и все ошибки не будут исправлены
- Устанавливаем версию библиотеки 1.0
- Устанавливаем версию зависимости в основном приложении в 1.0
- Осуществляем финальные проверки и выходим в релиз. Если находим ошибки, возвращаемся на шаг 6, меняя версию на 1.0.1
- Начинаем работу над следующим релизом
- Устанавливаем версию библиотеки 1.1-SNAPSHOT
- Устанавливаем версию зависимости в основном приложении в 1.1-SNAPSHOT
Чтобы снэпшоты не накапливались в репозитории в неограниченном количестве, в Nexus и Artifactory существуют политика удаления старых версий. Поэтому иногда старые снэпшоты теряют свою актуальность или вообще пропадают из репозитория, хотя по хорошему последняя-то версия не должна удаляться. Следует руководствоваться следующим правилом: версия release не должна иметь snapshot зависимостей. Только в этом случае релизная версия представляет собой что-то финальное и не подверженное случайным факторам. Если же мы оперируем версиями snapshot, значит наш код часто меняется и при случае мы легко можем восстановить артефакт в репозитории, запустив команду deploy.
На этом пока оставим релизы и снэпшоты. Заметили, что вместе с обычным артефактом сборки в репозиторий попала ещё версия «jar-with-dependencies»? При этом группа, артефакт и версия у них совпадают. Это возможно благодаря дополнительному атрибуту зависимостей — classifier. При использовании assembly плагина в файле конфигурации assembly.xml нужно указать id. В нашей сборке id неявно устанавливается в «jar-with-dependencies». Результаты сборки плагином assembly ведут себя так же, как и основной артефакт. Зависимость будет выглядеть так, если понадобится:
hipravin.samples samples-maven-simple 1.0-SNAPSHOT jar-with-dependencies
Атрибут classifier также используется при загрузке исходного кода и документации javadoc. По умолчанию загружается только скомпилированный код, а пользователи нашей библиотеки не увидят никакой документации, а если попытаются посмотреть исходный код, то получат в лучшем случае декомпилированную версию. Это не очень удобно, поэтому лучше публиковать sources и javadoc. Для этого добавляем в pom.xml два плагина:
org.apache.maven.plugins maven-source-plugin 3.2.1 attach-sources jar org.apache.maven.plugins maven-javadoc-plugin 3.2.0 attach-javadocs jar
Теперь при выполнении команды deploy в репозиторий будут загружены дополнительные артефакты с классификаторами «-sources» и «-javadoc», и соответствующая информация будет автоматически доступна в IDE разработчика, использующего нашу библиотеку.
Модули
До сих пор мы работали с приложением, состоящим из одного модуля. Однако модули — едва ли не главная функциональность Maven, сильнее всего влияющая на процесс проектирования и разработки приложений. Сегодня в эпоху микросервисов становится всё более популярно использовать один репозиторий (GIT) для одного сервиса. В этом случае объём кода и логики одного приложения часто не требует разделения на модули. Сейчас я не буду никак сравнивать монолит с микросервисной архитектурой. Многие работают с монолитом или как минимум c приложениями с большим объёмом кода в одном репозитории. В этом случае разделение кода на модули имеет тот же смысл, что разделение на пакеты, классы, методы.
Разделение на пакеты — по большей части логическое, визуальное, если не считать модификаторов доступа protected и default. При разделение на модули код одного модуля ни во время компиляции, ни во время исполнения ничего не знает о других модулях, если не установлена зависимость. Одна из ситуаций, когда модули жизненно необходимы — если в одном проекте уживаются вместе несколько приложений. Например, несколько web приложений, которые мы собираем в отдельные WAR архивы и может даже развертываем на разных серверах Apache Tomcat. Каждое приложение мы помещаем в отдельный модуль. Приложения не полностью различны, они как-то перекликаются, относятся к единому бизнес домену, поэтому возникают повторяющиеся классы и методы, то есть дублирование кода. Тогда мы выделяем отдельный модуль common, куда перемещаем все общие части. Потом нам хочется больше модулей, чтобы каждый отвечал за свою задачу, а не содержал сборную солянку разных утилит. Тогда в дополнение к common мы вводим модули security, model, dao и так далее. Просто чтобы в коде было чисто и аккуратно. Бонусом получаем скорость сборки, ведь нет смысла пересобирать модули, в которых не было изменений.
Код примера. Рассмотрим небольшое приложение, состоящее из нескольких модулей. Программа подсчитывает частоту появления различных слов во входном файле. В модуле common реализован сам алгоритм, модуль consoleapp содержит главный класс для запуска из консоли, а модуль webapp — Web приложение с REST сервисом. Между webapp и сonsoleapp нет зависимостей, но оба зависят от common. Можно представить, что приложение существует давно, а для работы с ним всегда использовалась консоль, но теперь решили добавить ещё и веб сервис. В коде приложения ничего показательного, его я приводить не буду, лучше сконцентрируюсь на Maven.
При создании проекта в корне я сразу удалил папку src, а в pom.xml установил свойство packaging в значение «pom». Также artifactId имеет окончание «-parent», но это необязательно, больше для удобства и потому что так принято.
hipravin.samples.maven samples-maven-multiple-parent 1.0-SNAPSHOT pom
Такой модуль называют родительским (parent) или иногда основным, главным. Он обычно не содержит исходного кода и артефактов сборки. Его предназначение — управлять остальными модулями. Все они должны быть перечислены в теге modules:
common consoleapp webapp
Все дочерние (child) модули наследуют свойства, зависимости, плагины от родительского модуля. Например, уровень языка для compiler плагина достаточно указать только в главном модуле. А вот зависимости в главном модуле указывать не стоит, потому что исключить их в дочерних модулях будет крайне затруднительно. Вместо этого в родительском модуле фиксируют список библиотек их версий, а в дочерних — лишь ссылаются на них. Выглядит это так: в главном модуле в pom.xml используется тег dependencyManagement, а в дочерних — dependency без версии:
. org.junit.jupiter junit-jupiter-engine 5.4.0 test org.junit.jupiter junit-jupiter-engine
Помимо перечисления версий библиотек в dependencyManagement по одной, существует дополнительный механизм указания версий для целой группы зависимостей — BOM (Bill Of Materials). Это очень полезно для проектов с большим количеством модулей (например, для spring: core, context, beans, web, jdbc, . ). Модуль webapp использует Spring Boot, BOM можно указать в главном модуле следующим образом:
org.springframework.boot spring-boot-dependencies 2.2.6.RELEASE pom import . org.springframework.boot spring-boot-starter-web
Ключевым здесь является значение параметра scope равное import.
Для проектов, использующих Spring Boot альтернативный вариант — указать в качестве родительского проекта spring-boot-starter-parent. То есть родительский модуль не обязательно должен располагаться рядом в том же проекте, он может загружаться и из удалённого Maven репозитория. В этом случае нужно установить свойство relativePath в пустое значение. Например, так:
org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE
При этом родительский модуль может быть только один.
Чтобы классы из модуля common были доступны в модуле consoleapp, нужно добавить зависимость так же, как ранее мы добавляли зависимость на библиотеку jackson.
hipravin.samples.maven common $
Вместо версии 1.0-SNAPSHOT мы ссылаемся на версию родительского модуля, потому что неразумно иметь разные версии в рамках одного проекта, а дублировать эту версию многократно неудобно. На этом этапе может возникнуть определённая путаница, как в понимании происходящего, так и в работе самого Maven. Как мы знаем, зависимости загружаются из репозитория, либо берутся напрямую из папки .m2. Но мы ещё ни разу не собирали наш проект и тем более не выполняли install или deploy. В рамках одного проекта Maven в этом не нуждается — он выстроит дерево зависимостей наших модулей, причём именно дерево, а не граф, потому что циклы запрещены. Потом он осуществит сборку модулей в правильном порядке и при работе с каждым модулем все его зависимости уже будут обработаны. Если какой-то из модулей Maven упорно пытается искать в репозитории, то вероятно допущена ошибка в координатах зависимости.
Чтобы собрать весь проект целиком достаточно запустить команду «mvn package» в корне проекта. Так же с install и deploy. Так выглядит лог успешной сборки:
. [INFO] Reactor Summary for samples-maven-multiple-parent 1.0-SNAPSHOT: [INFO] [INFO] samples-maven-multiple-parent . SUCCESS [ 1.012 s] [INFO] common . SUCCESS [ 10.054 s] [INFO] consoleapp . SUCCESS [ 1.032 s] [INFO] webapp . SUCCESS [ 4.769 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
Довольно редко возникает необходимость собирать только один модуль, однако если проект громоздкий и полный билд занимает минуты, то это может быть полезно. Для этого нужно указать список модулей для сборки, например один модуль webapp, а также параметр —also-make, чтобы были обработаны необходимые модули, от которых явно или неявно зависит webapp.
clean package --projects webapp --also-make . [INFO] Reactor Summary for samples-maven-multiple-parent 1.0-SNAPSHOT: [INFO] [INFO] samples-maven-multiple-parent . SUCCESS [ 0.382 s] [INFO] common . SUCCESS [ 6.051 s] [INFO] webapp . SUCCESS [ 2.755 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
Заметим, что сейчас в проекте несколько раз дублируется версия родительского модуля. Это вполне допустимо, но при обновлении версии на, скажем, 1.0 или 1.1-SNAPSHOT, нам придётся обновлять код в нескольких местах, что создаёт вероятность ошибки, вызванной человеческим фактором. Хуже всего обновлять по памяти: тут, тут и тут, потому что легко можно случайно забыть или пропустить один из модулей. Если артефакты попадают в локальный репозиторий, то проект продолжит собираться без ошибок, но код одного из модулей будет использовать старую версию родительского модуля. Второй вариант — использовать автозамену по проекту (Ctrl+Shift+R в IDEA). Этот вариант плох тем, что можно случайно заменить лишнего — но чаще всего в этом случае проект просто не скопмилируется. Правильный способ обновлять версию — использовать плагин versions:
mvn versions:set -DnewVersion=1.1-SNAPSHOT mvn versions:commit
Команда «commit» здесь не имеет ничего общего с коммитом в системе контроля версий, это специфический этап работы плагина, удаляющий сохранённую копию pom.xml файла, которая создаётся на первом этапе.
Разное
О нескольких моментах стоит упомянуть для полноты картины, хоть им не нашлось места в демонстрационных проектах, описанных ранее.
Параметр optional. Модули webapp и consoleapp транзитивно зависят от всех библиотек, от которых зависит common. Эти библиотеки можно исключить в pom.xml каждого из этих модулей, используя exclusions, как мы уже видели. Альтернативно можно указать на этих зависимостях в модуле common параметр optional в значение true, тогда в webapp и consoleapp изменения не потребуются. Используется редко, не буду заострять внимание на этом.
Беспорядок с версиями зависимостей. Версия одной и той же библиотеки может быть указана явно единожды, транзитивно единожды, явно многократно, транзитивно многократно. Теоретически у Maven есть детерминированный алгоритм по определению версии. Практически лучше избегать неопределённости и изучать граф зависимостей, а также список библиотек, попадающий в артефакт сборки JAR with dependencies, WAR или EAR. Версия, указанная явно в pom.xml самого модуля имеет приоритет над транзитивными версиями. Однако если версии отличаются ещё и в group id / artifact id как, например, log4j и log4j2, то проблему можно решить только аккуратным исключением всех лишних зависимостей. А найти проблему можно, опять же, только анализом графа зависимостей и артефактов сборки.
Dependency scope. Для каждой зависимости можно указать scope. Часть значений влияет на то, в какие classpath попадает данная зависимость, другие просто определяют некое особое поведение. Мы уже встречали compile (значение по умолчанию), test и import. Я просто приведу список всех значений с небольшими комментариями. Я буду писать «сохраняется в lib» имея в виду, что библиотека попадает в classpath во время исполнения, а также копируется в директорию lib внутри артефактов сборки таких как WAR и EAR.
compile Значение по умолчанию. Зависимость доступна во время компиляции основного кода и тестов, сохраняется в lib. provided Зависимость доступна во время компиляции основного кода и тестов, но не сохраняется в lib. Применяется, когда библиотека предоставляется контейнером. Например, Weblogic предоставляет драйвер для соединения с базой данных. runtime Зависимость не доступна во время компиляции основного кода, доступна для компиляции тестов (не знаю зачем), но сохраняется в lib. Пример — драйвер базы данных, библиотеки логирования. test Зависимость не доступна во время компиляции основного кода, доступна для компиляции тестов, не сохраняется в lib. system Позволяет подключить библиотеку, jar файл которой располагается по определённому пути на файловой системе. Не рекомендую к использованию, в этом случае следует просто установить библиотеку в локальный репозиторий с помощью install-file. import Используется в dependencyManagement вместе c так называемым BOM (bill of materials)
Я не разбираю scope подробно, потому что в большинстве случаев достаточно compile и test, которые тривиальны, а остальные применяются по ситуации и редко приводят к скрытым ошибкам. А вот понять и запомнить чем отличается provided от runtime при первом знакомстве мало кому удаётся.
Жизненный цикл, фазы. Жизненный цикл состоит из фаз, которые мы можем указывать в строке запуска mvn. Каждый плагин запускается в ту фазу, которая указана в его конфигурации. Список всех фаз: validate, compile, test, package, verify, install, deploy. Опять же углубляться не буду, полагаю станет только непонятней. С практической точки зрения мы уже рассмотрели все основные фазы жизненного цикла.
Архетипы. Без использования IDE чтобы создать пустой maven проект нужно будет где-то взять заголовок xml файла и добавить в него как минимум координаты проекта. А если наш проект использует какой-то фреймворк, то потребуются ещё какие-нибудь обязательные настройки и файлы. В Maven существует понятие архетипа — способа создавать готовые проекты по шаблону с указанием набора параметров. Spring initializer, вероятно, внутри работает на основе архетипов. Однако это отдельный сайт, да и ещё со встроенной поддержкой в IDEA, поэтому пользоваться шаблонами Spring Boot через интерфейс командной строки было бы странно. В своей практике я не применял архетипы кроме как в ознакомительных целях.
Gradle. Gradle — аналог Maven, который появился чуть позже и считается более продвинутым, современным, стильным — модным — молодежным. Основное различие между ними — Gradle использует язык Groovy или Kotlin для конфигурации, а не XML, а также по-другому определяет жизненный цикл. Одно и то же приложение может одновременно иметь эквивалентные конфигурации сборок на Maven и Gradle. На сайте spring.io примеры одновременно содержат инструкции и для Maven, и для Gradle. При этом сам springframework начиная как минимум с версии 4 собирается с помощью Gradle. В работе же я встречал только Maven, если не считать одного приложения, которое потом перенесли на Maven для порядка и потому что Gradle билд сломался, а починить никто не сумел. По своему опыту могу только сказать, что Gradle очень плохо настраивается в окружении, где отсутствует или ограничен доступ в интернет. Не исключено, что при должной сноровке это возможно, но у кого она есть, эта сноровка. Я уверен, что в коммерческой разработке Maven ещё долго будет популярен благодаря старым проектам и наработанному специалистами опыту.
Заключение
В Mаven очень много нюансов и тонкостей, но в целом это очень стройная и эффективная технология и экосистема. Так или иначе, в мире Java разработки встречи с Maven не избежать. Надеюсь, что наиболее частые, полезные и хитрые сценарии я как-нибудь, да затронул.
В чем набрать и чем собрать C++ проект
Задавшись этим вопросом я, в первую очередь, сформулировал требования: жесткие и опциональные (но желательные) для системы сборки и графической среды разработки.
Сразу хочу отметить что речь идет о написании C++ кода не под какую-то специфичную платформу типа Android или фреймворка, например Qt, — где все уже готово, как с построением так и с редактированием кода, а об generic коде не привязанному к конкретной платформе или фреймворку.
Общие:
- Свободный.
- Кросплатформенный (по крайней мере Windows и Linux).
Система сборки:
- Единая команда для сборки на разных платформах.
- Инкрементальная сборка с корректным учетом всех зависимостей: заголовочных файлов и сторонних компонентов, использующихся для сборки.
- Сборочный скрипт должен содержать только необходимый минимум конфигурации специфичный для конкретного проекта. Общая логика билда не должна кочевать из скрипта в скрипт, а находится в билд системе или ее плагинах.
- Встроенная параллельная сборка.
- Поддержка различных тулчейнов (хотя бы gcc, Visual C++, CLang).
- Возможность смены тулчейна с минимальными затратами, без переписывания всего билд скрипта.
- Легко переключаемые варианты построения: Debug и Release.
- Совершенно нежелательны зависимости на какие-то дополнительные низкоуровневые тулзы типа make. Одним словом система сборки должна быть самодостаточной.
- Очень желательна интеграция системы сборки с репозиториями сторонних компонентов типа pkg-config или как Maven Central для JVM.
- Система сборки должна быть расширяемой плагинами, т.к. процедура сборки для каждого конкретного проекта может оказаться сложнее типовой концепции построения (генерация кода, например, или сборка некоего нестандартного образа).
- Удобно, когда сценарий сборки представляет собой какой-то высокоуровневый язык программирования или еще лучше DSL. Это позволит не особо затратно и выразительно менять поведение построения прямо в скрипте.
- При настройке компилятора и линковщика из сценария сборки весьма удобно когда билд системой предоставляются хотя бы базовые абстракции: например, хочется добавить макрос — зачем думать какой параметр командной строки компилятора отвечает за это? /D на MSVC или -D на gcc — пусть система сборки разрулит эти несущественные детали сама.
- Хорошая интеграция с графическими средами разработки (IDE).
IDE:
- Способность IDE корректно «понимать» C++ код. IDE должна уметь индексировать все файлы проекта, а также все сторонние и системные заголовочные файлы и определения (defines, macro).
- IDE должна предоставлять возможность кастомизации команд для построения проекта, а так же где искать заголовочные файлы и определения.
- Должна эффективно помогать в наборе кода, т.е. предлагать наиболее подходящие варианты завершения, предупреждать об ошибках синтаксиса и т.д.
- Навигация по большому проекту должна быть удобной, а нахождение использования быстрым и простым.
- Предоставлять широкие возможности для рефакторинга: переименование и т.д.
- Также необходима способность к генерации шаблонного кода — создание каркаса нового класса, заголовочного файла и файла с реализацией. Генерация геттеров/сеттеров, определения методов, перегрузка виртуальных методов, шаблоны реализации чисто виртуальных классов (интерфейсов) и т.д.
- Подсветка и поддержка тегов документирования кода, таких, как Doxygen.
Make — [античность] мастодонт и заслуженный ветеран систем сборки, которого все никак не хотят отпустить на пенсию, а заставляют везти на себе все новые и новые проекты. Это очень низкоуровневая тулза со своим специфичном языком, где за пробел вместо таба вам сразу же грозит расстрел на месте. С помощью make можно сделать все, что угодно — билд любой сложности, но за это придется заплатить усилиями для написания скрипта, а также его поддержки в актуальном состоянии. Переносить логику билда из проекта в проект также будет накладно. Существуют некие современные «заменители» make-а: типа ninja и jam, но сути они не меняют — это очень низкоуровневые инструменты. Так же, как и на ассемблере можно написать все что угодно, только стоит ли?
CMake — [средневековье] первая попытка уйти от низкоуровневых деталей make-а. Но, к сожалению, далеко уйти не удалось — движком здесь служит все тот же make для которого CMake генерирует огромные make-файлы на основе другого текстового файла с более выскоуровневым описанием билда. По схожему принципу работает и qmake. Такой подход напоминает мне красивый фасад старого деревянного дома, который заботливо обшили свежим пластиком. CMake стабильная и хорошо зарекомендовавшая себя система, есть даже встроенная интеграция с Eclipse, но, к сожалению, мне не подошла потому что противоречит части требований изложенных в начале статьи. Под Linux все вроде бы хорошо, но если нужно построить тот же проект под Windows с помощью MSVC — а я предпочитаю нативный компилятор MinGW-шному, будут сгенерированы файлы для NMake. Т.е. зависимости на еще одну тулзу и разные команды на сборку для другой платформы. И все это следствия чуток кривоватой архитектуры, когда основная часть работы выполняется другими «помощниками».
Ant — [эпоха возрождения] своеобразный клон make для Java. Скажу честно, я потратил совсем немного времени для проверки Ant (а так же Maven) в качестве билд системы для C++. И у меня сразу же появилось ощущение что поддержка С++ здесь чисто «для галочки» и недостаточно развита. К тому же даже в Java проектах Ant уже используется достаточно редко. В качестве языка сценария (так же как и для Maven) здесь выбран XML — этот гнусный птичий язык :). Этот факт оптимизма мне совсем не прибавил для дальнейшего погружения в тему.
SCons — [новые времена] самодостаточная, кросплатформенная билд система, написанная на Python. SCons одинаково хорошо справляется как с Java так и с C++ билдами. Зависимости хидеров для инкрементальной сборки отрабатываются корректно (насколько я понял создается некая база данных с метаданными билда), а на Windows «без бубна» работает MSVC. Язык сценария сборки — Python. Весьма достойная система, и я даже хотел закончить свои изыскания на ней, но как известно, нет пределу совершенства, и при более детальном осмотре выявились некоторые минусы в свете вышеизложенных требований.
Нет никаких абстрактных настроек для компилятора, поэтому если, например, возникнет необходимость сменить тулчейн, возможно, понадобиться искать места в билд скрипте для внесения изменений. Те же макросы придется прописывать с вложенными условиями — если это Виндовс то сделай так, если это GCC сделай так и т.д.
Нет поддержки удаленных артефакториев и высокоуровневой зависимости одного билда на другой.
Общая архитектура построена так, что так называемые user defined builders существуют практически изолированно и нет возможности заиспользовать уже существующую логику билда, чтобы дополнить ее своей через несложный плагин. Но в целом это достойный выбор для небольших проектов.
Gradle [современность] — у меня уже был позитивный опыт использования Gradle для Java и Kotlin проектов и я возлагал на него большие надежды.
Для JVM языков в Gradle очень удобная концепция работы с библиотеками, необходимыми для построения проекта (билд зависимостями):
- В скрипте прописываются адреса репозиториев с артефактами: maven или ivy — например. Так же это может быть репозиторий любого другого типа/формата — лишь бы был плагин для него. Это может быть удаленный репозиторий, какой-нибудь Maven Central или ваш личный хостинг где-нибудь в сети или просто локальная репа на файловой системе.
- Так же в специальном разделе скрипта указываются непосредственно зависимости для построения — список необходимых бинарных артефактов с указанием версий.
- Перед началом построения Gradle пытается зарезолвить все зависимости и ищет артефакты с заданными версиями по всем репозиториям. Бинарники загружаются в кэш и автоматически добавляются в билд. Это очень удобно и я надеялся, что для C++, возможно, сделали нечто подобное.
Но, оказалось, что есть и «новые» плагины для поддержки C++: `cpp-application` — для приложений, `cpp-library` для библиотек: статических и динамических и наконец `cpp-unit-test` для юнит тестирования. И это было то что я искал! 🙂
Структура папок проекта по умолчанию похожа на проект для Java:
- src/main/cpp — корневая папка для основных файлов *.cpp проекта.
- src/main/headers — папка для внутренних заголовочных файлов.
- src/main/public — папка для экспортируемых заголовков — для библиотек.
- src/test/cpp — папка для файлов *.cpp юнит теста.
Кстати, билд скрипт — обычно build.gradle, это DSL языка Groovy или Kotlin (build.gradle.kts) на выбор. Внутри скрипта всегда доступен Gradle API и API добавленных в скрипт плагинов.
Для библиотек можно выбрать тип: статическая или динамическая (или собрать оба варианта).
По умолчанию сконфигурированы два варианта построения: Debug (gradle assemble) и Release (gradle assembleRelease).
Принцип запуска юнит тестирования такой же как в Java: gradle test выполнит постройку основного компонента, потом тестов, если они есть в папке src/test/cpp, а затем выполнит тестовое приложение.
Пресловутые дефайны можно задавать абстрактно — Gradle сам сгенерирует необходимые параметры компилятора. Есть еще несколько абстрактных настроек типа оптимизации, отладочной информации и т.д.
Из коробки поддерживаются GCC, Microsoft Visual C++, CLang.
Система плагинов очень развита, а архитектура расширений устроена удобно — можно взять готовую логику и задекорировать/расширить ее. Плагины бывают двух видов: динамические, которые пишутся прямо на Groovy и встраиваются в скрипт или написанные на Java (или на другом языке с JVM) и скомпилированные в бинарные артефакты. Для плагинов существует бесплатный Gradle-артифакторий, в котором любой желающий может разместить свой плагин, который будет доступен всем. Что успешно и проделал автор этой статьи 🙂 но об этом чуть позже.
Хотелось бы подробнее остановиться теперь на системе работы с бинарными компонентами в Gradle для C++: она почти такая же как в Java! Билд зависимости работают практически так же как я описал выше.
Возьмем для примера композитный билд:
- utils — папка с библиотекой
- app — папка с приложением, которое использует utils.
- settings.gradle — Gradle файл для объединения этих двух компонент в композитный билд.
dependencies
А все остальное проделает Gradle! Добавит в компилятор путь для поиска заголовочных файлов utils и прилинкует бинарь библиотеки.
И все это одинаково хорошо сработает как под Linux GCC, так и под Windows MSVC.
Инкрементальная сборка, естественно, тоже замечательно работает и при изменении хидеров в utils будет перестроен app.
Как оказалось, в Gradle пошли дальше и реализовали возможность выкладывать C++ артефакты в Maven Repository! Для этого используется стандартный `maven-publish` плагин.
В скрипте необходимо указать репозиторий куда вы хотите выложить свой артефакт и сделать gradle publish (или gradle publishToMavenLocal для локальной публикации). Gradle сбилдит проект и
выложит в специальном формате — с учетом версии, платформы, архитектуры и варианта билда.
Выкладываются сами бинарные файлы библиотек и публичные заголовочные файлы — из папки src/main/public.
Понятно что выложить С++ артефакт на Maven Cental нельзя — он не пройдет обязательные проверки системы. Но поднять Maven репозиторий в сети совсем нетрудно, а для локального репозитория вообще ничего делать не нужно — это просто папка на диске.
Теперь если вы хотите использовать в своем проекте чью-то библиотеку вы можете написать в билд скрипте что-то вроде:
repositories < maven < url = 'https://akornilov.bitbucket.io/maven' >> unitTest < dependencies < implementation 'org.bitbucket.akornilov.tools:gtest:1.8.1' >>
Здесь говориться что для юнит тестирования нужно использовать артефакт gtest версии 1.8.1 из Maven репозитория.
Это, кстати, вполне реальный репозиторий в котором выложен мой билд Google Test v1.8.1, простроенный с помощью Gradle для Windows и Linux x86_64.
Естественно, что всю низкоуровневую работу по конфигурированию компилятора и линковщика для работы с внешним компонентом Gradle берет на себя. Вам достаточно заявить о своих намерениях использовать такую-то библиотеку с такой-то версией из такого-то репозитория.
Для интеграции с IDE в Gradle есть два встроенных плагина для Visual Studio и XCode. Они хорошо работают, за исключением того что Visual Studio плагин игнорирует код юнит тестов из папки src/test/cpp и генерирует проект только для основного кода.
Теперь пришло время поговорить об IDE и о том как подружить их с Gradle
Eclipse CDT (2018-12R) — зрелый и качественный продукт. Если ему удалось успешно пропарсить Ваш проект, значит Вам повезло — редактировать будет комфортно. Скорее всего он даже «поймет» самые замороченные типы auto. Но если нет… Тогда он будет яростно подчеркивать красным пунктиром все подряд и ругаться нехорошими словами. Например, он не переваривает стандартные заголовочные файлы MSVC и Windows SDK. Даже вполне безобидный printf подчеркивается красным пунктиром и не воспринимается как нечто осмысленное. Там же оказался и std::string. Под Linux с родным ему gcc все замечательно. Но даже при попытке заставить его индексировать проект из родственного Android Native начались проблемы. В заголовках bionic он в упор отказывался видеть определение size_t, а заодно и всех функций которые его использовали. Наверное, под Windows можно исправить ситуацию если вместо заголовочных файлов Microsoft подсунуть ему, например, Cygwin или MinGW SDK, но мне такие фокусы не очень интересны, мне бы все же хотелось чтобы софт такого уровня «кушал то что дают», а не только то что он «любит».
Возможности по навигации, рефакторингу и генерации шаблонного кода замечательные, но вот к помощнику при наборе букв есть вопросы: допустим набираем несколько символов из какого-то длиннющего имени, почему бы не предложить варианты завершения? Нет, помощник терпеливо дожидается пока пользователь доберется до. или -> или . Приходится постоянно нажимать Ctrl + Space — раздражает. В Java эту досадную недоделку можно было исправить выбрав в качестве триггера весь алфавит в CDT же я не нашел простого решения.
NetBeans 8.1/10.0 — доводилось пользоваться этим IDE для Java, запомнился как неплохой и легковесный софт со всем необходимым функционалом. Для C++ у него есть плагин разработанный не сообществом, а непосредственно NetBeans. Для C++ проектов существует довольная жесткая зависимость на make и gcc. Редактор кода неторопливый. В генераторе шаблонного кода не нашел очень простую вещь: добавляем новый метод в заголовочном файле класса — нужно сгенерировать тело метода в cpp файле — не умеет. Степень «понимания» кода средняя, вроде бы что-то парсит, а что-то нет. Например, итерирование по мапе с автоитератором для него уже сложновато. На макросы из Google Test ругается. Закастомизировать билд команды проблематично — на Linux при доступном gcc и make (это при том что используется уже другая билд система) сработает, на Windows потребует MinGW, но даже при его наличии откажется построить. В целом работа в NetBeans с C++ возможна, но комфортной я бы ее не назвал, наверное, надо очень любить эту среду, чтобы не замечать разные ее болячки.
KDevelop 5.3.1 — когда-то задумывался как инструмент разработчика для KDE (Linux), но сейчас есть версия и под Windows. Имеет быстрый и приятный редактор кода с красивой подсветкой синтаксиса (основан на Kate). Закостомизировать левую билд систему не получится — для него основная система сборки CMake. Толерантно относится к MSVC и Windows SDK заголовкам, во всяком случае printf и std::string точно не приводят его в ступор как Eclipse CDT. Очень шустрый помощник по написанию кода — хорошие варианты завершения предлагает почти сразу во время набора текста. Имеет интересную возможность по генерации шаблонного кода: можно написать свой шаблон и выложить его онлайн. При создании по шаблону можно подключиться к базе данных готовых шаблонов и загрузить понравившийся. Единственное что расстроило: встроенный шаблон по созданию нового класса криво работает как под Windows так и под Linux. Wizard-а по созданию класса имеет несколько окон, в которым можно много чего настроить: какие конструкторы нужны, какие члены класса и т.д. Но на финальной стадии под Windows выскакивает какая-то ошибка успеть разглядеть текст которой невозможно и создаются два файла h и cpp размером 1 байт. В Linux почему-то нельзя выбрать конструкторы — вкладка пустая, а на выходе корректно генерится только заголовочный файл. В общем-то, детские болезни для такого зрелого продукта смотрятся как-то несерьезно.
QtCreator 4.8.1 (open source edition) — наверное, услышав это название, Вы недоумеваете, как сюда затесался этот монстр заточенный под Qt с дистрибутивом в гигабайт с гаком. Но речь идет о «легкой» версии среды для generic проектов. Его дистрибутив весит всего около 150 Мб и не тащит с собой вещи специфичные для Qt: download.qt.io/official_releases/qtcreator/4.8.
Собственно, он умеет делать почти все, о чем я написал в своих требованиях, быстро и корректно. Он парсит стандартные заголовки как Windows так и Linux, кастомизируется под любую билд систему, подсказывает варианты завершения, удобно генерит новый классы, тела методов, позволяет выполнять рефакторинг и навигацию по коду. Если хочется просто комфортно работать, не думая постоянно о том, как побороть ту или иную проблему есть смысл присмотреться к QtCreator-у.
Собственно, осталось рассказать о том, чего мне не хватило в Gradle для полноценной работы: интеграция с IDE. Чтобы билд система сама сгенерировала бы проектные файлы для IDE, в которых уже были бы прописаны команды для построения проекта, перечислены все исходные файлы, необходимы пути для поиска заголовочных файлов и определения.
Для этой цели мной был написан плагин для Gradle `cpp-ide-generator` и опубликован на Gradle Plugin Portal.
Плагин может использоваться только совместно с `cpp-application`, `cpp-library` и `cpp-unit-test`.
Вот пример его использования в build.gradle:
plugins < id ‘cpp-library’ id ‘maven-publish’ id ‘cpp-unit-test’ id ‘org.bitbucket.akornilov.cpp-ide-generator’ version ‘0.3’ >library < // Library specific parameters >// Configuration block of plugin: ide
Плагин поддерживает интеграцию со всеми вышеперечисленными графическими средами разработки, но в конфигурационном блоке плагина — ide можно отключить поддержку ненужных IDE:
kdevelop = false
Если параметр autoGenerate выставлен в true, проектные файлы для всех разрешенных IDE будут автоматически генерироваться прямо во время билда. Также в режиме автоматической генерации проектные файлы будут удаляться при очистке билда: gradle clean.
Поддерживается инкрементальная генерация, т.е. обновляться будут только те файлы, которые требуют реального обновления.
Вот список целей, которые добавляет плагин:
- generateIde — сгенерировать проектные файлы для всех разрешенных IDE.
- cleanIde — удалить проектные файлы для всех разрешенных IDE.
- generateIde[имя] — сгенерировать проектные файлы для IDE с заданным именем (IDE должно быть разрешено), например generateIdeQtCreator.
- Доступные имена: Eclipse, NetBeans, QtCreator, KDevelop.
- cleanIde[имя] — удалить проектные файлы для IDE с заданным именем, например cleanIdeQtCreator.
Второй плагин, который мне пришлось сделать, называется `cpp-build-tuner` и он также работает в паре с cpp-application`, `cpp-library` и `cpp-unit-test`.
У плагина нет никаких настроек, его достаточно просто зааплаить:
plugins
Плагин выполняет небольшие манипуляции с настройками тулчейнов (компилятора и линковщика) для разных вариантов билда — Debug и Release. Поддерживаются MSVC, gcc, CLang.
Особенно это актуально для MSVC, потому что по умолчанию в результате релизного билда Вы получите «жирный», не эстетичный бинарь с дебажной информацией и статически прилинкованной стандартной библиотекой. Часть настроек для MSVC я «подсмотрел» в самой Visual Studio, которые по дефолту он добавляет в свои C++ проекты. Как для gcc/CLang так и для MSVC в профиле Release включаются link time optmizations.
Заметка: Плагины проверялись с последней версией Gradle v5.2.1 и не тестировались на совместимость с предыдущими версиями.
Исходные коды плагинов, а так же простенькие примеры использования Gradle для библиотек: статических и динамических, а так же приложения, которое их использует можно посмотреть: bitbucket.org/akornilov/tools дальше gradle/cpp.
Так же в примерах показано, как пользоваться Google Test для юнит тестирования библиотек.
Версии QtCreator под Windows старше 4.6.2 (и по крайней мере, на момент написания этих строк, до 4.10 включительно) «разучились» понимать SDK MSVC. Все из пространства std:: подчеркивают красным и отказываются индексировать. Поэтому, на данный момент, версия 4.6.2 наиболее подходящая для работы под Windows.
Была выпущена новая версия плагина cpp-build-tuner v1.0 (и cpp-ide-generator v0.5 – небольшие улучшения).
1) В cpp-build-tuner добавлен блок конфигурации.
buildTuner < lto = false gtest = '1.8.1' libraries < common = ['cutils.lib'] windows = ['ole32', 'user32'] linux = ['pthread', 'z'] >libDirs.common = ['../build/debug', '../release'] >
lto (булевое значение) –разрешает или запрещает LTO для релизного билда. По умолчанию включено.
gtest (строка) – добавлет поддержку Google Test для юнит тестов. На данный момент поддерживается только версия 1.8.1 для GCC, MinGW-W64, и MSVC.
libraries (контейнер) – список библиотек для линковки. Внутри контейнера есть три поля (список строк): common – библиотеки для любой платформы, windows – только для Windows и linux – только для Linux.
libDirs (контейнер) – список папок для поиска библиотек линковщиком. Структура контейнера такая же как и у списка библиотек.
2) Добавлена возможность запуска приложения для cpp-application . Плагин добавляет в проект дополнительные задачи для этого: run , runDebug (тоже самое, что и run ) и runRelease . Задачи зависят от assemble , assembleDebug и assembleRelease соответственно.
Аналогично стандартному плагину “Java Application plugin” можно передавать параметры командной строки при запуске: gradle run —args=»arg1 arg2 . » .
В связи со сменой хостинга плагинов, была изменена группа:
Gradle: управляя зависимостями
Управление зависимостями – одна из наиболее важных функций в арсенале систем сборки. С приходом Gradle в качестве основной системы сборки Android-проектов в части управления зависимостями произошёл существенный сдвиг, закончилась эпоха ручного копирования JAR-файлов и долгих танцев с бубном вокруг сбоящих конфигураций проекта.
В статье рассматриваются основы управления зависимостями в Gradle, приводятся углублённые практические примеры, небольшие лайфхаки и ссылки на нужные места в документации.
Репозиторий
Как известно, Gradle не имеет собственных репозиториев и в качестве источника зависимостей использует Maven- и Ivy-репозитории. При этом интерфейс для работы с репозиториями не отличается на базовом уровне, более развёрнуто об отличиях параметров вы можете узнать по ссылкам IvyArtifactRepository и MavenArtifactRepository. Стоит отметить, что в качестве url могут использоваться ‘http’, ‘https’ или ‘file’ протоколы. Порядок, в котором записаны репозитории, влияет на порядок поиска зависимости в репозиториях.
// build.gradle repositories < maven < url "http://example.com" >ivy < url "http://example.com" >>
Объявление зависимостей
// build.gradle apply plugin: 'java' repositories < mavenCentral() >dependencies
В приведённом выше примере вы видите сценарий сборки, в котором подключены две зависимости для различных конфигураций (compile и testCompile) компиляции проекта. JsonToken будет подключаться во время компиляции проекта и компиляции тестов проекта, jUnit только во время компиляции тестов проекта. Детальнее о конфигурациях компиляции — по ссылке.
Также можно увидеть, что jUnit-зависимость мы подключаем как динамическую(+), т.е. будет использоваться самая последняя из доступных версия 4.+, и нам не нужно будет следить за минорными обновлениями (рекомендую не использовать эту возможность в compile-типе компиляции приложения, т.к. могут появиться неожиданные, возможно, сложно локализуемые проблемы).
На примере с jUnit-зависимостью рассмотрим стандартный механизм Gradle по поиску необходимой зависимости:
1. Зависимость compile ("org.junit:junit:4.+") 2. Получение версии модуля group: "org.junit" name: "junit" version: "4.+" 3. Получение списка возможных версий модуля [junit:4.1] … [junit:4.12] 4. Выбор одной версии зависимости [junit:4.12] 5. Получение версии зависимости [junit:4.12] dependencies < … >artifacts < … >6. Присоединение артефактов зависимости к проекту junit-4.12.jar junit-4.12-source.jar junit-4.12-javadoc.zip
Кэш
В Gradle реализована система кэширования, которая по умолчанию хранит зависимости в течение 24 часов, но это поведение можно переопределить.
// build.gradle configurations.all
После того, как время, установленное для хранения данных в кэше, вышло, система при запуске задач сначала проверит возможность обновления динамических (dynamic) и изменяемых (changing) зависимостей и при необходимости их обновит.
Gradle старается не загружать те файлы, которые были загруженны ранее, и использует для этого систему проверок, даже если URL/источники файлов будут отличаться. Gradle всегда проверяет кэш (URL, версия и имя модуля, кэш других версий Gradle, Maven-кэш), заголовки HTTP-запроса (Date, Content-Length, ETag) и SHA1-хэш, если он доступен. Если совпадений не найдено, то система загрузит файл.
Также в системе сборки присутствуют два параметра, используя которые при запуске вы можете изменить политику кэширования для конкретного выполнения задачи.
– –offline – Gradle никогда не будет пытаться обратиться в сеть для проверки обновлений зависимостей.
– –refresh-dependencies – Gradle попытается обновить все зависимости. Удобно использовать при повреждении данных, находящихся в кэше. Верифицирует кэшированные данные и при отличии обновляет их.
Более детально про кэширование зависимостей можно прочитать в Gradle User Guide.
Виды зависимостей
Существует несколько видов зависимостей в Gradle. Наиболее часто используемыми являются:
– Внешние зависимости проекта — зависимости, загружаемые из внешних репозиториев;
// build.gradle dependencies
– Проектные зависимости — зависимость от модуля (подпроекта) в рамках одного проекта;
// build.gradle dependencies
– Файловые зависимости — зависимости, подключаемые как файлы (jar/aar архивы).
build.gradle repositories < flatDir < dirs 'aarlibs' // инициализируем папку, хранящую aar-архивы как репозиторий >> dependencies < compile(name:'android_library', ext:'aar') // подключаем aar-зависимость compile files('libs/a.jar', 'libs/b.jar') compile fileTree(dir: 'libs', include: '*.jar') >
Также существуют зависимости клиентских модулей, зависимости Gradle API и локальные Groovy-зависимости. Они используются редко, поэтому в рамках данной статьи не будем их разбирать, но почитать документацию о них можно здесь.
Дерево зависимостей
Каждая внешняя или проектная зависимость может содержать собственные зависимости, которые необходимо учесть и загрузить. Таким образом, при выполнении компиляции происходит загрузка зависимостей для выбранной конфигурации и строится дерево зависимостей, человеческое представление которого можно увидеть, выполнив Gradle task ‘dependencies’ в Android Studio или команду gradle %module_name%:dependencies в консоли, находясь в корневой папке проекта. В ответ вы получите список деревьев зависимостей для каждой из доступных конфигураций.
Используя параметр configuration, указываем имя конфигурации, чтобы видеть дерево зависимостей только указанной конфигурации.
Возьмем специально подготовленные исходники репозитория, расположенного на github и попробуем получить дерево зависимостей для конкретной конфигурации (в данный момент проект находится в состоянии 0, т.е. в качестве build.gradle используется build.gradle.0):
Проанализировав дерево зависимостей, можно увидеть, что модуль app использует в качестве зависимостей две внешних зависимости (appcompat и guava), а также две проектных зависимости (first и second), которые в свою очередь используют библиотеку jsontoken версий 1.0 и 1.1 как внешнюю зависимость. Совершенно очевидно, что проект не может содержать две версии одной библиотеки в Classpath, да и нет в этом необходимости. На этом этапе Gradle включает модуль разрешения конфликтов.
Разрешение конфликтов
Gradle DSL содержит компонент, используемый для разрешения конфликтов зависимостей. Если посмотреть на зависимости библиотеки jsontoken на приведённом выше дереве зависимостей, то мы увидим их только раз. Для модуля second зависимости библиотеки jsontoken не указаны, а вывод самой зависимости содержит дополнительно ‘–> 1.1’, что говорит о том, что версия библиотеки 1.0 не используется, а автоматически была заменена на версию 1.1 с помощью Gradle-модуля разрешения конфликтов.
Для объяснения каким образом была разрешена конфликтная ситуация, также можно воспользоваться Gradle-таском dependencyInsight, например:
Стоит обратить внимание, что версия 1.1 выбирается в результате conflict resolution, также возможен выбор в результате других правил (например: selected by force или selected by rule). В статье будут приведены примеры использования правил, влияющих на стратегию разрешения зависимостей, и выполнив таск dependencyInsight вы сможете увидеть причину выбора конкретной версии библиотеки на каждом из приведённых ниже этапов. Для этого при переходе на каждый этап вы можете самостоятельно выполнить таск dependencyInsight.
При необходимости есть возможность переопределить логику работы Gradle-модуля разрешения конфликтов, например, указав Gradle падать при выявлении конфликтов во время конфигурирования проекта. (состояние 1)
// build.gradle // … configurations.compile.resolutionStrategy
После чего даже при попытке построить дерево зависимостей Gradle таски будут прерываться по причине наличия конфликта в зависимостях приложения.
У задачи есть четыре варианта решения:
Первый вариант – удалить строки, переопределяющие стратегию разрешения конфликтов.
Второй вариант – добавить в стратегию разрешения конфликтов правило обязательного использования библиотеки jsonToken, с указанием конкретной версии (состояние 2):
// build.gradle // … configurations.compile.resolutionStrategy
При применении этого варианта решения дерево зависимостей будет выглядеть следующим образом:
Третий вариант — добавить библиотеку jsonToken явно в качестве зависимости для проекта app и присвоить зависимости параметр force, который явно укажет, какую из версий библиотеки стоит использовать. (состояние 3)
// build.gradle // … dependencies < compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile project(":second") compile ('com.googlecode.jsontoken:jsontoken:1.1') < force = true >>
А дерево зависимостей станет выглядеть следующим образом:
Четвёртый вариант – исключить у одной из проектных зависимостей jsontoken из собственных зависимостей с помощью параметра exclude. (состояние 4)
// build.gradle dependencies < compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile(project(":second")) < exclude group: "com.googlecode.jsontoken", module: 'jsontoken' >>
И дерево зависимостей станет выглядеть следующим образом:
Стоит отметить, что exclude не обязательно передавать оба параметра одновременно, можно использовать только один.
Но несмотря на правильный вывод дерева зависимостей, при попытке собрать приложение Gradle вернёт ошибку:
Причину ошибки можно понять из вывода сообщений выполнения задачи сборки — класс GwtCompatible с идентичным именем пакета содержится в нескольких зависимостях. И это действительно так, дело в том, что проект app в качестве зависимости использует библиотеку guava, а библиотека jsontoken использует в зависимостях устаревшую Google Collections. Google Collections входит в Guava, и их совместное использование в одном проекте невозможно.
Добиться успешной сборки проекта можно тремя вариантами:
Первый — удалить guava из зависимостей модуля app. Если используется только та часть Guava, которая содержится в Google Collections, то предложенное решение будет неплохим.
Второй — исключить Google Collections из модуля first. Добиться этого мы можем используя описанное ранее исключение или правила конфигураций. Рассмотрим оба варианта, сначала используя исключения (состояние 5)
// build.gradle dependencies < compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile(project(":first")) < exclude module: 'google-collections' >compile(project(":second")) < exclude group: "com.googlecode.jsontoken", module: 'jsontoken' >>
Пример использования правил конфигураций (состояние 6):
//build.gradle configurations.all < exclude group: 'com.google.collections', module: 'google-collections' >dependencies < compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile(project(":second")) < exclude group: "com.googlecode.jsontoken", module: 'jsontoken' >>
Дерево зависимостей для обеих реализаций исключения Google Collections будет идентично.
Третий вариант — использовать функционал подмены модулей (состояние 7):
// build.gradle dependencies < modules < module('com.google.collections:google-collections') < replacedBy('com.google.guava:guava') >> compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile(project(":second")) < exclude group: "com.googlecode.jsontoken", module: 'jsontoken' >>
Дерево зависимостей будет выглядеть следующим образом:
Нужно учесть, что если оставить предопределенную логику разрешения конфликтов, которая указывает прерывать сборку при наличии любого конфликта, то выполнение любого таска будет прерываться на этапе конфигурации. Другими словами, использование правил замены модулей является одним из правил стратегии разрешения конфликтов между зависимостями.
Также важно заметить, что последний из озвученных вариантов является самым гибким, ведь при удалении guava из списка зависимостей Gradle, Google Collections сохранится в проекте, и функционал, от него зависящий, сможет продолжить выполнение. А дерево зависимостей будет выглядеть следующим образом:
После каждого из вариантов мы достигнем успеха в виде собранного и запущенного приложения.
Но давайте рассмотрим другую ситуацию (состояние 8), у нас одна единственная сильно урезанная (для уменьшения размеров скриншотов) динамическая зависимость wiremock. Мы её используем сугубо в целях обучения, представьте вместо неё библиотеку, которую поставляет ваш коллега, он может выпустить новую версию в любой момент, и вам непременно необходимо использовать самую последнюю версию:
// build.gradle configurations.all < exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.json', module: 'json' exclude group: 'org.eclipse.jetty' exclude group: 'com.fasterxml.jackson.core' exclude group: 'com.jayway.jsonpath' >dependencies
Дерево зависимостей выглядит следующим образом:
Как вы можете увидеть, Gradle загружает последнюю доступную версию wiremock, которая является beta. Ситуация нормальная для debug сборок, но если мы собираемся предоставить сборку пользователям, то нам определённо необходимо использовать release-версию, чтобы быть уверенными в качестве приложения. Но при этом в связи с постоянной необходимостью использовать последнюю версию и частыми релизами нет возможности отказаться от динамического указания версии wiremock. Решением этой задачи будет написание собственных правил стратегии выбора версий зависимости:
// build.gradle //… configurations.all < //… resolutionStrategy < componentSelection < all < selection ->if (selection.candidate.version.contains('alpha') || selection.candidate.version.contains('beta')) < selection.reject("rejecting non-final") >> > > >
Стоит отменить, что данное правило применится ко всем зависимостям, а не только к wiremock.
После чего, запустив задачу отображения дерева зависимостей в информационном режиме, мы увидим, как отбрасываются beta-версии библиотеки, и причину, по которой они были отброшены. В конечном итоге будет выбрана стабильная версия 1.58:
Но при тестировании было обнаружено, что в версии 1.58 присутствует критичный баг, и сборка не может быть выпущена в таком состоянии. Решить эту задачу можно, написав ещё одно правило выбора версии зависимости:
// build.gradle //… configurations.all < //… resolutionStrategy < componentSelection < // … withModule('com.github.tomakehurst:wiremock') < selection ->if (selection.candidate.version == "1.58") < selection.reject("known bad version") >> > > >
После чего версия wiremock 1.58 будет также отброшена, и начнёт использоваться версия 1.57, а дерево зависимостей будет выглядеть следующим образом:
Заключение
Несмотря на то, что статья получилась достаточно объемной, тема Dependency Management в Gradle содержит много не озвученной в рамках этой статьи информации. Глубже погрузиться в этот мир лучше всего получится с помощью официального User Guide в паре с документацией по Gradle DSL, в изучение которых придется инвестировать немало времени.
Зато в результате вы получите возможность сэкономить десятки часов, как благодаря автоматизации, так и благодаря пониманию того, что необходимо делать при проявлении различных багов. Например, в последнее время достаточно активно проявляются баги с 65К-методов и Multidex, но благодаря грамотному просмотру зависимостей и использованию exclude проблемы решаются очень быстро.