Введение
Когда проект разрастается, обзаводится множеством модулей и зависимостей, настройка сборки проекта вручную может занять непомерно много времени. К тому же настройкой, а значит и тратой времени, занимаются все технические участники проекта от разработчиков, тестировщиков, до администраторов.
К тому же надо постоянно обновлять разработческие и тестовые системы, да еще и ничего не перепутать. Тут не обойтись без практики непрерывной интеграции.
В этой статье приведен опыт внедрения сборок с помощью 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