Введение
Когда проект разрастается, обзаводится множеством модулей и зависимостей, настройка сборки проекта вручную может занять непомерно много времени. К тому же настройкой, а значит и тратой времени, занимаются все технические участники проекта от разработчиков, тестировщиков, до администраторов.
К тому же надо постоянно обновлять разработческие и тестовые системы, да еще и ничего не перепутать. Тут не обойтись без практики непрерывной интеграции.
В этой статье приведен опыт внедрения сборок с помощью maven, поэтому базовые знания по maven необходимы. Для быстрого старта будет достаточно изучения сайта http://www.apache-maven.ru, для более детального освоения темы — стоит обратиться к первоисточнику http://maven.apache.org
Цель статьи не научить пользоваться майвеном, а показать опыт внедрения с положительными и отрицательными решениями. Обладая этими знаниями, гораздо проще и быстрее внедрить сборки maven у себя в команде.
Почему не ant?
Предупреждая подобного рода вопросы, скажу, что мы используем и ant, но не для самой сборки, а для технических функций, об этом ниже.
Еще важным преимуществом maven является возможность собирать только тот код, с которым ты работаешь, все остальное собирается по ночам на отдельном сервере.
Окружение
Сначала стоит рассказать о структуре наших проектов и подходах к разработке. Это очень важно, в силу того, что проект развивается уже много лет и имеет структуру, не соответствующую maven-webapp-artifact
И так, у нас имеется множество модулей, хранящихся в одном SVN репозитарии. Каждый модуль имеет основную ветку разработки (trunk), а также метки (tags) и ветки (branches) по необходимости. Большинство модулей содержат как java-классы, так и jsp-страницы, скрипты, графику и пр. Большинство модулей являются самостоятельными веб-приложениями и могут работать независимо. Примеры модулей: core, soap, workflow, addons, common… Их я насчитал порядка 100.
Проект чаще всего состоит из отдельного модуля, в котором размещаются специфические ресурсы и логика в java-классах. Иногда, такого модуля нет. Итоговая сборка проекта состоит из набора модулей в виде одного web-приложения.
Необходимые модулю библиотеки хранятся в папке lib прямо в репозитарии, для веб-ресурсов имеется своя папка sx-war. А также стандартные папки src и test.

Естественно, почти все модули зависят от core, и друг от друга, по необходимости.
Еще есть база данных с мета-информацией. На основе нее взаимодействуют модули, строится вся работа с пользователем. Например, блок публикации — тоже являетс�� единицей мета-информации и хранится в БД. То есть помимо самих данных, в базе хранятся настройки работы с этими данными. Они настраиваются в консоли, которая тоже построена на блоках публикации. Есть возможность переносить мета-информацию с помощью пакетов обновлений.
В идеале, каждый модуль имеет свой общий пакет с мета-информацией. БД для core самая легкая БД и может расширяться любым модулем. Ядро — почти application-server, только без возможности динамической загрузки\выгрузки многофункциональных модулей. Не все модули имеют отделенную мету, но мы к этому стремимся :)
До нашей эры
Ходит у нас такая фраза. Означает, что «это» было очень давно, еще до начала нашей работы в компании. Расскажу, как проходила настройка сборки до меня. Мне тоже пришлось потратить немало времени на настройку в первый раз.
И вот вы устроились к нам на работу. Идет второй день. Первый ушел на просмотр скринкастов по подсистемам и тренировки работы с мета-информацией. Надо настроить сборку. Хорошо, если в базе знаний по проекту есть информация по необходимым модулям для проекта. Часто эта информация не актуальна (забывают обновлять или обновляют от случая к случаю). Информации о зависимостях самих модулей не найти, а та что есть, тоже устаревает.
Из SVN извлекаются trunk-версии всех необходимых модулей, создается проект в IDE. А дальше самое сложное: настроить импорт всех необходимых библиотек и связи модулей. Все еще осложняется, когда надо исключать одни версии библиотек из одних модулей и использовать другие. Настроить сборку всех модулей в одну папку. Далее настроить сервер веб-приложений (используется tomcat).
И вот проект настроен, затем надо настроить второй, третий, версию первого (использовать branches для работы над ошибками). Разработчику становится не по себе.
Поменялись зависимости, сломались сборки у всех. А администраторы используют ant для создания production-сборок — у них тоже все ломается. Часто возникают проблемы из-за разных сборок у администратора и разработчика.
Первая попытка
У нас не было специальных людей, отвечающих за сборку, как не было и хороших знаний maven. Поэтому первая попытка внедрения maven на проектах провалилась.
Родительский pom.xml был помещен в отдельную папку репозитария. После чего в отдельной проектной папке настраивались svn-externals и включались все необходимые проекту модули, плюс родительский pom.xml в отдельной папке:

