Наводим порядок в разработке ПО вместе с maven. Часть 7

May 8, 2009

Одним из самых важных характеристик любого программного продукта, технологии или языка программирования является его “кривая изучения”. По оси OX этого воображаемого графика вы откладываете временные затраты на изучение технологии, а по оси OY – сложность тех задач, которые вы сможете решать, потратив заданное время на обучение. Так вот, хороший продукт характеризуется плавным возрастанием “кривой изучения”. Т.е. вы можете делать простые вещи, потратив на изучение технологии совсем немного времени; но чем больше времени вы тратите, тем более сложные вещи можете делать.

Все это звучит крайне ожидаемо, однако я часто сталкиваюсь с такими областями в программировании, когда для достижения незначительных результатов требуется очень много сил и времени. А это может стать непреодолимым барьером для многих новичков, желающих изучить новый язык или технологию, но, не получив за приемлемое время результат, охладевших. Приятно, что maven подчиняется закону плавного возрастания сложности: так основы создания проекта и определения его зависимостей я рассказал за первые две статьи. Но те же две статьи я потратил на рассказ о создании многомодульных проектов или работу с архетипами. Как видите, по мере роста сложности темы, растут и затраты сил на ее изучение. В любом случае, нужно четко понимать, что “серебряной пули нет” и maven не может решить абсолютно все проблемы, связанные с управлением жизненным циклом проекта. Если вы хотите привнести больше порядка в работу своей команды или самого себя лично, вам все равно придется изучать смежные с maven технологии. Прежде всего, это continuous integration server-ы (bamboo, teamcity, hudson). Продукты этого класса предназначены для постоянной сборки проекта, автоматизированному запуску тестов и формированию отчетов для всех участников команды. В дальнейшем я планирую написать серию статей, рассказывающих о продуктах компании atlassian (http://www.atlassian.com/), однако сегодня я сосредоточусь на более простых вещах: я расскажу об интеграции maven и ant.

Хотя часто ant противопоставляют maven, но это не верно. Т.к. эти два инструмента решают совершенно различные задачи: управление проектом и автоматизация повторяющихся действий (эти действия записываются в виде специальных исполнимых файлов сценариев). Т.е. maven скорее предназначен для декларативного описания того, что мы хотим получить в ходе сборки проекта (его компиляции и упаковке). Затем maven, выполняя предопределенный набор шагов жизненного проекта, применяет к ним настроенные нами вызовы плагинов. Что касается ant, то он концентрируется на записи последовательности шагов, которые нужно выполнить для достижения цели (той же компиляции проекта и его упаковке). Формально, ant является более гибким инструментом т.к. размер строительных кубиков, из которых мы строим программу-сценарий сборки проекта, крайне мал. Эти кубики – элементарные действия, такие как: удалить файл или каталог, скопировать файл, запустить компилятор с определенными настройками, скопировать файл по ftp, заменить внутри файла все вхождения какой-то фразы на другую. Есть в ant и команды работы с cvs-репозиториями, специальные команды для java серверов (инсталляция и удаление приложения с сервера). Плюс, в ant есть поддержка интеграции с системами автоматизированного тестирования и оценки качества покрытия кода тестами и многое другое. Количество ant “кубиков” огромно, настолько огромно, что даже простое перечисление названий доступных команд может занять весь объем этой статьи и это не преувеличение. В случае же maven, мы ограничены набором предопределенных фаз жизненного цикла и вызываемых при этом плагинов (ладно, ладно набор maven-плагинов также достаточно велик, и мы можем разрабатывать свои собственные). Но в любом случае, ant более низкоуровневый и гибкий по сравнению с maven. И в этом вся беда: точнее, беда в культуре разработке и профессиональном уровне программистов. В своей практике мне несколько раз попадались проекты, которые управлялись ant-сценариями. Да так, что через несколько лет переписывания, сменяющими друг друга разработчиками, эти ant-сценарии превращались в неудобочитаемую “лапшу”. На каком-то этапе, когда затраты времени на поддержку написанного ant-сценария становились слишком большими, рубился “гордиев узел” и начиналось переписывание начисто ant-сценариев, управляющих жизненным циклом проекта. Или, что было гораздо чаще, от ant отказывались и переходили на maven.

Нужно четко понимать, что такое преимущество ant, как его гибкость, становится и недостатком. Поскольку нет четко предопределенных, ставших хоть и не де-юре, так пусть де-факто, правил написания сценария. Нет правил структурирования каталогов, нет правил размещения в папках с предопределенными именами исходных кодов приложения или файлов с графическими ресурсами и т.д. Зато все эти правила “умолчания” есть в maven. И для того, чтобы “переиграть” ту же структуру каталогов с проектом, нужно потратить много времени, а значит, вам нужно четко осознавать, что вы хотите сделать и почему. А ant никоим образом вас не останавливает и не навязывает привила игры (структурирования проекта). Здесь возможно большее количество необоснованных решений; решений в стиле “потому что так быстрее на 15 минут”, а значит больше бардака. Знаете, когда я начинал писать серию статей про maven, то, размышляя над названием серии, уже почти склонился к такому кричащему заголовку как “Разрушаем бардак в разработке ПО вместе с maven”. Потом я все же решил сделать заголовок более нейтральным, но фраза “разрушаем бардак” идеально подходит на роль лозунга для maven. Возвращаясь к ant и его недостаткам, стоит сказать, что превознесенная мною до небес, гибкость ant все-таки “слабовата” по сравнению с гибкостью любого современного языка программирования. Тут нужно уточнение: дело в том, что гибкость любого инструмента является обратно пропорциональной величине его строительных блоков (элементарных единиц из которых строится программа или сценарий). Ant оперирует развитым набором высокоуровневых команд. Представляете, из какого количества действий состоит одна ant-команда “скопировать файл по ftp”, и сколько строк кода нужно было бы записать на обычным языке программирования для повторения ant-команды? Очень много. Но, тем не менее, на ant неудобно записывать такие действия как сложные условные переходы, циклы, или подпрограммы. И не зря в мире ant-появились такие супер-команды, которые представляют собой небольшую программу, написанную или на полноценном языке программирования, или на скриптовом языке вроде beanshell. Мое представление о будущем технологий для описания проекта и его жизненного цикла основано на совместной работе maven (на нем описываются структура проекта и настраиваются плагины стандартных действий). А для сложных и нестандартных операций используется, нет, не ant, а какой-то легкий скриптовый язык (тот же javascript или groovy), но содержащий в себе всю ту наработанную огромную базу высокоуровневых команд, которые есть сейчас в ant. Подобные продукты уже появляются, хотя говорить об их широком распространении рано. Если, кто-то заинтересовался и хочет увидеть пример, то обратите внимание на проекты: gant (http://gant.codehaus.org) и gradle (http://www.gradle.org/). Авторы gant буквально одним предложением описывают то для чего предназначен gant и как он соотносится с ant: “A Groovy-based build system that uses Ant tasks, but no XML”. Т.е. gant это “молочный брат” ant (вся ant-функциональность поддерживается), но без громоздкого xml. Если у вас есть немножко свободного времени (и, конечно, вы имеете представление об groovy), то попробуйте “поиграть” с gant и посмотрите то, насколько четче могут быть переписаны сценарии сборки проекта. Для “быстрого старта” с gant советую попробовать такой инструмент как ant2gant (его можно найти на официальном сайте gant). Ant2gant предназначен для автоматизации конвертации xml сценария ant в gant-сценарий. Что касается второго продукта (gradle), то если gant – аналог ant, то gradle замахивается на большее: на то чтобы стать рядом с maven. В gradle есть поддержка концепции жизненного цикла проекта, декларативное управление зависимостями, и, что приятно, gradle интегрируется с maven репозиториями артефактов (библиотек). Вступление несколько затянулось, но я считаю очень важным донести до вас очень простую мысль: ant и maven не соперники – они решают одну и туже проблему (управление проектом), но с разных позиций. И лучше всего нам научиться использовать ant и maven совместно.

А теперь я перехожу к “немного попрограммировать” и расскажу о таких maven плагинах как “maven-ant-plugin” и “maven-antrun-plugin”. Первый из них предназначен для генерации на основании pom-файла с maven-проектом, файла build.xml. Файл build.xml будет содержать набор ant-шагов, повторяющих все те действия, которые выполняет и maven при сборке и упаковке проекта. В качестве “подопытного кролика” я возьму многомодульный java-проект, о котором рассказывал последние две статьи. Зайдя в корневой каталог проекта “testmultimodule”, я выполняю команду “m2 ant:ant”. В результате ant плагин сгенерирует 11 новых файлов. В корневом каталоге проекта появятся два файла-компаньона: build.xml и maven-build.xml. Давайте посмотрим на их внутреннее устройство. Файл build.xml на самом деле почти пустой и содержит только подключение файла maven-build.xml. Что касается второго, то здесь все хитрее. Т.к. проект многомодульный, то необходимо для каждого из модулей написать свой особенный ant-скрипт. И действительно, в каждом из подкаталогов (и в каталоге модуля business-logic и модуля web-interface) мы находим два ant-файла: build.xml и maven-build.xml. Что касается файла maven-build.xml, расположенного на верхнем уровне, то в нем подключаются файлы build.xml из подчиненных модулей. А также определяются ant “цели”, названия которых совпадают с названиями фаз жизненного цикла из мира maven: clean, compile, compile-tests, test, javadoc, package … Естественно, что каждый из модулей вкладывает в эти цели свое представление, так модуль application при выполнении цели package формирует ear-файл с готовым для развертывания на сервере приложением. А модуль web-interface в цели (чуть не сказал фазе) package генерирует war-файл веб-приложения. Если выполнить команду “ant compile”, то через пару секунд получим в каталоге target скомпилированный и готовый к работе файл application-1.0.ear. Приятно, что сгенерированный ant-скрипт, знает о существовании maven-репозитория и умеет брать оттуда нужные для компиляции библиотеки. Единственно, о чем стоит упомянуть так это о файле с настройками ant. Посмотрите еще раз внимательно на подкаталоги модулей проекта: каждый из них содержит текстовой файл maven-build.properties. Файлы эти имеют очень простой формат: ключ=значение. И содержат список переменных, нужных для работы ant (например, путь к каталогу с исходными кодами и каталогу, куда нужно поместить результаты компиляции). Ни одну из настроек трогать не стоит, кроме одной, первой опции “maven.settings.offline”. Эта переменная представляет собой аналог такой функции maven как работа в offline-режиме. Напомню, что, работая в режиме offline, maven не обращается к internet репозиториям за артефактами, в тех случаях, когда артефакты уже находятся в локальном репозитории. Ant скрипты не такие умные как maven, и если вы выключите (а это значение по-умолчанию) переменную “maven.settings.offline”, то ant не будет проверять наличие библиотек артефактов у вас на компьютере, а сразу полезет в internet за ними - и это не хорошо. Если вас заинтересует вопрос использования ant-сценариями maven-репозиториев, то обратите внимание на проект maven ant tasks (http://maven.apache.org/ant-tasks/index.html). В следующем примере (любезно позаимствованном на официальном сайте проекта) показывается то, как можно определить набор “dependencies” библиотек из maven-репозитория (все функции связанные с транзитивным разрешением зависимостей и областями видимости поддерживаются).
  1. <artifact:dependencies pathId="dependency.classpath">
  2.   <dependency groupId="junit" artifactId="junit" version="3.8.2" scope="test"/>
  3. </artifact:dependencies>
Так мы определяем ant-переменную “dependency.classpath” равную списку библиотек зависимостей. Затем мы можем либо скопировать набор библиотек, например, в каталоге WEB-INF/lib для веб-приложения. Либо мы можем передать эти сведения компилятору javac:
  1. <javac ...>
  2.   <classpath refid="dependency.classpath" />
Теперь перейдем к рассмотрению следующего maven плагина – antrun. Этот плагин, в полном соответствии со своим названием, дает нам возможность вызывать из maven кусочки ant-кода. Эти кусочки могут быть либо непосредственно расположенными внутри файла pom.xml, выглядит это примерно так:
  1. <build>
  2. <plugins>
  3.  <plugin>
  4.    <groupId>org.apache.maven.plugins</groupId>
  5.    <artifactId>maven-antrun-plugin</artifactId>
  6.    <version>1.3</version>
  7.    <executions>
  8.      <execution>
  9.        <phase>install</phase>
  10.        <goals> <goal>run</goal> </goals>
  11.      <configuration>
  12.       <tasks if="doIt" >
  13.        <echo>
  14.   $basedir = ${basedir}
  15.   $maven.compile.classpath = ${maven.compile.classpath}
  16.   $maven.runtime.classpath = ${maven.runtime.classpath}
  17.   $maven.plugin.classpath = ${maven.plugin.classpath}
  18.         </echo>
  19.       </tasks>
  20.      </configuration>
  21.     </execution>
  22.    </executions>
  23.   </plugin>
  24.  </plugins>
  25. </build>
Либо мы размещаем ant-сценарий во внешнем файле и ссылаемся на него из maven-проекта, так:
  1. <tasks>
  2.   <ant antfile="build.xml" />
  3. </tasks>
Вызов плагина antrun привязан к фазе install жизненного цикла проекта. Т.е. сразу после того, как проект будет скомпилирован, упакован и помещен внутрь в локальный maven-репозиторий, maven выполнит те ant команды, которые мы разместили внутри тега . В моем случае, я просто распечатал на экране значения предопределенных ant-maven переменных (maven.compile.classpath, maven.runtime.classpath, maven.plugin.classpath). Эти переменные нам пригодятся, во-первых, когда мы захотим выполнить, например, компиляцию проекта и нам будут нужны все библиотеки, от которых были заявлены зависимости в pom.xml файле. Различия переменных maven.compile.classpath и maven.runtime.classpath а также, непоказанной в примере, переменной maven.test.classpath в том, к какой области действия (scope) относятся артефакты, пути к которым хранятся в переменных maven.compile.classpath, maven.runtime.classpath, maven.test.classpath. Это, соответственно, compile, runtime, test области действия артефактов. Из ant-скрипта, внедренного внутрь maven-проекта, мы можем ссылаться и на переменные, объявленные в maven. Синтаксис обращения к переменным идентичен и для maven и для ant. В следующем примере я обращусь к maven-переменной “myapp.version” и распечатаю ее значение на экран:
  1. <echo> ${myapp.version} </echo>
Кроме переменных, объявленных внутри файла проекта, мы можем из ant-сценария сослаться и на переменные, объявленные внутри файла с глобальными maven-настройками settings.xml. О назначении этого файла я упоминал в третьей статье серии. В следующем примере я возьму из settings.xml переменную, хранящую путь к каталогу с maven-репозиторием и распечатаю ее значение на экран:
  1. <echo> ${settings.localRepository} </echo>
Что касается переменной maven.plugin.classpath, то она имеет особое назначение и представляет собой список путей к тем артефактам, от которых была заявлена зависимость, но не самого проекта, а плагина antrun. А зачем нужно добавлять зависимости к самому плагину? Дело в том, что почти всем стандартным командам (task) ant поставлены в соответствии maven плагины. Например, ant-команда war, выполняющая упаковку веб-приложения в war-архив, имеет аналог в виде maven плагина war (мы рассматривали этот плагин прошлой статье серии). И подобных примеров соответствия можно привести очень много. С другой стороны, за прошедшие годы ant стал общепринятым стандартом; и было разработано огромное число плагинов, специальных команд для ant, аналогов которых среди maven нет. Подобное использование “ну очень нестандартных ant-команд” предполагает следующее. Перед запуском ant-сценария нужно подготовить особый каталог, внутри которого разместятся не идущие в стандартной поставке ant библиотеки (внутри этих библиотек и находятся специфические ant команды). А сам ant-скрипт, перед тем как вызывать некоторую нестандартную команду должен ее предварительно “определить” (taskdef), указав путь к файлу библиотеки в котором описывается нужная нам ant-команда. Как следствие, если, вызываемый из maven, скрипт ant содержит обращения к какой-то нестандартной библиотеке, то нужно задекларировать зависимость плагина antrun от нее, например, так (теперь я могу внутри ant-сценария вызывать команды, находящиеся внутри библиотеки apache-bsf):
  1. <plugin>
  2.   <groupId>org.apache.maven.plugins</groupId>
  3.   <artifactId>maven-antrun-plugin</artifactId>
  4.   <version>1.3</version>
  5.   <dependencies>
  6.     <dependency>
  7.       <dependency>
  8.       <groupId>ant</groupId>
  9.       <artifactId>ant-apache-bsf</artifactId>
  10.       <version>1.6.5</version>
  11.     </dependency>
  12.   </dependencies>
  13. </plugin>
Еще интересный момент в моем примере ант-сценария, внедряемого внутрь pom-файла с проектом, – это возможность выполнять или не выполнять ant-задачи в зависимости от параметров, с которым был запущен maven-проект на сборку. Обратите внимание на строку ‘’. Это значит, что если вы запустите maven привычной уже командой “m2 install”, то ant-сценарий выполняться не будет, т.к. значение переменной doIt не определено. А теперь попробуем вызвать maven таким образом “m2 install –DdoIt” (-D служит для создания переменных поступающих внутрь и maven и ant-сценариев). Во втором случае ant-сценарий будет выполнен. По аналогии с условием “if” мы можем записать условие “unless”. Это будет значить, что ant-сценарий будет выполняться всегда, за исключением тех случаев, когда значение переменной “doIt” будет определено. Т.е. передав переменную doIt при вызове maven, мы отключим выполнение ant-скрипта.