...
...

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

Одна из самых широко разрекламированных и приятных возможностей maven – это управление зависимостями. Описав в файле pom.xml список артефактов, нужных для работы проекта, мы перекладываем на maven все заботы, связанные с загрузкой библиотек из Интернета, разрешение транзитивных зависимостей. И можем сосредоточиться на, собственно, разработке проекта, написании кода. Увы, но задачу разрешения зависимостей не всегда можно выполнить автоматически, так как существует вероятность конфликтов различных версий библиотек. Как находить и устранять такие конфликты – это как раз тема сегодняшнего материала.

В прошлой статье я остановился на том, что начал рассказывать о репозиториях артефактов, о том, как регистрировать в файле проекта дополнительные сайты-репозитории артефактов, как настроить proxy-сервер для работы maven в сети, в которой нет прямого доступа в Интернет. Давайте продолжим рассмотрение характеристик артефакта-зависимости:

<dependency>
<groupId>mygroup</groupId>
<artifactId>myartifact</artifactId>
<version>1.0</version>
<classifier>linux</classifier>
<type>jar</type>
<scope>import</scope>
<systemPath>path-to-lib</systemPath>
<optional>true</optional>
</dependency>

Первые пять характеристик артефакта (groupId, artifacId, version, classifier и type) вместе называются maven coordinates и служат для однозначного (с небольшой оговоркой) определения, какой именно файл-библиотека нужен проекту, какое имя этого файла. Формально координата артефакта - это четыре слова, разделенные знаком двоеточия, в следующем порядке groupId:artifactId:packaging:version, например, jboss: javassist:jar: 3.0. Путь, по которому находится файл артефакта, использует указанные выше четыре характеристики, например, "c:\Documents and Settings\blackzorro\.m2\repository\jboss\javassist\3.0\javassist-3.0.jar". То есть группе артефакта соответствует имя подкаталога (jboss) внутри каталога репозитория (./m2/repository). Затем идет подкаталог с именем артефакта, его версией. И, наконец, идет файл, в названии которого присутствует имя артефакта, его версия, а расширение файла соотносится с packaging. Правило "расширение файла с артефактом равно его packaging" не всегда верно. К примеру, те, кто знаком с разработкой enterprise приложений, состоящих из бизнес-логики в виде ejb-модулей и интерфейса в виде war-модулей, знают, что модули ejb внешне представляют собой файлы обычных архивов с расширением jar. Хотя записывая зависимость от такого модуля в веб-проекте, мы внутри элемента type пишем именно слово ejb. Если проект или библиотека имеют только одно представление, например, тот же файл архива jar, то packaging опускается, и координаты выглядят так: groupId:artifactId:version. Что касается пятого параметра – classifier, то его значение также участвует в формировании имени файла артефакта и записывается сразу после основного имени файла, перед расширением, например, так: javassist-3.7.ga-classifier.jar.

Зачем я полез в такие дебри координат maven? Все дело в том, что часто на сайтах библиотек или в статьях, блогах, когда идет рассказ о создании проекта, и дело доходит до перечисления того, какие библиотеки нужны проекту, то они задаются не длинными пространными описаниями, вроде "скачайте с сайта A файл B.zip, распакуйте его, найдите в нем…", а короткими тройками (четверками) maven-координат. В любом случае знание о координатах maven будет для вас полезным, так как те же координаты вы будете использовать не только для декларирования "что нужно для проекта", но и для задания координат вашего проекта. Помните, я многократно акцентировал ваше внимание на том, что в maven-мире "все является артефактами". К примеру, когда мы создаем проект и выполняем его компиляцию, то формируем имя файла, в названии которого присутствуют основные черты "maven coordinates". Эти артефакты уже готовы к установке как в локальный репозиторий на вашем компьютере, чтобы их могли использовать другие проекты, так и для распространения в public-репозитории в Интернете. Напомню, что в самом начале файла pom.xml вы указываете такие элементы-координаты, как:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>testgroup </groupId>
<artifactId>obmach</artifactId>
<version>1.0</version>
<packaging>jar</packaging>

