От велосипеда к Maven
Java
Из песочницы
Так уж сложилось, что до недавнего времени все проекты, написанные мною на 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} чуть попозже.https://www.youtube.com/
Автоматизация нумерации сборок без необходимости лезть в код
А что же там такого, внутри этих файлов .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-аутентификацией, поэтому многие зависимости приходилось скачивать вручную и класть в нужные поддиректории.