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

May 25, 2009Comments Off on Наводим порядок в разработке ПО вместе с maven. Часть 8

Этой статьей я завершаю рассказ об maven и о том, как он позволяет управлять проектами разработки программного обеспечения. Сегодня мы поговорим о том, как выполнить подготовку разработанного вами проекта к поставке заказчику. Тема эта сложна и включает в себя множество аспектов. Начинается все с подготовки исполнимого файла приложения со всеми нужными для его работы ресурсами и библиотеками. Затем нужно подготовить документацию, создать инсталлятор для приложения. И, наконец, выполнить доставку приложения заказчику или же, как вариант, скопировать на сервер в internet, откуда приложение может скачать любой желающий.

Начну я с того, что еще раз повторю список фаз жизненного цикла проекта, разумеется, не всех, т.к. всего есть 21 позиция и с большинством фаз вы никогда не столкнетесь. В простейшем случае, вы можете считать, что есть фазы compile, когда выполняется компиляция проекта и файлы с ее результатами копируются в подкаталог target/classes. После того как компиляция была завершена, необходимо выполнить упаковку (фаза package) текстовых или графических ресурсов приложения вместе со скомпилированными class-файлами в архив, например, jar или war. Затем файл артефакта инсталлируется внутрь локального maven-репозитория (эта фаза называется install). Благодаря этому вы можете из одного maven-проекта ссылаться на результаты, полученные в ходе разработки другого проекта. Последний этап – это когда упакованный файл артефакта копируется в удаленное хранилище, например, в общедоступный internet-репозиторий или ftp-сервер (фаза deploy).

Сегодня мы сосредоточимся на рассмотрении действий, выполняемых в ходе фазы package. В зависимости от того, какой тип проекта или модуля мы задекларировали в pom-файле проекта, maven выполняет различные действия по упаковке проекта. Так, когда в прошлых двух статьях я рассказывал о создании много-модульного проекта, содержащего в себе ejb, war и ear-модули. Тогда я упоминал о том, что maven к фазе package для ejb-модуля привязывает вызов плагина maven-ejb-plugin, для модуля war – плагин maven-wa-plugin и т.д. А эти плагины знают то, по каким правилам (зависящим от типа модуля) нужно формировать архив с упакованным (т.е. готовым к запуску или развертыванию на веб-сервере) проектом. На официальном сайте maven, в списке плагинов, относящихся к разделу “Packaging types/tools”, упоминается только об ear, ejb, jar, war, ear ,rar. RAR – это вовсе не популярный формат архиватора, а сокращение технологии resource adapter archive, служащей для подключения java¬проект к внешним информационным системам. Обо всех этих плагинах (кроме rar) я рассказывал в прошлых статьях, так что сегодня нам осталось разобраться с плагинами jar и shade. Плагин jar используется для упаковки обычных, не веб и не ejb модулей java. Предположим, что мы написали простенькое приложение на java c графическим интерфейсом на swing и хотим, чтобы клиент по клику мыши мог запустить наше приложение и работать с ним.

Сначала я напомню несколько общих положений о том, как устроены исполнимые java приложения. После того как вы установили у себя на компьютере исполнимую среду для java – jre (java runtime environment), то файлы с расширением jar начинают рассматриваться не как архивы, а как исполнимые java-файлы. Т.е. по двойному клику на файле jre читает содержимое архива, находит в нем файл со специальным именем META-INF/manifest.mf (это обычный текстовой файл). Внутри файла находятся специальные директивы, указывающие на то, какое имя класса является точкой входа в приложение (в следующем примере это “com.black-zorro.App”)
Manifest-Version: 1.0
Main-Class: com.black-zorro.App
Затем в том же jar-архиве уже ищется файл с именем (com/black-zorro/App.class) и выполняется. Вроде бы ничего сложного и, фактически, мы можем создавать исполнимые java-архивы с помощью любого zip-архиватора (т.к. jar формат архивов это почти одно и тоже что и zip архив), а также простенького блокнота, для того чтобы написать файл META-INF/manifest.mf. Естественно, что мы не будем делать это “ручками” и попросим maven автоматизировать не только создание jar-архива, но и наполнение файла manifest.mf содержимым. По умолчанию maven выполняет упаковку в архив всех скомпилированных файлов проекта. Напомню, что исходные файлы находятся в каталоге src/main/java, а результаты компиляции размещены внутри подкаталога target/classes. Предположим, мы создали простой java-проект (например, консольное приложение), не нуждающееся для запуска ни в каких дополнительных ресурсах или библиотеках, т.е. нам следует только создать в jar-архиве файл манифеста и указать какой класс является главным. Для этого в pom-файле проекта я внутри раздела project/build/plugins (напоминаю, что это стандартное место для настройки всех плагинов) пишу следующие команды:
  1. <plugin>
  2.   <groupId>org.apache.maven.plugins</groupId>
  3.   <artifactId>maven-jar-plugin</artifactId>
  4.   <configuration>
  5.    <archive>
  6.      <manifest>
  7.      <mainClass>blackzorro.App</mainClass>
  8.      </manifest>
  9.    </archive>
  10.   </configuration>
  11. </plugin>