Как видите, есть четкое отображение элементов, присутствующих в названии проекта, и того, как можно на этот проект сослаться в другом проекте. groupId, artifactId, version имеют названия, идентичные с названиями элементов внутри объявления зависимости dependency. Поменялось название элемента только для формата (расширения, стратегии упаковки) артефакта: так packaging в объявлении проекта соответствует type. Куда-то только пропал элемент classifier, но о нем позже, так как classifier — вещь сложная и требует рассказа о большом количестве редко используемых функций maven.

Вернемся немного назад и продолжим рассмотрение элементов, формирующих зависимости, на очереди стоит элемент scope. Что же это такое и какими могут быть его значения? Мы уже сталкивались с областями действия зависимостей (именно так переводится scope), когда в самом первом примере проекта хотели использовать библиотеку junit для написания теста, проверяющего логику работы основного приложения. Scope означает область действия, или этап жизненного цикла проекта, в котором эта зависимость будет доступна. К примеру, когда я пометил библиотеку junit как: <scope>test</scope>, это значит, что зависимость будет "подсунута" maven-ом в проект, когда выполняется компиляция той части проекта, которая содержит тесты (напомню, что по правилам maven тесты должны размещаться в каталоге src/test). Также библиотека junit будет "подсунута" в проект перед запуском тестов на выполнение и построением отчета с результатами тестирования кода. Если мы попробуем сослаться на какой-то класс или функцию из библиотеки junit в основной части приложения (каталог src/main), то получим ошибку. Наиболее часто используемая зависимость – это compile. То есть библиотека, помеченная как <scope>compile</scope> (или для которой мы не указали значение элемента scope вообще), будет доступна для компиляции и основного приложения, и его тестов, и на стадии запуска тестов, и на стадии запуска основного приложения. Напомню, что инициировать запуск тестов из управляемого maven-проекта можно выполнив команду "m2 test", а для запуска приложения используется плагин exec (подробнее о нем во второй статье серии). Третий вариант значения scope – provided, его назначение можно разъяснить на примере разработки веб- приложения. Так как веб-приложение выполняется в среде веб-сервера, то в самом сервере уже могут быть установлены наиболее популярные библиотеки, например, hibernate или jsf. Таким образом, нет необходимости упаковывать в файл проекта (war-файл) эти библиотеки, а нужно только те, которые являются редкими и которых наверняка не будет в стандартном джентльменском наборе хостинг-провайдера. Либо есть ситуация, когда версии библиотек, которые были установлены хостером, вам категорически не подходят. Тогда вам придется включать правильные версии в файл war. В любом случае нет смысла включать в файл веб-проекта те библиотеки, которые должны гарантированно быть на веб-сервере по стандарту (например, servlet-api).

