Что такое многомодульный проект
Многомодульный проект с Maven
1. Обзор
В этом руководстве мы покажем, как создать многомодульный проект с Maven.
Сначала мы обсудим, что такое многомодульный проект, и рассмотрим преимущества использования этого подхода. Затем мы настроим наш образец проекта. Чтобы получить хорошее представление о Maven, ознакомьтесь с этим руководством.
2. Многомодульный проект Maven
3. Преимущества использования мультимодулей
Существенным преимуществом использования этого подхода является то, что мы можем уменьшить дублирование.
Допустим, у нас есть приложение, которое состоит из нескольких модулей, пусть это будет интерфейсный модуль и серверный модуль. Теперь мы работаем над обоими и меняем функциональность, которая влияет на них обоих. В этом случае без специального инструмента сборки нам придется собрать оба компонента по отдельности или написать скрипт, который скомпилирует код, запустит тесты и покажет результаты. Затем, когда мы получим еще больше модулей в проекте, им станет труднее управлять и поддерживать.
Кроме того, в реальном мире проектам могут потребоваться определенные плагины Maven для выполнения различных операций во время жизненного цикла сборки, совместного использования зависимостей и профилей или включения других проектов спецификации.
4. Родительский POM
Maven поддерживает наследование таким образом, что каждый файл pom.xml имеет неявный родительский POM, он называется Super POM и может находиться в двоичных файлах Maven. Эти два файла объединены Maven и образуют эффективный POM.
Помимо наследования, Maven предоставляет понятие агрегирования. Родительский POM, использующий эту функцию, называется агрегированным POM . По сути, этот тип POM явно объявляет свои модули в файле pom.xml.
5. Подмодули
6. Сборка приложения
Теперь, когда мы понимаем подмодули и иерархию Maven, давайте создадим пример приложения, чтобы продемонстрировать их. Мы будем использовать интерфейс командной строки Maven для создания наших проектов.
Это приложение будет состоять из трех модулей, которые будут представлять:
Поскольку мы сосредоточимся на Maven, реализация этих сервисов останется неопределенной.
6.1. Создание родительского POM
Сначала создадим родительский проект :
Как только родительский элемент создан, мы должны открыть файл pom.xml, расположенный в родительском каталоге, и изменить упаковку на pom.
Теперь, когда наш агрегатор готов, мы можем сгенерировать наши подмодули.
6.2. Создание подмодулей
Теперь наш родительский объект явно объявляет агрегированные модули.
Затем при запуске команды mvn package в родительском каталоге проекта Maven соберет и протестирует все три модуля.
В конце концов, если мы хотим поделиться всей конфигурацией с нашими подмодулями, в их файлах pom.xml нам нужно будет объявить родительский элемент :
Отметим, что у подмодулей может быть только один родитель. Однако мы можем импортировать множество спецификаций. Более подробную информацию о файлах BOM можно найти в этой статье.
6.3. Создание проекта
Теперь мы можем собрать сразу все три модуля. В каталоге родительского проекта запустите:
Это построит все модули, мы должны увидеть следующий вывод команды:
The Reactor lists the parent-project, but as it’s pom type it’s excluded and the build results in three separate .jar files for all other modules. In that case, build occurs in three of them.
7. Conclusion
In this tutorial, we discussed the benefits of using Maven multi-modules. Also, we distinguished between regular Maven’s parent POM and aggregate POM. In the end, we showed how to set up a simple multi-module to start to play with.
Maven is a great tool but it is complex on its own. If you’d like to find more details about Maven, have a look at the Sonatype Maven reference or Apache Maven guides. If you seek advanced usages of Maven multi-modules set up, have a look how Spring Boot project leverages the usage of it.
Все примеры кода на Baeldung созданы с использованием Maven, поэтому вы можете легко проверить наш веб-сайт проекта GitHub, чтобы увидеть различные конфигурации Maven.
Многомодульный maven проект
Maven позволяет собирать проект из нескольких модулей. Каждый программный модуль включает свой проектный файл pom.xml. Один из проектных pom.xml файлов является корневым. Корневой pom.xml позволяет объединить все модули в единый проект. При этом в корневой проектный файл можно вынести общие для всех модулей свойства. А каждый модульный pom.xml должен включать параметры GAV (groupId, artifactId, version) корневого pom.xml.
Общие положения разработки многомодульного maven-приложения рассмотрены на странице Наследование проектов в maven. В данной статье рассмотрим пример сборки многомодульного приложения. В качестве «подопытного» приложения используем пример, представленный на странице Pluggable решение. На выходе данного примера мы должны получить 3 архивных и один исполняемый jar-файлов. Главный исполняемый jar-модуль динамически «при необходимости» загружает остальные архивные jar’ники. Данный «подопытный» пример был использован для «оборачивания» jar’ника в exe-файл с использованием maven-плагина launch4j.
Описание многомодульного проекта
На скриншоте представлена структура проекта pluggable, включающая следующие проектные модули :
• hello-plugin1 | – динамически загружаемый плагин №1 (hello1.jar); |
• hello-plugin2 | – динамически загружаемый плагин №2 (hello2.jar); |
• plugin-api | – интерфейсы описания плагинов (plugin-api.jar); |
• plugin-loader | – главный исполняемый jar модуль. |
Дополнительные поддиректории проекта, используемые для размещения jar-модулей :
• commons | – поддиректория размещения архивного jar-модуля описания интерфейса плагинов; |
• plugins | – поддиректория размещения jar-модулей (плагинов); |
Главный исполняемый модуль plugin-loader.jar размещается в корневой директории проекта, где размещается и проектный/корневой pom.xml. Файл run.bat можно использовать для старта plugin-loader.jar из консоли в Windows.
Примечание : в исходные коды классов внесены изменения, связанные с из размещением в пакетах. В исходном примере все классы располагаются в «корне».
Начнем рассмотрение примера с корневого многомодульного pom.xml.
Листинг многомодульного корневого pom.xml
Корневой pom.xml включает параметры GAV (groupId, artifactId, Version), общую для всех модулей проекта секцию
Следует отметить, что порядок включения программных модулей проекта составлен таким образом, что сначала представлены исполняемый модуль plugin-loader.jar и плагины (hello-plugin1.jar, hello-plugin2.jar), после чего следует интерфейсный модуль plugin-api.jar. Если собирать проект по-отдельности, то модуль plugin-api.jar должен быть собран в первую очередь и размещен в репозитории командой «mvn install». В этом случае зависимые модули plugin-loader.jar и плагины (hello-plugin1, hello-plugin2) собрались бы нормально. Ну, а мы в этом примере посмотрим, как поступит Maven в случае, если порядок описания модулей для сборки «нарушен».
Модуль описания интерфейсов плагинов plugin-api.jar
На следующем скриншоте представлена структура проекта plugin-api. Интерфейсные классы Plugin, PluginContext располагаются в пакете «ru.plugin».
Листинг pom.xml
Проектный pom.xml модуля plugin-api.jar включает GAV-параметры, секцию описания родительского GAV (
Модуль описания плагина hello-plugin1.jar
Структура проекта hello-plugin1 представлена на следующем скриншоте.
Класс HelloPlugin, расположенный в пакете «ru.plugins», реализует свойства интерфейса Plugin. При инициализации класса в методе init определяется значение контекста PluginContext родительского/вызвавшего объекта. Метод invoke выводит в консоль сообщение и изменяет надпись на кнопке родительского объекта.
Листинг pom.xml
Проектный pom.xml модуля hello-plugin1.jar включает GAV-параметры, секцию описания родительского GAV (
Примечание : второй плагин hello-plugin2 структурно ничем не отличается от hello-plugin1. Отличия касаются текста сообщения в консоли, надписи на кнопке и параметров GAV в pom.xml.
Проектный pom.xml модуля plugin-loader
Проектный pom.xml включает GAV-параметры jar-модуля, секцию описания родительского GAV (
Многомодульные проекты maven
Со временем все программные проекты разрастаются. То, что начиналось как довольно жирный Hello World, весьма скоро обзаводится отдельным фронтендом, парочкой batch процессов, тремя видами RPC и общим кодом доступа к данным. И вот, в какой-то момент времени, возникает желание распилить этого монстра на неколько раздельных maven проектов, которые будут существовать независимо друг от друга.
Однако на пути к светлому многоартефактному будущему имеются некоторые препятствия — артефакты имеют зависимости друг от друга, требуют использования одной и той же версии какой-то библиотеки, должны собираться все вместе и так далее. К счастью, в maven есть механизм для автоматического решения этих проблем — многомодульные проекты.
Многомодульный проект проще всего представить себе как дерево — у него есть общий корень, который ничего не делает, а лишь описывает общие параметры, и листья, которые наследуют эти общие параметры. Листья могут иметь свои листья и так далее, пока память не кончится 🙂
Родительский модуль
Родительский модуль состоит из одного лишь pom файла, в котором описаны его дочерние модули:
Многомодульность и Dagger 2. Лекция Яндекса
Когда ваше приложение построено на многомодульной архитектуре, приходится посвящать много времени тому, чтобы все связи между модулями были корректно прописаны в коде. Половину этой работы можно поручить фреймворку Dagger 2. Руководитель группы Яндекс.Карт для Android Владимир Тагаков Noxa рассказал о плюсах и минусах многомодульности и удобной организации DI внутри модулей при помощи Dagger 2.
— Меня зовут Владимир, я разрабатываю Яндекс.Карты и сегодня буду рассказывать вам про модульность и второй Dagger.
Самую длинную часть я понимал, когда сам ее изучал, быстрее всего. Вторую часть, над которой я сидел несколько недель, я расскажу очень быстро и сжато.
Зачем мы в Картах начали непростой процесс разделения на модули? Мы хотели просто повысить скорость сборки, все про это знают.
Второй пункт цели — уменьшить зацепление кода. Зацепление я взял из Википедии. Это значит, что мы хотели уменьшить взаимосвязи между модулями, чтобы модули были обособленными и могли использоваться вне приложения. Изначальная постановка задачи: другие проекты Яндекса должны иметь возможность использовать часть функциональности Карт ровно так, как у нас. И чтобы разработкой этой функциональности занимались мы в рамках развития проекта.
Хочу бросить горящий тапок в сторону [k]apt, который замедляет скорость сборки. Я его не сильно ненавижу, а сильно люблю. Он позволяет мне пользоваться Dagger.
Главный недостаток процесса разделения на модули — это, как ни парадоксально, замедление скорости сборки. Особенно в самом начале, когда вы выносите первые два модуля, Common и какую-то вашу фичу, общая скорость сборки проекта падает, как бы вы ни старались. В конце, когда в вашем главном модуле будет оставаться все меньше кода, скорость сборки будет расти. И все равно это не значит, что все очень плохо, есть способы это обойти и даже из первого модуля поиметь профит.
Второй недостаток — сложно разделять код на модули. Кто пытался, знает, что ты начинаешь тянуть какие-то зависимости, какие-то классики, и все кончается тем, что ты весь свой главный модуль скопировал в другой модуль и начинаешь заново. Поэтому нужно четко понимать момент, когда нужно остановиться и разбить связь, используя какую-то абстракцию. Недостаток — больше абстракций. Больше абстракций — сложнее проект — больше абстракций.
Сложно добавлять новые Gradle-модули. Почему? Например, приходит разработчик, берет в разработку новую фичу, сразу делает хорошо, делает отдельный модуль. В чем проблема? Он должен помнить обо всем доступном коде, который есть в главном модуле, чтобы, если что, его переиспользовать и вынести в Common. Потому что процесс выноса какого-то модуля в Common — постоянный, пока ваш главный модуль App не превратится в тонкую прослойку.
Модули, модули, модули… Модули Gradle, модули Dagger, модули-интерфейсы — ужас.
Доклад будет состоять из трех частей: маленькой, большой и сложной. Вначале про разницу между Implementation и API в AGP. Android Gradle Plugin 3.0 появился относительно недавно. Как все было до него?
Вот типичный проект здорового разработчика, состоящий из трех модулей: модуль App, который является главным, собирается и устанавливается в приложение, и два Feature-модуля.
Сразу поговорим про стрелки. Это большая боль, каждый рисует в ту сторону, в которую ему удобно рисовать. У меня они значат, что от Core идет стрелка к Feature. Значит, Feature знает о Core, может пользоваться классами из Core. Как видите, между Core и App стрелки нет, значит App вроде как не должен использовать Core. Core — не Common-модуль, он есть, от него все зависят, он отдельный, в нем мало кода. Пока его не будем рассматривать.
Наш Core-модуль изменился, нам нужно как-то его переделать. Мы изменяем в нем код. Желтый цвет — изменение кода.
После пересобираем проект. Понятно, что после изменения какого-то модуля его придется пересобрать, перекомпилировать. Окей.
После собирается и Feature модуль, который от него зависит. Тоже понятно, его зависимость пересобралась, и нужно себя обновить. Кто знает, что там изменилось.
И тут происходит самое неприятное. Собирается модуль App, хотя непонятно почему. Я точно знаю, что Core я никак не использую, и почему App пересобирается — неясно. А он очень большой, потому что в самом начале пути, и это очень большая боль.
К тому же, если от Core зависит несколько фич, много модулей, то пересобирается весь мир, это очень долго.
Давайте перейдем на новую версию AGP и заменим, как говорит инструкция, все compile на API, а не на Implementation, как вы подумали. Ничего не меняется. Схемы тождественные. Что за новый способ указывания зависимостей Implementation? Представим эту же схему, используя только это ключевое слово, без API? Оно будет выглядеть так.
Здесь в implementation явно видно, что есть связь между Core и App. Тут мы можем явно понять, что она нам нафиг не нужна, мы хотим от нее избавиться, поэтому просто убираем это. Становится все вроде попроще.
Теперь почти что все хорошо, даже более чем. Если мы изменяем в Core какое-то API, добавляем новый класс, новый публичный или package private метод, то пересобирается Core и Feature. Если меняете внутри метода реализацию или добавляете приватный метод, то теоретически пересборки Feature вообще ничего не должно происходить, потому что ничего же не изменилось.
Поехали дальше. Так получилось, что от нашего Core зависит много кто. Наверное, Core — это какой-то Network или обработка пользовательских данных. Потому что это Network, все меняется довольно часто, все пересобирается, и мы получили ту же боль, от которой старательно убегали.
Давайте рассмотрим два способа, как можно с этим бороться.
Мы можем из нашего Core модуля вынести в отдельный модуль только интерфейсы API, его API, чем мы пользуемся. И в отдельный модуль можем вынести реализацию этих интерфейсов.
Вы можете посмотреть на связи на экране. Core Impl не будет доступен для фичеров. То есть не будет никакой связи между фичерами и реализацией Core. А модуль, подсвеченный желтым, будет только предоставлять фабрики, которые будут предоставлять какие-то никому не известные реализации ваших интерфейсов.
После такого преобразования, хочу обратить внимание, что Core API, из-за того что ключевое слово API стоит, будет доступен всем фичерам транзитивно.
После этих преобразований изменяем что-то в реализации, что вы делаете чаще всего, и будет пересобираться только модуль с фабриками, он очень легенький, маленький, можно даже не рассматривать, сколько времени это занимает.
Другой вариант работает не всегда. Например, если это какой-то Network, то я сложно себе представляю, как это может произойти, но если это какой-то экран логина пользователя, то вполне может быть.
Можем сделать Sample, такой же полноправный корневой модуль как App, и собирать в нем только одну фичу, это будет очень быстро, и это можно быстро итеративно разрабатывать. В конце презентации покажу, сколько времени занимает обычная сборка и сборка сэмпла.
С первой частью закончили. Какие модули бывают?
Модули бывают трех типов. Common, понятно, должен быть как можно более легким, и в нем должны находиться не какие-то фичи, а только функциональность, который используется всеми. Для нас в нашей команде это особенно важно. Если мы будем предоставлять наши Feature модули другим приложениям, то мы будем их заставлять тащить Common в любом случае. Если он будет очень жирный, то нас никто не будет любить.
Если у вас проект поменьше, то с Common можно чувствовать себя посвободнее, то тоже не надо сильно усердствовать.
Следующий тип модулей — Standalone. Самый обычный и интуитивно понятный модуль, который содержит в себе конкретную фичу: какой-то экран, какой-то юзерский сценарий и так далее. Он должен быть по возможности независимый, и для него чаще всего можно сделать sample App и разрабатывать его в нем. Sample App очень сильно важны в начале процесса разбиения, потому что все билдится по-прежнему медленно, и вы хотите получить как можно быстрее профит. В конце, когда все будет побито на модули, вы можете пересобирать все, это будет быстро. Потому что не будет пересобираться лишний раз.
Celebrity-модули. Это я сам придумал слово. Смысл в том, что он очень известен всем, и от него много кто зависит. Тот же Network. Я уже рассказал, если вы часто его пересобираете, как можно избежать того, что у вас все пересобирается. Есть еще один способ, который можно применить для небольших проектов, для которых не стоит цели отдавать все наружу как отдельную зависимость, отдельный артефакт.
Как это выглядит? Повторим, что из Celebrity выносите API, выносите его реализацию, и теперь следите за руками, обратите внимание на стрелочки от Feature к Celebrity. Происходит это. API вашего модуля попал в Common, реализация осталась сама в нем, а фабрика, которая предоставляет реализацию этого API, появилась в вашем главном модуле. Если кто-то смотрел Mobius, то про это рассказывал Денис Неклюдов. Очень похожая схема.
Мы в проекте используем Dagger, нам он нравится, и мы хотели как можно больше от этого пользы получить в контексте разных модулей.
Мы хотели, чтобы в каждом модуле был независимый граф зависимостей, чтобы был конкретный корневой компонент, от которого можно что угодно делать, мы хотели, чтобы был свой сгенерированный код для каждого Gradle-модуля. Мы не хотели, чтобы сгенерированный код пролезал в главный. Мы хотели как можно больше compile-time валидации. Мы же страдаем от [k]apt, хоть какой-то профит должны получить от того, что дает Dagger. И при всем этом мы не хотели заставлять никого использовать Dagger. Ни того, кто реализует свежий фиче-модуль отдельный, ни того, кто его потом потребляет, наши коллеги, которые просят какие-то фичи себе.
Как организовать отдельный граф зависимостей внутри нашего фиче-модуля?
Можно попытаться использовать Subcomponent, и это даже будет работать. Но у этого довольно много недостатков. Можно увидеть, что в Subcomponent непонятно, какие именно зависимости он использует из Component. Чтобы это понять, вам придется долго и мучительно пересобирать проект, смотреть, на что ругается Dagger и его добавлять.
К тому же сабкомпоненты устроены так, что заставляют других использовать Dagger, и не получится все это легко запустить у ваших клиентов и вас самих, если вы решите вдруг в каком-то модуле отказаться.
Одна из самых отвратительных вещей, что при использовании Subcomponent, все зависимости вытягиваются в главный модуль. Dagger устроен так, что сабкомпоненты генерятся вложенным классом их обрамляющих компонентов, родительских. Может, кто-то смотрел генеренный код и его размер, на свои генеренные компоненты? У нас 20 тыс. строчек в нем. Так как сабкомпоненты всегда вложенные классы для компонентов, то получается, что сабкомпоненты сабкомпонентов тоже вложенные, и весь генеренный код попадает в главный модуль, этот двадцатитысячестрочный файл, который нужно компилить, и нужно его рефакторить, Студия начинает тормозить — боль.
Но есть решение. Можно использовать просто Component.
В Dagger можно компоненту указывать зависимости. Это показано в коде, и показано на картинке. Зависимости, где вы указываете Provision методы, фабричные методы, которые показывают, от каких именно сущностей зависит ваш компонент. Он хочет их получить в момент создания.
Раньше я всегда думал, что в эти зависимости можно указывать только другие компоненты, и вот почему — в документации написано так.
Теперь я понимаю, что значит использовать component interface, но раньше я думал, что это просто компонент. На самом деле нужно использовать интерфейс, который составлен по правилам создания интерфейса для компонента. Короче говоря, просто Provision-методы, когда у вас просто есть геттеры для каких-то зависимостей. Также пример кода есть в документации Dagger.
Там тоже написано otherComponent, и это сбивает с толку, потому что на самом деле туда можно не только компоненты засовывать.
Как бы нам хотелось это дело использовать в реальности?
В реальности есть Feature-модуль, у него есть пакет API, который виден, находится близко к корню всех пакетов, и там указано, что есть точка входа — FeatureActivity. Необязательно использовать typealias, просто чтобы было понятно. Это может быть фрагмент, может быть ViewController — неважно. И есть его зависимости, FeatureDeps, где указано, что ему нужен контекст, какой-то Network-сервис, из Common какая-то штука, которую хочется получать из App, и любой клиент обязан это удовлетворить. Когда он это сделает, все заработает.
Как мы всем этим пользуемся в Feature-модуле? Здесь я использую Activity, это необязательно. Мы как обычно создаем свой корневой Dagger-компонент и используем волшебный метод findComponentDependencies, он сильно похож на Dagger for Android, но мы его не можем использовать в первую очередь потому, что не хотим тащить сабкомпоненты. В остальном всю логику мы можем у них перенять.
Я сначала пытался рассказать, как он работает, но вы это сможете увидеть в семпл-проекте в пятницу. Как это нужно использовать клиентам вашей библиотеки в вашем главном модуле?
В первую очередь это просто typealias. На самом деле он имеет другое название, но для краткости так. MapOfDepth по классу интерфейса Dependency отдает вам его реализацию. В App говорим, что мы умеем проводить зависимости так же, как в Dagger for Android, и очень важно, что компонент наследует этот интерфейс, автоматически получает Provision-методы. Dagger с этого момента начинает нас заставлять предоставить эту зависимость. Пока вы ее не предоставите, он не будет компилиться. В этом главное удобство: вы решили устроить фичу, расширили свой компонент этим интерфейсом — все, пока вы не сделаете все остальное, он не будет просто компилиться, а будет выдавать понятные сообщения об ошибках. Модуль простой, смысл в том, что он биндит ваш компонент к реализации интерфейса. Примерно так же, как в Dagger for Android.
Перейдем к результатам.
Я проверял на нашем мейнфреймере и на моем локальном ноутбуке, перед этим выключив все что можно. Если мы добавляем публичный метод в Feature, то время билда значительно отличается. Здесь я показываю различия в случае, когда я билжу семпл-проект. Это 16 секунд. Или когда собираю все карты — это значит две минуты сидеть и ждать при каждом, даже минимальном изменении. Поэтому многие фичи мы разрабатываем и будем разрабатывать в семпл-проектах. На мейнфреймере время сопоставимое.
Еще один важный результат. До выделения модуля Feature это выглядело так: на мейнфреймере было 28 секунд, теперь стало 49 секунд. Мы выделили первый модуль, и уже получили замедление сборки чуть ли не в два раза.
И еще один вариант — простая инкрементальная сборка нашего модуля, не фичи, как в предыдущем. 28 секунд было до выделения модуля. Когда выделили код, который не нужно каждый раз пересобирать, и [k]apt, который не нужно каждый раз проводить, — выиграли три секунды. Не бог весть что, но надеюсь, с каждым новым модулем время будет только уменьшаться.