Теперь запуская команду “m2 package –Dmaven.test.skip=true” (опция “maven.test.skip” позволяет пропустить запуск тестов), я упаковал все классы проекта в единый jar-файл и сформировал файл манифеста следующего вида:
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: blackzorro
Build-Jdk: 1.5.0_11
Main-Class: blackzorro.App
Если теперь дважды “кликнуть” мышью на файле архива или набрать в командной строке следующую команду “java –jar имя-файла-архива.jar”, то среда jre загрузит и выполнит класс “blackzorro.App”. Естественно, что такой файл должен быть в проекте и должен содержать функцию main, как этого и требуют стандарты. Если же наш проект не так тривиален и нуждается для своей работы в разных библиотеках (мы их подключили как maven-зависимости), то требуется сделать пару дополнительных шагов. Напоминаю, что идеология java в области запуска программы на выполнение требует, чтобы все нужные для работы приложения библиотеки были упакованы в архивы с расширением jar. Затем имена этих файлов должны быть перечислены в системной переменной CLASSPATH. Например, так мы указываем список архивов, где выполнять поиск нашего приложения:
set CLASSPATH=H:\docs_xp\testartifact1-1.0.jar;c:\mylibs\hibernate.jar
А так мы его запускаем:
java blackzorro.App
Можно совместить установку CLASSPATH-переменной и запуск программы, если написать так:
java -cp "H:\Java Libs\artifact1-1.0.jar" blackzorro.App
Обо всех этих “внутренностях” запуска java-программ обязан знать любой уважающий себя программист. Вот только наш заказчик, явно, не будет себя утруждать ни созданием системной переменной CLASSPATH (что такое эти системные переменные?!).

Не будет заказчик и писать длинные строки в командной строке (что такое командная строка?!). Заказчик хочет выполнить “клик” по файлу, и он должен работать. Значит, нам нужен способ сконструировать classpath переменную динамически. Достигается это за счет того, что внутри файла manifest.mf перечисляются пути ко всем jar-библиотекам (перечисляются имена библиотек через знак пробела) нужным для работы нашего приложения и выглядеть это может так:
Class-Path: commons-cli-1.1.jar
Теперь мы поставляем заказчику не один jar-файл с нашим проектом, а два – файл проекта и файл commons-cli-1.1.jar. Заказчик выполняет клик по файлу проекта. А затем jre на основании записей внутри файла manifest.mf загружает в память еще и содержимое файла commons-cli-1.1.jar. Для того, чтобы maven автоматически нашел и добавил в файл манифеста сведения обо всех библиотеках, подключенных как зависимости к нашему проекту, нужно всего лишь внутри раздела настройки jar-плагина добавить следующие строки:
  1. <manifest>
  2.   <addClasspath>true</addClasspath>
  3.   <mainClass>blackzorro.App</mainClass>
  4. </manifest>
Но это еще не все: если наше приложение нуждается в большом количестве библиотек, то представляете себе, как “красиво” будет выглядеть поставляемый заказчику каталог с нашим приложением? Внутри каталога будет находиться 20 файлов с непонятными именами и одинаковыми расширениями, среди которых заказчик должен узнать главный “файл” нашего приложения, чтобы выполнить по нему “клик”. Это не допустимо! Первым шагом улучшения будет размещение всех библиотек отдельно от главного файла приложения, например в подкаталоге lib. Для этого внутри конфигурации плагина, внутри элемента “” добавляется еще одна строка:
  1. <classpathPrefix>lib/</classpathPrefix>
После выполнения команды “m2 package” файл манифеста будет содержать перечисление библиотек, предполагая, что они размещены внутри каталога lib:
Class-Path: lib/commons-cli-1.1.jar
В тех случаях, когда вы хотите добавить внутрь файла манифеста собственные, не покрываемые возможностями jar-плагина опции, то можно создать свой файл manifest.mf (как и все прочие файлы с ресурсами проекта, его размещают внутри подкаталога src/main/resources). Затем мы просим maven добавить к генерируемому файлу манифеста дополнительно те строки, которые были написаны вами:
  1. <configuration>
  2.   <archive>
  3.   <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
  4. ... и все как раньше …
