Pull to refresh

От велосипеда к Maven

Reading time13 min
Views72K
Так уж сложилось, что до недавнего времени все проекты, написанные мною на 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-аутентификацией, поэтому многие зависимости приходилось скачивать вручную и класть в нужные поддиректории.
Tags:
Hubs:
+40
Comments56

Articles