Наводим порядок в разработке ПО вместе с maven. Часть 2
Я продолжаю рассказ об maven – инструменте управления проектом, составляющими его модулями, зависимостями модулей от различных библиотек-артефактов и многим другим. Maven имеет несколько “лиц” или областей применения, которые будут раскрываться перед вами по мере изучения maven, плагинов для maven и смежных инструментов.
Сегодня я продолжаю рассказ о начатом в прошлой статье примере создания проекта maven, его настройки. Так, разобравшись с основными параметрами проекта (название, описание, список авторов), самое время перейти к описанию зависимостей и тому, как инициировать различные события из жизненного цикла проекта.
Напомню, что рассматриваемый проект maven состоял из трех классов: HelloBean, HelloMaven, TestHelloBean. Первый из них - HelloBean – это класс, реализующий бизнес-логику приложения (не слишком сложную: вывод на экран фразы “hello world”). Затем идет класс HelloMaven, состоящий только из одного метода main, в котором демонстрировалось создание объекта HelloBean и вызов методов “сказать_привет”. В полном соответствии с правилом “код, для которого нет тестов и который нельзя проверить, просто не существует” я создал и третий класс примера - TestHelloBean. Он является наследником стандартного для junit 3 класса TestCase. Возникает естественный вопрос: откуда maven узнает о месторасположении junit3 библиотеки, и как нужно указать к ней путь при запуске javac (компиляции проекта) или запуске самого теста? Все это происходит благодаря “магии” maven dependencies. В файле pom я перечисляю то, какие библиотеки нужны для работы проекта:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Количество зависимостей “dependency”, которые вы можете объявить внутри элемента “dependencies” не ограничено, и каждая из зависимостей состоит из обязательных элементов groupId, artifactId, version. Теперь попробуем скомпилировать проект и запустить его на выполнение. Для этого в каталоге проекта я выполняю команду “m2 compile”. В результате на экране появится множество сообщений вида:
[INFO] Scanning for projects...
[INFO] ------------------------------------
[INFO] Building Simple Maven 2 Artifact
[INFO] task-segment: [compile]
[INFO] ------------------------------------
Downloading: http://repo1.maven.org/maven2/org/apache/maven/plugins/maven-resources-plugin/2.2/maven-resources-plugin-2.2.pom
Если у вас не активно подключение к internet, то запуск maven-а завершится неудачей с сообщением, что maven не может загрузить нужные для своей работы артефакты:
[INFO] Failed to resolve artifact.
GroupId: org.apache.maven.plugins
ArtifactId: maven-resources-plugin
Version: 2.2
Reason: Unable to download the artifact from any repository
org.apache.maven.plugins:maven-resources-plugin:pom:2.2
from the specified remote repositories:
central (http://repo1.maven.org/maven2)
Вспомните, когда вы загружали из internet архив с дистрибутивом maven, разве вас не удивил его маленький размер (всего пара мегабайт). На самом деле дистрибутив maven-а содержит только его ядро, а для любого действия (даже такого примитивного как компиляция) нужны плагины. И эти плагины загружаются из internet, из репозитория расположенного по адресу
http://repo1.maven.org/maven2. Например, maven решил загрузить плагин maven-resources-plugin только для того, чтобы обработать файлы, расположенные в каталоге resources. Для того, чтобы выполнить компиляцию нужен плагин maven-compiler-plugin, и так далее. Значит, вам нужно включить internet и запустить команду “m2 compile” еще раз. После некоторого ожидания (пока нужные плагины загрузятся из сети) maven скомпилирует исходные коды проекта и поместит их в новый каталог target (размещен в корне каталога с проектом). Помимо компилированных *.class файлов, в каталог target будут помещены также все файлы, которые были найдены в каталоге “src/main/resources”. Например, если я использую в своем проекте библиотеку log4j для того, чтобы выводить на экран или в файл отладочные сообщения по ходу работы приложения, то файл log4j.properties нужно размещать, именно, внутри каталога “src/main/resources”. Редактируя java-код в вашей любимой IDE и компилируя его с помощью “m2 compile” можно организовать простенький цикл разработки, но … В практике никто не выполняет запуск только команды compile, т.к. она не выполняет синхронизацию каталога target с содержимым каталога src. К примеру, если в первоначальной версии проекта у вас был класс Apple, и вы его скомпилировали и разместили внутри target,то после того как Apple был удален из каталога “src/main/java”, то в каталоге “target/classes” он все равно будет присутствовать. Поэтому нужно каталог target каждый раз перед компиляцией очищать. И делать это нужно не “руками”, а возложить на maven: maven умеет принимать и обрабатывать сразу несколько команд, например: “m2 clean compile”. Нужные для работы maven библиотеки-плагины загружаются из internet только один раз и сохраняются в локальном репозитории. Если вы работаете на windows xp, то в каталоге с настройками вашей учетной записи "C:\Documents and Settings\MyAccountUserName\" появится подкаталог .m2, в котором maven будет хранить все нужные своей работы артефакты и файлы настроек. Артефакты размещаются внутри подкаталога repository, а файл настроек settings.xml размещен в корне каталога “.m2” (изначально файла с настройками может и не быть). Давайте попробуем найти в репозитории библиотеку junit. Из названия артефакта мы можем предположить, что файл jar должен быть размещен в подкаталоге junit, каталога repository. Однако, ничего подобного в репозитории нет. Почему, ведь разве мы не сказали, что проекту нужна библиотека junit? Не совсем: я не только указал, что мне нужна junit, но и определил область действия библиотеки “
test”. Т.е. запуск команды compile не приводит к компиляции файлов размещенных в каталоге “src/test/java” и, соответственно, загрузке для них нужных артефактов-библиотек. Давайте, выполним команду “m2 test”. Только в этом случае maven начнет загружать библиотеку junit из internet (ого, кроме junit потянулись еще и еще библиотеки: какая-то surefire и многое другое). А еще на экране появится сообщение:
------------------------
T E S T S
------------------------
Running blackzorro.TestHelloBean
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.125 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Как видите, maven не только скомпилировал файлы в каталоге src/test/java (результаты компиляции были помещены в каталог target/test-classes), но еще и запустил тесты на выполнение (один единственный junit тест завершился удачно). Отчеты о запуске тестов были представлены в виде xml и txt файлов, размещенных в каталоге “target/surefire-reports” (вот зачем был нужен плагин surefire). Теперь вернемся в каталог “C:\Documents and Settings\MyAccountUserName\.m2\repository" и снова попробуем найти там подкаталог junit. Теперь каталог мы найдем, но очень важно то, что сама jar-библиотека junit.jar не размещена сразу внутри каталога junit. Вместо этого maven создал подкаталог “junit/3.8.2” и уже внутри которого поместил jar-файл библиотеки junit-3.8.2.jar и еще какие-то, пока непонятные, файлы junit-3.8.2.jar.sha1, junit-3.8.2.pom, junit-3.8.2.pom.sha1 (о них позже). Подобная стратегия хранения артефактов наводит на мысль, что maven может одновременно хранить в своем локальном репозитории библиотеки различных версий и переключение между ними в вашем проекте может быть сведено к тривиальной смене цифры внутри тега “
” для зависимости “”. Давайте, попробуем сменить версию junit с давным-давно устаревшего “3.8” на “4.4”. Я исправил файл pom.xml, также я подправил исходный код
класса TestHelloBean:
public class TestHelloBean {
@Test
public void testSimpleMessage() {
String message = new HelloBean("Maven 2").sayHello();
Assert.assertEquals("Test Hello Machine", "Hello, Maven 2", message);
}
}
Попробуем скомпилировать тесты и запустить их, выполнив команду “m2 test”. Увы, но ничего не получилось: maven вывел сообщение об ошибке:
c:\testartifact1\src\test\java\blackzorro\TestHelloBean.java:[9,5] annotations are not supported in -source 1.3
(try -source 1.5 to enable annotations)
@Test
Если я попробую использовать возможности java 5, такие как аннотации или generic в основном коде приложения (класса HelloBean), то получу аналогичное сообщение об ошибке. Maven, точнее плагин выполняющий компиляцию исходных кодов приложения, решил, что мы пишем код для архидревней java 1.3. Естественно, что это не так и нам нужно каким-то образом подсунуть правильный номер java maven-у. Cамый простой способ это сделать это - указать номер java в параметрах команды compile:
m2 test -Dmaven.compiler.source=1.5 -Dmaven.compiler.target=1.5
Для того, чтобы “подставить” правильную версию (равно как передавать и любые другие конфигурационные переменные плагинам maven) используется стандартный для java способ объявления переменных среды окружения “–D”. Что касается самого перечня конфигурационных переменных, то его легко можно найти на сайте maven. К примеру, для плагина compile, я открываю страницу http://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html и вижу табличку с перечнем всех переменных, управляющих плагином maven-compiler-plugin. Этот пример поднимает очень важный вопрос о настройках maven-а и его плагинов. Дело в том, что даже приведенная выше строка с двумя (пока еще двумя переменными) уже достаточно громоздка и нам нужен способ упростить настройки среды проекта. Можно создать cmd или bat-файл, который вызывает maven с нужными настройками, а можно перенести настройки плагинов в файл проекта pom.xml. Именно на втором варианте мы и сосредоточимся, вот только предварительно мне нужно рассказать о жизненном цикле проекта и том, как с ним связаны плагины. На следующей картинке, позаимствованной с сайта maven (рис. 1), приведена цепочка стадий жизненного цикла. Список этот не полный, т.к. фаз цикла гораздо больше и, более того, фазы различаются в зависимости от того какой вид артефакта “производит” наш проект. Так проект, формирующий настольное java-приложение (swing, swt), имеет стадии жизненного цикла отличные от тех, что характерны для разработке enterprise application (ear). Т.е. когда я запускал команду “m2 test”, то я инициировал целый набор шагов в жизненном цикле проекта: “process-resources”, “compile”, “process-classes”, “process-test-resources”, “test-compile”, “test”. Если внимательно рассмотреть выводимые maven-ом сообщения, то можно найти упоминания этих фаз:
... и ответ maven ...
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [surefire:test]
Каждая из фаз жизненного цикла проекта – это ничего более чем вызов плагинов. Т.к. плагин (jar-библиотека) состоит из некоторого количества целей (goal). Например, плагин “maven-compiler-plugin” содержит две цели: “compiler:compile” (для компиляции основного исходного кода проекта) и цель “compiler:testCompile” (компилируем тесты). Формально, список фаз можно изменять, хотя эта ситуация крайне редка и говорить о ней подробно не стоит. Когда я вызываю maven, то могу указать как параметры командной строки не только имена фаз, но и имена и цели плагинов в форме “плагин:цель”. Например, вызов фазы цикла “m2 clean” эквивалентен вызову плагина “m2 clean:clean”. Мы в файле проекта pom.xml можем настроить для каждого из плагинов жизненного цикла набор конфигурационных переменных, например, так:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<verbose>true</verbose>
<executable>E:/Program_Files_2/jrockit_150_11/bin/javac.exe</executable>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
Элемент build располагается сразу после перечисления зависимостей проекта и состоит из большого количества элементов (конфигурационных переменных). Так элемент sourceDirectory служит для указания пути к каталогу с исходными кодами. Элемент outputDirectory задает каталог, в который будут помещены результаты компиляции модуля. А элемент finalName служит для того, чтобы переопределить правило по которому будет сформировано имя конечного файла. Результат компиляции в виде jar-архива изначально называется так: идентификатор артефакта отделенный дефисом от версии артефакта (например, testartifact-1.0.jar). Нас же больше интересует внутри элемента build возможность влиять на плагины жизненного цикла. Для этого, я создаю элемент plugin, указываю имя его артефакта (напоминаю, в maven артефакты – это почти все). А затем внутри элемента configuration я (подсматривая одним глазком на официальную документацию maven) переопределяю настройки компилятора. Так я не только указал версию исходных файлов, и то какая версия среды выполнения (jre) необходима для запуска моего кода. Но еще я решил переопределить сам используемый компилятор, заменив привычный sun java-компилятор на его аналог от bea (теперь уже oracle). Если же сформировать строку вызова компилятора javac, используя описанные выше “фрагментарные” свойства, не получается, то вам поможет параметр compilerArgument – значение которого это вся строка вызова javac. Как видите, файлы проекта maven могут быть очень маленькими, когда настройки по-умолчанию вас устраивают, и очень большими по мере того, как вам требуется все больше и больше нестандартных функций (и это ожидаемое поведение). В этом плане maven выгодно отличается, например, от ant, для которого даже настройка небольшого проекта часто превращается в сотни строчек конфигурационного файла. Хотя противопоставлять ant и maven не корректно т.к. это продукты с различными целями, но от небольшой шпильки я удержаться не смог. В случае, если вам нужно выполнить какое-то не стандартное действие в определенной фазе, например, на стадии генерации исходников “generate-sources”, то вы можете добавить вызов плагина в файле pom.xml:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>имя-плагина</artifactId>
<executions>
<execution>
<id>myCustomTask</id>
<phase>generate-sources</phase>
<goals>
<goal>PluginGoal</goal>
</goals>
... и настройки плагина …
Самое важное здесь – это указать для плагина элемент “execution/phase” равном имени той фазы, в которой нужно встроить вызов цели плагина “goal”. Фаза “generate-sources” не показана на рис. 1, но она есть, располагается перед вызовом фазы compile и очень удобна, например, для того чтобы сгенерировать часть исходных кодов проекта, например, артефакты для веб-сервисов (wsdl, jaxb-классы).
После того как мы научились компилировать и тестировать проект самое время его запустить. Maven предоставляет очень мощные средства по созданию среды выполнения приложения. И если в случае простых java desktop приложений все эти действия сводятся к вызову плагина “exec:java”, с указанием имени запускаемого java класса в системном свойства “exec.mainClass”, как показано в следующем примере:
mvn exec:java -Dexec.mainClass=package.ClassName
С другой стороны, если вы разрабатываете сложное приложение (например, веб-сайт), то maven предоставит вам возможность запустить в рамках жизненного цикла настоящий веб-сервер, например, jetty (http://jetty.mortbay.org). По моему субъективному мнению jetty работает немного быстрее, чем ставший стандартом де-факто tomcat, но в любом случае, среди плагинов maven есть и средства интеграции с другими веб-серверами и серверами приложений. Завершающим этапом разработки проекта будет его упаковка в такую форму, в которой оно будет поставлено заказчику. К примеру, хотя мы компилировали java-исходники проекта и получили скомпилированные классы в каталоге target/classes, но в таком виде проект бесполезен. Общепринято, упаковать *.class файлы в архив jar, вместе с нужными для запуска проекта библиотеками, также добавить в файл META-INF/manifest.mf строку с именем выполняемого класса. Таким образом, получивший наше приложение клиент просто выполнит двойной клик по запускному файлу (jar-архиву) и приложение автоматически запустится. Все эти действия выполняются в фазе package с помощью вызова плагина “jar:jar”. Мы можем вызвать либо сам плагин “m2 jar:jar”, в результате чего в каталоге target появится новый файл “testartifact1-1.0.jar”. Либо можно инициировать фазу цикла install, в ходе которой мы выполним все предшествующие шаги, включая компиляцию исходных кодов, затем запуск тестов (хотя тесты можно пропустить, если в параметре командной строки указать “m2 install –Dmaven.test.skip=true”) и, наконец, формирование jar-файла в каталоге target.