Внимательный читатель уже мог задаться следующим вопросом. Я много рассказывал о том, как добавить внутрь файла манифеста требование искать файлы с артефактами-библиотеками внутри каталога lib или в текущем каталоге с главным файлом приложения. Вот только после выполнения команды “m2 package”, ни в одно из этих мест maven не скопировал автоматически сами библиотеки. Откуда же их взять, кроме как “ручками” скопировать артефакты из локального репозитория “G:\Documents and Settings\MyName\.m2\repository”? Да, мы можем заставить maven выполнить “черную работу” за нас, но для этого придется использовать другой плагин – dependency. Мы знакомы с этим плагином по четвертой статье серии. Тогда с помощью плагина dependency мы смогли построить дерево “зависимостей”, представив в наглядной форме то, в каких зависимостях напрямую нуждается наш проект, и то какие зависимости “тянутся” косвенно (транзитивные). Еще, плагин dependency умеет этот сформированный список зависимостей проекта скопировать в указанное нами место. В следующем примере я помещаю внутрь pom-файла проекта, рядышком с описанием плагина jar, ссылку на плагин dependency. А вызов этого плагина привязывается к фазе package. Т.е. при выполнении команды “m2 package”, maven последовательно запустит сначала плагин jar (он сформирует файла манифеста, предполагающий наличие библиотек в каталоге lib). А затем будет вызван плагин dependency, и скопирует в упомянутый выше каталог lib и сами файлы артефактов:
  1. <plugin>
  2.   <groupId>org.apache.maven.plugins</groupId>
  3.   <artifactId>maven-dependency-plugin</artifactId>
  4.   <executions>
  5.    <execution>
  6.     <id>copy-dependencies</id>
  7.     <phase>package</phase>
  8.     <goals><goal>copy-dependencies</goal></goals>
  9.     <configuration>
  10.       <outputDirectory>target/lib</outputDirectory>
  11.      </configuration>
  12.    </execution>
  13.   </executions>
  14. </plugin>