Классический подход с использованием иерархии модулей в единой структуре SVN нам не подошел, потому что каждый модуль используется в десятках проектов. А не только в одном единственном. А любые манипуляции с parent pom на проектах нежелательны. К тому же фиксация версии любого модуля автоматически отразится на всех проектах.
Все необходимые библиотеки не искались на http://search.maven.org, потому что их очень много и все они обезличены в репозитарии (в папке lib хранятся библиотеки без версий. Например axis.jar, jtds.jar, velocity.jar). Поступили просто: все библиотеки с помощью скрипта загрузили в Nexus. GroupId и ArtifactId были заданы по маске sx.<имя библиотеки> (sx.axis).
Вот как выглядел типичный pom.xml модуля:
Базовый pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- Родительский проект --> <parent> <groupId>sx.config</groupId> <artifactId>project-settings</artifactId> <version>2.0.1</version> <relativePath>project-settings/pom.xml</relativePath> </parent> <!-- Координаты --> <groupId>sx.builds</groupId> <artifactId>wfBuilder</artifactId> <version>2.0.1</version> <packaging>pom</packaging> <!-- Репозиторий плагинов --> <pluginRepositories> <pluginRepository> <id>sx-repository</id> <url>http://v5-neptun/nexus/content/repositories/sx-repository</url> </pluginRepository> </pluginRepositories> <!-- Репозиторий Зависимостей --> <repositories> <repository> <id>sx-repository</id> <url>http://v5-neptun/nexus/content/repositories/sx-repository</url> </repository> </repositories> <!--URL SVN--> <scm> <connection>scm:svn:svn://svn-server/buildsMaven/trunk/workflowBuild</connection> </scm> <!--Модули--> <modules> <module>core</module> <module>workflow</module> </modules> </project>
Видно, что необходимо менять в нем список модулей для каждого проекта.
Во время сборки, каждый модуль собирался в отдельный war, а потом распаковывался в общий деплой с помощью maven-overlay.
Причины провала
Полностью перечеркнул преимущества maven подход использования svn-externals. Это свойство не позволяет однозначно зафиксировать код всех модулей без правки свойств svn папки вручную. Это становится практически нереально при наличии 10-15 модулей и выпуске патча для некой версии. А так хотелось использовать maven release plugin.
Плюсы
- Все собирается «здесь и сейчас». Разработчик не зависит от периода обновления SNAPSHOT-зависимостей (сборки из основной ветки разработки);
- Разработчик может вносить изменения сразу в несколько модулей (сомнительный пункт, потому что нарушается инкапсуляция модулей);
- Виден весь набор необходимых модулей;
Минусы
- Полная сборка всех модулей — длительная процедура;
- Нельзя нормально зафиксировать версию кода;
- Все библиотеки не берутся из публичного репозитория, добавление же новой — это отдельная процедура;
- Возможен jar-hell, когда кто то решит использовать библиотеку из публичного репозитория и она уже имеется в корпоративном (в деплой попадут обе версии, а то и три!). Это происходит, потому что изменяется GroupId:ArtifactId:Version;
- Код вроде бы хранится в одной папке SVN, а вычекивать надо совсем другую (с svn-externals). Не прозрачно — для эстетов;
Вторая попытка
Вторая попытка внедрения сборок развивалась в несколько итераций.
Итерация 1
В первую очередь необходимо было получить механизм фиксации кода (выпуска версии) как отдельных модулей, так и всего продукта в целом. Для этого мы избавились от использования svn-externals. Каждый модуль получился автономным и собирался отдельно. В конечную сборку попадали все зависимости, которые есть у модуля.
Также был модифицирован родительский pom.xml в соответствии с задачами. Все модули используют его в качестве базового:
Новый базовый pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- Координаты --> <groupId>sx.config</groupId> <artifactId>main-settings</artifactId> <version>1.1-SNAPSHOT</version> <packaging>pom</packaging> <!--URL SVN--> <scm> <connection>scm:svn:svn://svn-server/maven/main-settings/trunk</connection> </scm> <!-- Репозиторий плагинов --> <pluginRepositories> <pluginRepository> <id>sx-repository</id> <url>http://v5-neptun/nexus/content/repositories/sx-repository</url> </pluginRepository> </pluginRepositories> <!-- Репозиторий Зависимостей --> <repositories> <repository> <id>sx-repository</id> <url>http://v5-neptun/nexus/content/repositories/sx-repository</url> </repository> <repository> <id>releases-repository</id> <url>http://v5-neptun/nexus/content/repositories/releases</url> </repository> <repository> <id>snapshots-repository</id> <url>http://v5-neptun/nexus/content/repositories/snapshots</url> </repository> <repository> <id>central-repository</id> <url>http://v5-neptun/nexus/content/repositories/central</url> </repository> </repositories> <profiles> <profile> <id>production</id> <!-- На ФТП публикуются только конечные сборки продуктов. Модули публиковать не нужно --> <distributionManagement> <repository> <id>ftp-repository</id> <url>ftp://ftp.sample.com/builds</url> </repository> </distributionManagement> </profile> <profile> <id>development</id> <activation> <activeByDefault>true</activeByDefault> </activation> <!-- Этот репозиторий указывается только для дочерних модулей --> <distributionManagement> <snapshotRepository> <id>snapshots-repository</id> <url>http://v5-neptun/nexus/content/repositories/snapshots</url> </snapshotRepository> <repository> <id>releases-repository</id> <url>http://v5-neptun/nexus/content/repositories/releases</url> </repository> </distributionManagement> </profile> </profiles> <!-- Настройки сборки --> <build> <defaultGoal>package</defaultGoal> <!--Исходный код --> <sourceDirectory>src</sourceDirectory> <testSourceDirectory>test</testSourceDirectory> <!--Ресурсы --> <resources> <resource> <directory>src</directory> <includes> <include>**/*.properties</include> <include>**/*.sql</include> <include>**/*.xml</include> </includes> </resource> </resources> <extensions> <!-- Enabling the use of FTP --> <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ftp</artifactId> <version>1.0-beta-6</version> </extension> </extensions> <plugins> <!--Сборка --> <!--Синтаксический сахар для идеи --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.2</version> <configuration> <!-- У нас не стандартные директории --> <warSourceDirectory>sx-war</warSourceDirectory> <attachClasses>true</attachClasses> </configuration> </plugin> <!--Задаем опции выделения памяти, на больших модулях может выпадать out of memory--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <configuration> <minmemory>256m</minmemory> <maxmemory>512m</maxmemory> </configuration> </plugin> <!-- Настройки Ресурсов --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.4.3</version> <configuration> <encoding>cp1251</encoding> </configuration> </plugin> <!-- Версия плагина для вычекивания --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-scm-plugin</artifactId> <version>1.4</version> <configuration> <username>${SvnUsername}</username> <password>${SvnPassword}</password> </configuration> </plugin> <!-- Плагин выпуска релизов модуля --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-release-plugin</artifactId> <configuration> <tagBase>svn://gelios/cms/maven/main-settings/tags</tagBase> <branchBase>svn://gelios/cms/maven/main-settings/branches</branchBase> <preparationGoals>clean install</preparationGoals> <goals>deploy</goals> <autoVersionSubmodules>true</autoVersionSubmodules> </configuration> </plugin> </plugins> </build> </project>
Итерация 2
Во все модули проекта внедрили плагин выпуска версий. Для этого в каждый модуль были добавлены блоки:
Подключение плагина выпуска релизов
<!-- Путь до модуля в SVN. Меняется при выпуске версий автоматически --> <scm> <connection>scm:svn:svn://svnserver/module/trunk</connection> </scm> <build> <plugins> <!-- Плагин выпуска релизов модуля --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-release-plugin</artifactId> <configuration> <!-- Пути в SVN для хранения версий модуля --> <tagBase>svn://svnserver/module/tags</tagBase> <branchBase>svn://svnserver/module/branches</branchBase> <preparationGoals>clean install</preparationGoals> <goals>deploy</goals> <autoVersionSubmodules>true</autoVersionSubmodules> </configuration> </plugin> ... </plugins>
Были мысли вынести эту конфигурацию в базовый pom.xml, и использовать свойство ArtifactId-Version, но не во всех проектах это значение совпадает с названием папки в репозитарии. Поэтому мысль была отложена до лучших времен. Первым версию получил как раз базовый для всех модулей pom.xml.
В базовый pom.xml добавили новый профиль: production. Он закачивает сборку на корпоративный FTP и может дальше может распространяться на проекты.
Итерация 3
После неприятного столкновения с jar-hell, решено было привести библиотеки в порядок во всех сборках. А это было не просто, потому что только в ядре оказалось больше 100 библиотек и почти все без версий. Каждую надо было найти в публичном репозитарии, “вычислить” версию. Вскрылись библиотеки “сюрпризы” с внутренними сборками и патчами, измененными версиями. Их мы бережно поместили в maven-репозиторий с сохранением координат артифакта, но с припиской _custom в Version.
В качестве репозитория у нас используется Nexus. На нем был настроен шлюз к публичным репозиториям с кешированием. Появилась единая точка входа: корпоративный maven-репозиторий. До этого их было больше трех: библиотеки, релизы, ежедневные сборки и остальные публичные репозитории. Все требовали авторизацию. Три похожих репозитория в каждом pom.xml, да еще параметры доступа в settings.xml вызывали постоянную путаницу.
Делали попытку оставить ссылку на репозиторий только в базовом pom.xml. Это вполне реально, но при первой сборке любого модуля майвен должен загрузить базовый модуль из репозитория, а его координаты неизвестны. Можно это сделать вручную, после этого все отлично работает. Отказались из-за непрозрачности этого действия.
Итерация 4
Настроили еженощные сборки всех базовых и проектных модулей в системе непрерывной интеграции Hudson. С ними не возникло проблем, потому что модули теперь полностью независимы. Нет никакой разницы, публичная это библиотека или проектный модуль. Сборки на основе maven запускаются после каждого изменения кода в SVN и успешные артифакты попадают в репозитарий Nexus.
Со временем, настроили каскадную сборку модулей в задачах, а также деплой на разработческие сборки и сборки для тестирования. Деплой выполняется с помощью достаточно простых ant-скриптов. Они извлекают из Nexus последнюю сборку модуля (dependency:get), распаковывают ее на сервере и перезапускают сервер приложений (сами скрипты можно найти в конце статьи).
Плюсы
- Можно работать с каждым модулем в отдельности;
- Фиксируя версии всех модулей-зависимостей можно зафиксировать весь код проекта;
- Непрерывная интеграция избавляет от рутинной работы и позволяет быстрее переносить функционал в продуктивную среду и к заказчику;
- Выпуск версии модуля полностью контролируем и достаточно прост;
- Появилась возможность вести разработку только с одним конкретным модулем. Ведь общий деплой нескольких модулей может влиять на конечное поведение кода;
- Благодаря Hudson, всегда видно, кто сломал сборку;
- Вообще не надо настраивать проекты, IDE все сама делает. Надо только запустить сборку (да и mvn package достаточно);
- Всегда ясно, от какого кода зависит модуль. А раньше всегда собирался trunk со своими ошибками, битыми коммитами и пр.;
Минусы
- В IDE нет общего проекта со всеми модулями, поэтому внесение изменений сразу в несколько модулей требует отдельной работы с каждым модулем. Это конечно не особо удобно, но концептуально правильно, потому что любой модуль должен быть самодостаточен;
- Нельзя выпустить все версии компонентов каскадно, при выпуске основного продукта
- …
Развиваем решение
В процессе работы, в сборки вносятся правки. Для тех, кто все еще собирает «по-старинке» заведена страница в базе знаний, которая содержит зависимости модулей. Эта информация обновляется на основе pom.xml модуля каждую ночь отдельной задачей в Hudson (не забываем о DRY).
Для ускорения сборки модулей была исключена лишняя работа во время сборки. Например, мы собираем модуль soap, который зависит от core. Код core попадает в конечную сборку soap. Далее собирается project, который зависит от core и soap. soap в отдельности не используется, поэтому из его сборки исключается core, а в project он включается. Тем самым мы ускоряем сборку. Для возможности сборки soap с включением зависимого core был добавлен профиль сontext, который запускается при необходимости.
Можно заметить, что все модули встречаются в pom.xml дважды: одна запись используется при компиляции, вторая означает, что модуль попадает в деплой. Чтобы не подменять версии модулей по всему pom.xml, все версии корпоративных модулей были вынесены в отдельные свойства и располагаются в самом начале файла. Это снижает вероятность ошибки при переходе на другую версию:
Использование свойств для хранения версий
<!-- Версии компонент и другие свойства. В самом начале, чтобы проще искать --> <properties> <sx.core>4.5-SNAPSHOT</sx.core> </properties> ... <profiles> <profile> <!--Профиль сборки контекста--> <id>context</id> <dependencies> <dependency> <groupId>sx.core</groupId> <artifactId>core</artifactId> <version>${sx.core}</version> <type>war</type> <!-- Модуль попадет в деплой через maven-overlay --> <scope>runtime</scope> </dependency> </dependencies> </profile> </profiles> <!-- Настройка зависимостей --> <dependencies> <dependency> <groupId>sx.core</groupId> <artifactId>core</artifactId> <version>${sx.core.core}</version> <classifier>classes</classifier> <!-- Модуль используется только для сборки\компиляции --> <scope>provided</scope> </dependency> ... </dependencies>
ant-cкрипты для работы с maven-артифактами
Заключение
Так была внедрена непрерывная интеграция в нашем проекте. Пропала необходимость в ручных обновлениях кода на серверах. Всегда видны результаты сборки, которые рассылаются по RSS. Большой экран мониторинга сборки еще не поставили.
Новые разработчики не тратят много времени на настройку сборок. А это только радует.
Наладился выпуск версий кода, теперь этот процесс почти полностью автоматизирован и не занимает много времени. Стало гораздо легче сопровождать ранее выпущенные версии.
Новые наработки попадают на контексты разработки в течение суток, а значит и к заказчику они попадают быстрее. Сложно уловимые ошибки интеграции тоже стали проявляться, а значит, и исправляться, быстрее.
UPDATE: Полезная статья по опыту внедрения от комментирующих: http://bazhenov.me/blog/2011/04/09/build.html