Написав эти строки, я с усмешкой вспомнил встретившийся мне пару месяцев назад на одном из сайтов учебный материал, рассказывающий о создании на java собственного то ли сайта, то ли блога. Материал был очень хорош, но несколько портила впечатление ссылка в конце статьи, где предлагалось скачать к себе на компьютер архив размером в пару десяток мегабайт, из которых 99% занимали библиотеки, и только 1% непосредственно код учебного материала. Итак, возвращаясь к maven, — вы разрабатываете приложение, использующее некоторую большую библиотеку X, и при этом уверены, что эта библиотека будет на той машине или веб-сервере, где приложение будет запускаться. В этом случае имеет смысл пометить зависимость как provided, и хоть она будет доступна на стадии компиляции и тестирования приложения, но из финального архива, поставляемого заказчику, зависимость будет исключена. Не могу удержаться от небольшого лирического отступления, адресованного тем, кто говорит про себя: подумаешь, размер больше на десяток-другой мегабайт, за то не будет такого, что потом приложение не запустится у клиента из-за "потерянной" библиотеки. С одной стороны, действительно, наличие в архиве "лишних" библиотек позволит избежать проблемы "на веб-сервере хостера не оказалось чего-то". Но у другой проблемы, проблемы "на сервере хостера оказалась библиотека не той версии", увы, легкого решения нет. К сожалению, в мире java разработчики стандартов не слишком долго думали над задачами размещения и одновременной работы на сервере множества веб-приложений. Так как разным приложениям могут потребоваться разные версии библиотек (и часто несовместимые между собой), то инсталлировать эти библиотеки внутрь сервера, так чтобы они были доступными, общими для всех веб-сайтов на одном физическом сервере, опасно. С другой стороны, если общих библиотек нет, и каждое веб-приложение будет содержать десятки мегабайт библиотеки, дублирующих библиотеки "вот того, соседнего приложения", то очень скоро встанет вопрос об исчерпании памяти, а, следовательно, медленной и неустойчивой работы. Именно это я считаю одной из причин того, что java, будучи очень востребованной на рынке разработки сложных, больших, высокопроизводительных и прочая и прочая веб-приложений, имеет совершенно противоположную сторону для небольших веб-сайтиков, размещаемых пачками на одном физическом сервере. К сожалению, пока нет ни стандарта, ни конкретных продуктов (веб-серверов), которые бы позволяли создавать веб-приложения, построенные на идеологии декларативного описания списка зависимостей, нужных для работы приложения, и их эффективного "коллективного использования" между несколькими веб-приложениями. В любом случае проблема конфликта версий известна, и многие веб-серверы имеют специальные, конечно же, проприетарные технологии разрешения конфликтов. Снова вернемся к maven и возможным значениями для характеристики scope. Четвертым видом области действия scope является runtime, такая библиотека не нужна для компиляции проекта, но нужна на стадии выполнения приложения. В редких случаях вам может пригодиться такое значение scope, как system. К примеру, проект нуждается для работы в некоторой особой, недоступной в public-репозитории зависимости. По какой-то причине вы не хотите выполнять принудительную maven-изацию зависимости и инсталлировать ее в локальный репозиторий (про команду install более подробно смотрите в прошлой статье). В таком случае вы можете указать путь к файлу зависимости внутри элемента systemPath:

<dependency>
<groupId>sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>1.5.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

Есть требование, что значение systemPath должно быть абсолютным путем к файлу с артефактом. Соблюсти это требование практически нереально в случае, если один файл проекта pom.xml используется одновременно коллективом разработчиков (у всех ведь есть свои настройки компьютера). В любом случае, я не рекомендую использовать system scope, вместо этого выполнять установку артефактов в локальный или корпоративный репозиторий, да и разработчики maven говорят, что эту scope они могут в любой момент выкинуть вон как устаревшую. Есть еще один недавно появившийся вариант scope – import, но он слишком специфичен и пока нас не интересует. Давайте лучше пойдем далее и рассмотрим понятие распространения "propogation" для области действия артефакта. Scope propogation тесно связано с автоматическим обнаружением транзитивных зависимостей. К примеру, мы создаем проект A, который зависит от проекта B. Но этот проект, в свою очередь, нуждается в проекте C. Подобная цепочка зависимостей может быть сколь угодно длинной, но нам нужно четкое понимание того, что делает maven и как связаны между собой проект A и проект C. В следующей табличке (любезно позаимствованной с сайта maven) приводится набор правил переноса режима scope. К примеру, если мы подключаем библиотеку "B" как compile, а она в свою очередь подключает библиотеку "C" как provided, то наш проект "A" будет зависеть от "C" так, как указано в ячейке, находящейся на пересечении строки "compile" и столбца "provided".


CompileProvidedRuntimeTest
CompileCompile-Runtime-
ProvidedProvidedProvidedProvided-
RuntimeRuntime-Runtime-
TestTest-Test-


Имея приведенную выше таблицу правил переноса scope и набор файлов pom, соответствующих артефактам (в pom-файлах хранятся сведения о том, какие зависимости нужны для артефакта), мы можем сами построить дерево зависимостей для каждой из фаз жизненного цикла проекта. Другое дело, что строить дерево зависимостей вручную долго и сложно. Поэтому я познакомлю вас с одним из самых полезных maven-плагинов – dependency. Так, выполнив команду "m2 dependency:list", мы получим итоговый список артефактов и их вычисленных scope:

[INFO] [dependency:list]
[INFO]
[INFO] The following files have been resolved:
[INFO] ant:ant:jar:1.5.2:compile
[INFO] antlr:antlr:jar:2.7.6:compile
[INFO] aopalliance:aopalliance:jar:1.0:compile
[INFO] asm:asm:jar:1.5.3:compile
[INFO] asm:asm-attrs:jar:1.5.3:compile
[INFO] bouncycastle:bcprov-jdk15:jar:135:test
[INFO] c3p0:c3p0:jar:0.9.1:compile
[INFO] cglib:cglib:jar:2.1_3:compile

Такой "итоговый" список не слишком удобен для расследования вопроса: откуда взялся тот или иной артефакт (точнее, какой другой артефакт потянул эту зависимость). Гораздо удобнее, если информация будет представлена в виде дерева. Например, команда dependency:tree сформирует дерево зависимостей как показано на рис. 1. Плагин dependency содержит большое количество целей, одна из самых полезных — это dependency:purge-local- repository, служит для удаления из локального репозитория всех артефактов, от которых прямо или косвенно зависит наш проект. Затем удаленные артефакты заново загружаются из Интернета, это может быть нужно, когда какой-то из файлов артефактов был загружен из Интернета со сбоями, а у вас нет времени искать его, и проще очистить репозиторий (но ведь не весь) и попробовать загрузить библиотеки заново. Цель (goal) плагина dependency:sources служит для загрузки из Интернета исходников для всех артефактов, используемых в проекте. Это одна из самых полезных функций, которые есть в maven. Ведь разрабатывая и тем более отлаживая какой-то код, часто возникает необходимость подсмотреть исходный код какой-либо библиотеки. В Интернете, в public репозиториях часто (хотя и не всегда) хранятся не только скомпилированные и готовые к использованию файлы артефактов в виде jar-библиотек, но и их исходники и документация. Например, для артефакта google-collections-0.8.jar исходники будут расположенном рядом в архиве google-collections-0.8-sources.jar, а документация — в файле google-collections-0.8-javadoc.jar (как видите, слово sources или javadoc занимают места, зарезервированные для classifier). В практике использовать вызов dependency:sources только для получения списка файлов с исходниками проекта мало: мы ведь хотим разрабатывать проект в своей любимой среде разработки (IDE), такой, как eclipse или idea. Генерацию проекта выполняет команда: "m2 idea:idea" или "m2 eclipse:eclipse", но о них в следующий раз, а сегодня я продолжу рассказ о плагине dependency. Еще одна полезная функция maven – это создание каталога, внутрь которого будут скопированы абсолютно все, как прямые, так и косвенные, зависимости для проекта. Это первый шаг для того, чтобы сделать разрабатываемое вами приложение переносимым между различными компьютерами. Разработанное вами приложение после выполнения фазы install будет представлено в виде архива jar, содержащего написанный вами код. Естественно, что если нужные для проекта библиотеки-зависимости не будут найдены при запуске проекта в classpath, то ваше приложение не запустится. Общепринятой методикой является создание структуры установочного каталога в виде двух подкаталогов: bin и lib. Внутри lib находится и ваш код, и абсолютно все библиотеки, нужные для его запуска. В каталоге bin находится исполняемый файл в виде cmd-скрипта (для Windows) или sh- скрипта (для Linux). Действия, которые выполняет запускной скрипт (назовем его run.cmd), тривиальны. Необходимо динамически сконструировать строку classpath на основании списка всех библиотек внутри подкаталога lib и передать эту строку на выполнение, например, так:

java –cp ../lib/bar.jar;app.jar myapp.Starter

Как может выглядеть подобный скрипт запуска, я расскажу в следующий раз, а пока сформируем каталог lib с помощью maven (единственная настройка плагина – это путь к каталогу, куда будут скопированы зависимости проекта):
m2 dependency:copy-dependencies -DoutputDirectory=target/lib.

black-zorro@tut.by, black-zorro.com

© Компьютерная газета

полезные ссылки
Аренда ноутбуков
Ремонт ноутбуков в центре Минска