Еще одна интересная сторона совместного использования плагинов jar и dependency – это создание “срезов” (граней, facet) проекта. Здесь уместнее всего будет привести пример из другой области программирования – создания 3d-графики и игр. Если вы создаете игру, то вам нужен 3d-движок, отвечающий за формирование изображения из элементарных частей – полигонов, текстур, источников света. Вы посылаете в движок команду “нарисовать прямоугольник зеленого цвета”, но сам движок ничего не рисует т.к. не имеет прямого доступа к средствам видеокарты. Вместо этого он обращается к одному из двух низкоуровневых средств вывода 3d-изображения – библиотеке directX или openGL. Т.е. приложение состоит как бы из оболочки и двух выбираемых реализаций алгоритмов. С равной долей успеха я могу заменить слово “3d-графика” на “расчет и визуализация физических процессов”, “моделирование” и т.д. В любом случае, наш проект будет состоять из некоторой константной части – графический интерфейс пользователя. Также в состав проекта может входить одна из возможных реализаций алгоритмов – быстрая и не точная для приближенных вычислений или медленная, но качественная реализация модели. Под “срезом” я понимаю каждый из вариантов поставки приложения, т.е. “общая часть” плюс “первый вариант ядра” образуют первый “срез”. Еще один вариант реализации алгоритма плюс интерфейс пользователя – второй “срез” и т.д. Если привести пример специфический для мира java, то пример facet-ов можно найти в ejb-модулях (хотя как раз для создания facet-ов ejb-модулей использовать плагин jar не нужно – там есть свой специфический плагин ejb). EJB-модуль состоит из двух частей – “контрактная” часть – это набор интерфесов (interface), и все нужное для их описания, т.е. набор исключений (exceptions), которые могут выбрасываться из методов интерфейса, список типов данных, которые передаются внутрь методов или возвращаются из ejb-бинов. Вторая часть проекта – это реализация логики работы интерфейсов в виде конкретных классов ejb-бинов. После того как ejb-модуль был развернут на сервере, мы можем получать к нему доступ различными способами, например, через rmi-вызов. Но чтобы сделать так, клиенту необходимо знать то, как выглядят интерфейсы бинов, какие у них есть методы и входные/выходные параметры для них. Так вот, подмножество классов, образующих “контракт” ejb-бинов, можно упаковать в облегченную версию jar-файла. Эти “урезанные” библиотеки называются client-ejb и используются в веб или desktop-приложениях для коммуникации с бизнес-логикой размещенной на сервере приложений. Следующий пример покажет как с помощью плагина jar можно создать facet для проекта, затем дать этому faсet свой classifier:
  1. <plugin>
  2.   <groupId>org.apache.maven.plugins</groupId>
  3.   <artifactId>maven-jar-plugin</artifactId>
  4.   <executions>
  5.    <execution>
  6.     <id>jar-phase1</id>
  7.     <phase>package</phase>
  8.     <goals><goal>jar</goal></goals>
  9.     <configuration>
  10.       <classifier>phase-one</classifier>
  11.       <includes>
  12.         <include>blackzorro/phase1/**/*</include>
  13.         <include>blackzorro/*</include>
  14.       </includes>
  15.     </configuration>
  16.    </execution>
  17.   </executions>
  18. </plugin>
Настройки плагина тривиальны. Во-первых, я не хотел бы потерять “обычные” результаты работы плагина jar (когда упаковываются все файлы проекта). Поэтому я создал отдельное ”исполнение” (execution), затем привязал его к фазе “package” и перечислил в виде элементов include список шаблонов для имен файлов, которые будут упакованы в архив. Само же имя архива будет таким “myProject-1.0-phase-one.jar”. Здесь “phase-one” это значение указанное внутри элемента classifier. Напомню, что все maven проекты, точнее результаты их компиляции и упаковки, подлежат установке в локальный или удаленный репозиторий под специальными maven-координатами. Maven-координаты состоят из названия группы артефакта, названия самого артефакта, версии артефакта, способа упаковки артефакта и classifer или какой-то разновидности артефакта в рамках базовой, например, различные версии одной и той же библиотеки, предназначенные для запуска на разных версиях jre могут отличаться classifier. Для того, чтобы в другом проекте сослаться на не просто артефакт, а его разновидность нужно в секции dependencies, написать так:
  1. <dependency>
  2.   <groupId>blackzorro</groupId>
  3.   <artifactId>myProject </artifactId>
  4.   <version>1.0</version>
  5.   <classifier>phase-one</classifier>
  6. </dependency>
Если мы хотим генерировать для одного проекта несколько facet, то создайте такое же количество элементов execution и разместите их внутри конфигурации плагина. Естественно, что каждое исполнение плагина должно иметь не только уникальный набор включаемых (include) ресурсов, но и уникальный идентификатор.

Разобравшись в работе плагина jar, мы уже можем поставить заказчику исполняемое приложение java, но есть еще одно улучшение. Хотя мы отделили главный файл приложения от библиотек (разместив их в подкаталоге lib), но еще лучшим вариантом будет создание uber-jar. Uber jar-файл - это один огромный jar-файл, содержащий и код нашего приложения и все нужные для его работы библиотеки. Т.е. заказчику поставляется только один файл jar, делая “клик” по которому, клиент запустит наше приложение.
  1. <plugin>
  2.  <groupId>org.apache.maven.plugins</groupId>
  3.  <artifactId>maven-shade-plugin</artifactId>
  4.  <version>1.2.1</version>
  5.  <executions>
  6.    <execution>
  7.      <phase>package</phase>
  8.      <goals><goal>shade</goal></goals>
  9.      <configuration>
  10.        <transformers> 
  11.            <transformer implementation="org.apache.maven.plugins.shade.resource.ComponentsXmlResourceTransformer"/>
  12.        </transformers> 
  13.      </configuration>
  14.    </execution>
  15.  </executions>
  16. </plugin>
Теперь maven возьмет все библиотеки-зависимости нашего проекта, распакует их в один общий каталог с файлами, полученными в ходе компиляции нашего проекта, а затем запакует назад в один общий jar-файл. Проверьте, в каталоге target вы должны найти два jar-файла. Один из них будет иметь перед именем префикс “original-“ – это наш исходный jar-файл без внедренных в него зависимостей, а файл без префиксов содержит uber-jar.

На этом я заканчиваю рассказ об maven. Если взглянуть на первый абзац статьи, то можно увидеть что в списке нераскрытых тем осталось и создание инсталлятора для приложения, и подготовка документации. Но рассказ об этих задачах, скорее всего, превратится в описание специальных программ, например, doxia или izPack. А это лучше всего делать в виде отдельных статей, а не “кусочками” в рамках темы maven. Также мне пришло несколько писем с просьбами рассказать о применении maven разработчиками не на java, а на flex/flash. Но если я это буду делать, то в отдельной серии статей, чтобы охватить и ряд других инструментов для профессиональной работы с flex/flash, но будет это позже. Если же тема совместной работы flex и maven вас заинтересовала, то обратитесь к серии статей опубликованных на сайте http://riapriority.com/blogs/agahov.php/c90/