Так уж сложилось, что до недавнего времени все проекты, написанные мною на Java я собирал, кхм, за меня собирал NetBeans. И меня такой расклад вещей вполне устраивал: после сборки всего проекта всё аккуратно складывалось в директорию dist со всеми подвязанными библиотеками, оставалось накидать туда пользовательской документации, необходимых native-библиотек (например от Firebird) и в путь, т.е. всё в архив. Когда то я делал это вручную, потом велосипедом, а потом уже Maven'ом. Под катом находится история о том, как же я пришел в стан maven и что из этого получилось.Интересные вещи начали твориться, когда мне захотелось обернуть явовский jar-ник в exe-файл, как-то смотреть приятнее на него, чем на bat-ник (хотя бы потому что можно иконку прикрутить :)), а потом ещё и документация чтоб сама копировалась, а потом чтоб в архив всё собиралось, а потом и номер версии чтоб присутствовал в названии архива, а потом… а потом… в общем понеслось. Закончились эти хотелки написание велосипеда, добавлением к названию проекта префикс builder как имени моего велосипеда и написанием небольшого кода, выполняющего всю работу за меня (кроме, правда что сборки проекта NetBeans'ом). А номер версии, о ужас, вообще вытягивался из строковой константы где-то в исходниках. Понятное дело, на универсальность мой инструмент не претендовал, а вышеописанные хотелки обещали повторяться и дальше для других проектов.
И вот я познакомился с Maven. Случайно почитал о нем на одному русско-язычном ресурсе, понял, что мои задачи он решит, но до конца не понимал, зачем использовать именно его, чем велосипед не крут? Полистал форумы в интернете, благо там нашлись такие же непонимающие как и я, которым объясняли, объясняли, вроде убедили. Началось изучение, трудное, с учетом моего знания английского, а вся документация именно на этом языке. Очень помогли статьи с хабра: Apache Maven — основы и Maven — автоматизация сборки проекта. Последней каплей принять правильное решение оказалось воспоминание, как приходилось заново линковать библиотеки для проекта на Java, склонированного с Mercurial-репозитория. Хорошо хоть, немного их было и разработчик прежних проектов сидел напротив, спросить можно было :).
Итак, перед глазами витала цель: надо всё сделать красиво, чтоб всё собиралось за меня :) А лень, как говорится, двигатель прогресса. В итоге сформировались следующие задачи:
— сборка всего проекта как в NetBeans (со всеми библиотеками);
— автоматизация нумерации сборок без необходимости лезть в код;
— оборачивание jar в exe-файл;
— синхронизация с Mercurial-репозиторием (вообще хотелось создавать сборки на основе номера коммита);
Благо, поддержка Maven в NetBeans идет из коробки;
— добавление пользовательской документации и прочих нужных файлов (например, файлов .properties вне jar-архива).
Что входит в понятие «как в NetBeans»: скопировать все ресурсы (изображения, .properties-файлы), подтянуть все связанные библиотеки, прописать необходимую информацию в manifest. Стандартно maven собирает в jar-архив только .class-файлы и ничего лишнего, а манифест скуден до безобразия.
В этой статье я не буду рассказывать о фазах жизненного цикла проекта. Посмотреть можно здесь Introduction to the Build Lifecycle и в статьях, приведенных выше.
Итак, все мы (ура, уже и я придачу :)) знаем, что вся информация по сборке проекта хранится в файле pom.xml. Там и зависимости (артефакты проекта и plugin'ов), и конфигурация этих самых plugin'ов, что и как нужно сделать, куда положить, какие ещё манипуляции провести. Каждый плагин конфигурировался в своем теге <plugin/> с указанием, конечно же, его версии, имени, фазы жизненного цикла. Все конфигурации помещались в общий тег <plugins/> внутри тэга <build/>, отвечающего за сборку всего проекта.
Разберемся по пунктам.
Сборка проекта как в NetBeans
Что сюда входит? Ага, уже определились: копирование всех ресурсов из директории src (там лежат исходники), копирование в итоговую директорию (target) всех зависимостей, добавление информации в manifest.
Копирование всех зависимостей осуществлялось плагином maven-dependency-plugin, ниже его конфигурация:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <configuration> <outputDirectory>${project.build.directory}/lib/</outputDirectory> <overWriteReleases>false</overWriteReleases> <overWriteSnapshots>false</overWriteSnapshots> <overWriteIfNewer>true</overWriteIfNewer> </configuration> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> </execution> </executions> </plugin>
Обращаем внимание, что все они складываются в директорию lib (как в NetBeans :)). При этом указываем, что перезаписываем библиотеки с наличием более новых версий, не перезаписываем текущие версии и не перезаписываем зависимости без окончательной версии (snapshot). Также указывается, на какой фазе происходит данная операция: package. В принципе, тут без разницы, когда гости пожалуют.
Информация в манифест добавлялась следующим образом:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <classpathLayoutType>simple</classpathLayoutType> <mainClass>com.khmb.block_v2.Block_v2App</mainClass> </manifest> <manifestEntries> <Version>${buildNumber}</Version> </manifestEntries> </archive> </configuration> </plugin>
В тэге <classpathPrefix/> как раз и указывается, что библиотеки тянутся из директории lib. Тэг <classpathLayoutType/> со значением simple говорит сборщику, что jar-ники следует скидывать в одну кучу. Есть ещё значение repository, тогда библиотеки будут складываться как в репозитории maven, т.е. со всеми поддиректориями с именами пакетов, версий, названий библиотек.
Стоит отметить переменную ${buildNumber}, о ней рассказано ниже.
Помимо зависимостей, в манифесте указывается класс, с которого будет запускаться программа.
Описание всех параметров используемого плагина лежит здесь: Maven JAR plugin, а там много интересного.
Далее требовалось перед сборкой в jar-архив скидать все ресурсы (картинки и .properties-файлы) в директорию со скомпилированными .class-файлами.
<plugin> <artifactId>maven-resources-plugin</artifactId> <version>2.5</version> <executions> <execution> <id>copy-resources</id> <phase>validate</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.outputDirectory}/com/khmb/${project.name}</outputDirectory> <resources> <resource> <directory>${project.build.sourceDirectory}/com/khmb/${project.name}</directory> <filtering>true</filtering> <includes> <include>**/*.properties</include> </includes> </resource> <resource> <directory>${project.build.sourceDirectory}/com/khmb/${project.name}</directory> <includes> <include>**/*.png</include> </includes> </resource> </resources> </configuration> </execution> </executions> </plugin>
Никаких хитрых копирований, просто прошу maven положить всё точно также, как и было. Но есть одна особенность: если png-файлы (а в проекте используются картинки только в этом формате) копируются просто так, то файлы .prioperties копируются фильтрующе, т.е. плагин посмотрит внутрь них и если что нужно заменить переменными maven'а, заменит. На это указывает параметр тэга <filtering/> — true. Поэтому-то ресурсы и разнесены по разным тэгам <resource/> — картинки фильтровать бессмысленно.
Компиляция проекта регулируется следующим плагином:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>${jdkVersion}</source> <target>${jdkVersion}</target> </configuration> </plugin>
О переменной ${jdkVersion} чуть попозже.
Автоматизация нумерации сборок без необходимости лезть в код
А что же там такого, внутри этих файлов .properties? Всё очень просто, в одном из них лежит номер версии приложения, который тянется в runtime'е и отображается в окошке с информацией об этом приложении (так называемый about).
Application.version = ${buildNumber}
И откуда взялся этот билд-намбер? Своим появлением на свет он обязан плагину buildnumber-maven-plugin. Роль плагина заключается в формировании номера версии, абсолютно любого. Я же решил включить туда номер версии моей программы (а точнее артефакта), плюс дату сборки:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>buildnumber-maven-plugin</artifactId> <version>1.0</version> <configuration> <format>{0}-{1,date,yyyyMMdd}</format> <items> <item>${project.version}</item> <item>timestamp</item> </items> <doCheck>true</doCheck> <doUpdate>true</doUpdate> </configuration> <executions> <execution> <phase>validate</phase> <goals> <goal>create</goal> </goals> </execution> </executions> </plugin>
Номер версии собирается из нескольких частей (тэг <format/>), каждая из которых заключается в фигурные скобки и формируется согласно описанию из MessageFormat языка Java. Каждой часть соответствует тэг <item/>, указывающий, какое значение должно быть подставлено. Кстати говоря, можно вставить в него текст «buildNumber», только каждый раз при сборке будет генерироваться номер этой самой сборки (значение хранится в файле buildNumber.properties) в корне директории проекта. Я отказался, ведь номер может быть очень большим ввиду постоянных проверок работоспособности своей программы, сколько раз приходится его запускать.
Оборачивание jar в exe-файл
Во времена моего велосипеда я наткнулся на хорошую программку launch4j, которая мало того, что оборачивала jar в exe, так она ещё и позволяла добавить иконку приложению (а без неё exe-шник очень похож был на какой-нибудь вирус или старую добрую dos-программу), информацию об авторе, версии и прочем, указать, какую версию jre следует использовать, откуда на неё ссылаться (можно ведь и portable-версию с собой таскать), да в общем много чего там ещё можно сделать. Все настройки хранятся в xml-файле, его описание лежит на сайте программы. Велосипедом я формировал этот xml-файл и передавал его путь в качестве параметра вызываемого launch4j.exe. На выходе получал exe-файл, который был привязан к зависимостям также, как и его jar-собрат (т.е. должен лежать там же, ссылаться на те же зависимости, если не указаны какие-либо особые параметры конечно же). Каково было моё счастье, что эта полезная программка существует и в виде плагина к maven. Конфигурация плагина практически полностью соответствует его старшему брату-программе, за исключением некоторых особенностей, которые можно подглядеть вот здесь. Кстати говоря, можно собирать бинарники не только ОС Windows, но и под Linux. Ниже приведен мой конфиг.
<plugin> <groupId>com.akathist.maven.plugins.launch4j</groupId> <artifactId>launch4j-maven-plugin</artifactId> <executions> <execution> <id>l4j-clui</id> <phase>package</phase> <goals> <goal>launch4j</goal> </goals> <configuration> <headerType>gui</headerType> <outfile>target/${exeFileName}.exe</outfile> <jar>target/${project.artifactId}-${project.version}.jar</jar> <errTitle>${product.title}</errTitle> <icon>favicon.ico</icon> <classPath> <mainClass>com.khmb.block_v2.Block_v2App</mainClass> <addDependencies>true</addDependencies> <preCp>anything</preCp> </classPath> <jre> <minVersion>${jdkVersion}.0</minVersion> </jre> <versionInfo> <fileVersion>${project.version}.0</fileVersion> <txtFileVersion>${project.version}</txtFileVersion> <fileDescription>Программа для блокировки счетов в соответствии со списком</fileDescription> <copyright>Copyright © 2011 ${product.company}</copyright> <productVersion>${project.version}.0</productVersion> <txtProductVersion>${project.version}</txtProductVersion> <companyName>${product.company}</companyName> <productName>${product.title}</productName> <internalName>${exeFileName}</internalName> <originalFilename>${exeFileName}.exe</originalFilename> </versionInfo> </configuration> </execution> </executions> </plugin>
Я указал, какую версию jre следует использовать (соответствует переменной ${jdkVersion}), входной файл, выходной файл, какую иконку прикрутить, ну и информацию, которую можно глянуть при просмотре информации об exe-файле. Почему номер версии jre написан так: ${jdkVersion}.0? Всё очень просто, плагин launch4j требует номер версии в формате x.x.x[_x], а в другом месте файла pom.xml (в конфигурации плагина компиляции) требуется указать номер версии в формате x.x. Ну не следить же за идентичными параметрами по отдельности? Поэтому общая часть была вынесена в переменную (смотри в конце).
Кстати, чтобы плагин подтянулся из maven-репозитория в интернете (в центральном репозитории этого плагина нет) требуется указать ещё один:
<repositories> <repository> <id>akathist-repository</id> <name>Akathist Repository</name> <url>http://www.9stmaryrd.com/maven</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>akathist-repository</id> <name>Akathist Repository</name> <url>http://www.9stmaryrd.com/maven</url> </pluginRepository> </pluginRepositories>
Тэги <repositories/> и <pluginRepositories/> лежат внутри корневого тэга project, т.е. наравне с тэгом-описанием зависимостей и тэгом-описанием сборки.
Синхронизация с Mercurial-репозиторием
Изначально была задумка, чтобы плагин брал номер последнего коммита активной ветки и вставлял его в качестве номера сборки проекта. Но у меня так и не вышло, как это сделать. Буду очень благодарен тем, кто подскажет.
А вообще суть работы с репозиторием сводился к тому, чтобы перед сборкой всего проекта он получал последние изменения из своего репозитория. Тут мне помог плагин maven-scm-plugin. К тому же, была заявлена полная поддержка Mercurial, в отличие, скажем, от Git. Ну и здорово, потому что я привык именно к ртутному репозиторию :). А конфигурация следующая:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-scm-plugin</artifactId> <version>1.5</version> <configuration> <!-- <username>username</username> <password>password</password> //--> <connectiontype>developerConnection</connectiontype> </configuration> <executions> <execution> <phase>validate</phase> <goals> <goal>update</goal> </goals> </execution> </executions> </plugin>
Параметры логина и пароля закомментированы по той причине, что у меня используется локальный репозиторий без необходимости в авторизации. Да, кстати, вне тэга <build/> указываются параметры, где находится наш репозиторий:
<scm> <connection>scm:hg:file:///${project.basedir}</connection> <developerConnection>scm:hg:file:///${project.basedir}</developerConnection> <url>file:///${project.basedir}</url> </scm>
Как, что и где подкручивать, описано опять-таки в официальной документации: SCM Implementation: Mercurial. Делается запрос на репозиторий, принимаются последние изменения, фиксируются и у нас последняя версия проекта (в рамках активной ветки конечно же).
Финиш
Осталось подвести финальную черту, включить в проект пользовательскую документацию, запаковать всё в архив и радоваться. За это отвечает плагин maven-assembly-plugin
<plugin> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <id>assembly</id> <phase>package</phase> <goals> <goal>attached</goal> </goals> <configuration> <descriptors> <descriptor>assembly.xml</descriptor> </descriptors> </configuration> </execution> </executions> </plugin>
В документации к плагину настоятельно рекомендуется использование именно цели attached. Вся же конфигурация хранится в отдельном файле assembly.xml. Вот его содержимое:
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> <id>bin</id> <formats> <format>zip</format> </formats> <fileSets> <fileSet> <directory>${project.basedir}</directory> <outputDirectory>/</outputDirectory> <includes> <include>data.ini</include> <include>ReleaseNotes.txt</include> </includes> </fileSet> <fileSet> <directory>${project.basedir}</directory> <outputDirectory>/docs</outputDirectory> <includes> <include>User's guide.pdf</include> </includes> </fileSet> <fileSet> <directory>${project.build.directory}</directory> <outputDirectory>/</outputDirectory> <includes> <include>${exeFileName}.exe</include> <include>${project.artifactId}-${project.version}.jar</include> <include>lib/**</include> </includes> </fileSet> </fileSets> </assembly>
Указываем, что нам нужно вложить из каких директорий (не забываем про exe-файл и подвязанные библиотеки), а в списке форматов указываем только zip, остальные в принципе в моем случае не нужны. Всё, на выходе получаем архив со всем необходимым внутри (главное ничего не забыть :)).
Переменные
Все конфигурации просто кишат этими переменными. Что ж, расскажем.
— ${project.basedir} — хранит в себе путь до корневой директории проекта на maven;
— ${project.build.directory} — обычно соответствует поддиректории target проекта на maven;
— ${project.build.outputDirectory} — соответствует директории внутри target, куда складываются скомпилированные .class-файлы. Обычно имеет имя classes, и именно из её содержимого собирается конечный jar-архив.
— ${project.name} — название нашего артефакта, берется из тэга <artifactId/>
— ${project.version} — версия артефакта, значение тэга <version/>
Остальные переменные определялись собственноручно, в тэге <properties/> внутри корневого тэга <project/> файла pom.xml. Вот его содержимое:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <exeFileName>block</exeFileName> <product.title>Блокировка счетов</product.title> <product.company>Название моей организации :)</product.company> <product.year>2011</product.year> <jdkVersion>1.6</jdkVersion> </properties>
Замечу, что имя exe-файла указано без расширения, т.к. в конфигурации плагина launch4j иногда требуется указать полное имя, а иногда без расширения.
Вывод
Используqте maven! :) потратив раз на его изучение вы сэкономите время потом.
Так мой проект оказался в локальном maven-репозитории, но можно настроить его публикацию в удаленном репозитории (например, в сети организации). При клонировании же проекта из репозитория Mercurial все зависимости подтянутся автоматически — очень удобно. И разрабатывайте проект дальше, хоть в NetBeans, хоть в Eclipse, хоть в IntelliJ IDEA — кому как больше нравится.
PS: все зависимости подтягиваются из интернета, потом складываются в локальный maven-репозиторий и в дальнейшем берутся именно от туда. Каково было моё «счастье», что на работе конфигурация прокси maven'а была несовместима с NTLM-аутентификацией, поэтому многие зависимости приходилось скачивать вручную и класть в нужные поддиректории.
