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

April 6, 2009

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

В прошлой статье я остановился на том, что начал рассказывать об репозиториях артефактов, о том как регистрировать в файле проекта дополнительные сайты-репозитории артефактов, как настроить proxy-сервер для работы maven в сети, в которой нет прямого доступа в internet. Давайте продолжим рассмотрение характеристик артефакта-зависимости:
  1. <dependency>
  2.   <groupId>mygroup</groupId>
  3.   <artifactId>myartifact</artifactId>
  4.   <version>1.0</version>
  5.   <classifier>linux</classifier>
  6.   <type>jar</type> 
  7.   <scope>import</scope>
  8.   <systemPath>path-to-lib</systemPath>
  9.   <optional>true</optional>
  10. </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-репозитории в internet. Напомню, что в самом начале файла pom.xml вы указываете такие элементы-координаты как:
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  3.    <modelVersion>4.0.0</modelVersion>
  4.   <groupId>testgroup </groupId>
  5.   <artifactId>obmach</artifactId>
  6.   <version>1.0</version>
  7.   <packaging>jar</packaging>
  8.   ...
Как видите, есть четкое отображение элементов присутствующих в названии проекта и того, как можно на этот проект сослаться в другом проекте. groupId, artifactId, version имеют названия идентичные с названиями элементов внутри объявления зависимости dependency. Поменялось название элемента только для формата (расширения, стратегии упаковки) артефакта: так packaging в объявлении проекта соответствует type. Куда-то только пропал элемент classifier, но о нем позже т.к. classifier вещь сложная и требует рассказа о большом количестве редко используемых функций maven.

Вернемся немного назад и продолжим рассмотрение элементов формирующих зависимости и на очереди стоит элемент scope. Что же это такое, и какими могут быть его значения? Мы уже сталкивались с областями действия зависимостей (именно так переводится scope), когда в самом первом примере проекта хотели использовать библиотеку junit для написания теста, проверяющего логику работы основного приложения. Scope означает область действия, или этап жизненного цикла проекта, в котором эта зависимость будет доступна. К примеру, когда я пометил библиотеку junit как: test. То это значит, что зависимость будет “подсунута” maven-ом в проект, когда выполняется компиляция той части проекта, которая содержит тесты (напомню, что по правилам maven, тесты должны размещаться в каталоге src/test). Также библиотека junit будет “подсунута” в проект перед запуском тестов на выполнение и построением отчета с результатами тестирования кода. Если мы попробуем сослаться на какой-то класс или функцию из библиотеки junit в основной части приложения (каталог src/main), то получим ошибку. Наиболее часто используемая зависимость – это compile. Т.е. библиотека помеченная как compile (или для которой мы не указали значение элемента 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:
  1. <dependency>
  2.   <groupId>sun.jdk</groupId>
  3.   <artifactId>tools</artifactId>
  4.   <version>1.5.0</version>
  5.   <scope>system</scope>
  6.   <systemPath>${java.home}/../lib/tools.jar</systemPath>
  7.  </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”.
Compile Provided Runtime Test
Compile Compile - Runtime -
Provided Provided Provided Provided -
Runtime Runtime - Runtime -
Test Test - 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 сформирует дерево зависимостей как показано здесь:
[INFO] [dependency:tree]
[INFO] mygroup:shared:jar:0.1-SNAPSHOT
[INFO] +- javax.ejb:ejb-api:jar:3.0:provided
[INFO] \- com.flexive:flexive-shared:jar:3.1-SNAPSHOT:provided
[INFO]    +- com.flexive:fxStream:jar:3.1-SNAPSHOT:provided
[INFO]    +- com.flexive:jboss-common-core-42-compat:jar:3.1-SNAPSHOT:provided
[INFO]    +- commons-codec:commons-codec:jar:1.3:provided
[INFO]    +- com.google.collections:google-collections:jar:0.8:provided
[INFO]    +- javax.activation:activation:jar:1.1:provided
[INFO]    +- commons-io:commons-io:jar:1.4:provided
[INFO]    +- commons-lang:commons-lang:jar:2.4:provided
[INFO]    +- commons-validator:commons-validator:jar:1.3.1:provided
[INFO]    |  +- commons-beanutils:commons-beanutils:jar:1.7.0:provided
[INFO]    |  +- commons-digester:commons-digester:jar:1.6:provided
[INFO]    |  |  \- commons-collections:commons-collections:jar:2.1:provided
[INFO]    |  \- commons-logging:commons-logging:jar:1.0.4:provided
[INFO]    +- org.codehaus.groovy:groovy-all-minimal:jar:1.5.6:provided
[INFO]    +- com.thoughtworks.xstream:xstream:jar:1.3:provided
[INFO]    |  \- xpp3:xpp3_min:jar:1.1.4c:provided
[INFO]    +- org.jboss.cache:jbosscache-core:jar:2.1.1.GA:provided
[INFO]    |  \- jgroups:jgroups:jar:2.6.2:provided
[INFO]    \- com.flexive:sanselan:jar:0.87-incubator:provided
[INFO] ------------------------------------------------------------------------
[INFO] Building ejb
[INFO]    task-segment: [dependency:tree]
[INFO] ------------------------------------------------------------------------
[INFO] snapshot mygroup:shared:0.1-SNAPSHOT: checking for updates from maven.flexive.org
[INFO] [dependency:tree]
[INFO] mygroup:ejb-jar:ejb:0.1-SNAPSHOT
[INFO] +- mygroup:shared:jar:0.1-SNAPSHOT:compile
[INFO] +- javax.ejb:ejb-api:jar:3.0:provided
[INFO] +- javax.persistence:persistence-api:jar:1.0:provided
[INFO] \- com.flexive:flexive-shared:jar:3.1-SNAPSHOT:provided
[INFO]    +- com.flexive:fxStream:jar:3.1-SNAPSHOT:provided
[INFO]    +- com.flexive:jboss-common-core-42-compat:jar:3.1-SNAPSHOT:provided
[INFO]    +- commons-codec:commons-codec:jar:1.3:provided
[INFO]    +- com.google.collections:google-collections:jar:0.8:provided
[INFO]    +- javax.activation:activation:jar:1.1:provided
[INFO]    +- commons-io:commons-io:jar:1.4:provided
[INFO]    +- commons-lang:commons-lang:jar:2.4:provided
[INFO]    +- commons-validator:commons-validator:jar:1.3.1:provided
[INFO]    |  +- commons-beanutils:commons-beanutils:jar:1.7.0:provided
[INFO]    |  +- commons-digester:commons-digester:jar:1.6:provided
[INFO]    |  |  \- commons-collections:commons-collections:jar:2.1:provided
[INFO]    |  \- commons-logging:commons-logging:jar:1.0.4:provided
[INFO]    +- org.codehaus.groovy:groovy-all-minimal:jar:1.5.6:provided
[INFO]    +- com.thoughtworks.xstream:xstream:jar:1.3:provided
[INFO]    |  \- xpp3:xpp3_min:jar:1.1.4c:provided
[INFO]    +- org.jboss.cache:jbosscache-core:jar:2.1.1.GA:provided
[INFO]    |  \- jgroups:jgroups:jar:2.6.2:provided
[INFO]    \- com.flexive:sanselan:jar:0.87-incubator:provided
[INFO] ------------------------------------------------------------------------
[INFO] Building war
[INFO]    task-segment: [dependency:tree]
[INFO] ------------------------------------------------------------------------
 .....
Плагин dependency содержит большое количество целей, одни из самых полезных это: dependency:purge-local-repository – служит для удаления из локального репозитория всех артефактов, от которых прямо или косвенно зависит наш проект. Затем удаленные артефакты заново загружаются из internet, это может быть нужно, когда какой-то из файлов артефактов был загружен из internet со сбоями, а у вас нет времени искать его, и проще очистить репозиторий (но ведь не весь) и попробовать загрузить библиотеки заново. Цель (goal) плагина dependency:sources служит для загрузки из internet исходников для всех артефактов используемых в проекте. Это одна из самых полезных функций, которые есть в maven. Ведь разрабатывая и тем более отлаживая какой-то код, часто возникает необходимость подсмотреть исходный код какой-либо библиотеки. В internet, в 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