29 сентября 2011 г.

Основы использования системы сборки Apache Ant

Когда ведется разработка программного продукта, он может состоять из множества исходных частей. Например, разработка может вестись несколькими людьми практически независимо друг от друга, а файлы с исходным кодом, за который они отвечают, могут находится в совершенно разных местах файловой системы и даже на разных машинах. Кроме файлов исходного кода, в проекте могут использоваться также и готовые библиотеки от сторонних разработчиков (например, файлы jar, dll или so), файлы конфигурации, различные картинки и иконки, звуковые файлы и куча всего остального. Все эти штуки могут так же изменяться в ходе разработки проекта другими людьми.

А теперь представьте, что из всего этого безобразия необходимо собрать всего один файл сжатого архива, который пользователь мог бы скачать с вашего сайта, распакавать и сразу же запустить ваш продукт. Вам нужно будет собрать вместе все файлы с исходным кодом Java и скомпилировать их, прогнать тесты, чтобы убедиться что проект никто не поломал, полученные class-файлы упаковать в JAR (или несколько JAR'ов), для которого ещё необходимо сгенерировать правильный манифест, содержащий CLASSPATH, имя главного класса, номер версии и т.п., затем собрать вместе все остальные файлы вроде библиотек, картинок и конфигов, распихать это все в правильные каталоги, сгенерировать документацию, создать из всего этого архив и залить его на ваш вэб-сервер, а всё произошедшее записать в лог и сохранить его в базе данных, которая хранит информацию о различных сборках. Делать всё это руками довольно уныло, да и не должен психически здоровый человек этим заниматься.

Системы сборки, такие как Apache Ant, были созданы для того, чтобы автоматизировать весь этот порой достаточно сложный и рутиный процесс по сборке проекта. Причем, в случае с Apache Ant проект не обязательно должен быть на Java, и не обязательно должен быть вообще программным обеспечением. Это может быть что угодно, хотя ориентирован он, конечно же, в первую очередь на Java. В данной статье я хочу на очень простом примере показать, как можно облегчить себе жизнь с помощью Apache Ant.

Минимальные знания об использовании Apache Ant

О том, как собирать проект, ant узнает из специального xml-файла, в котором разработчик описывает все шаги, необходимые для сборки. Обычно этот файл носит имя build.xml, но оно может быть и другим. Просто при запуске ant сам ищет файл build.xml в каталоге из которого он запущен, а при использовании нестандартного имени необходимо передать его программе ant в качестве параметра.

Файл build.xml содержит внутри себя корневой элемент project, наполнением которого являются элементы, относящиеся к одному из следующих понятий:
  • Цели (Targets)
  • Задачи (Tasks)
  • Ресурсы (Resources)
  • Свойства (Properties)
Задачи представляют из себя различные атомарные операции: компиляция исходных файлов Java, создание JAR, создание каталога, копирование файлов, вывод информации на экран и т.п.. Задачи могут использовать для своей работы отдельные ресурсы (например, файл) или коллекцию ресурсов (множество файлов). Цели, в свою очередь, состоят из одной или нескольких задач, поэтому цель можно рассматривать как функцию или процедуру в языках программирования. Целями могут быть: создание дерева каталогов, компиляция всего проекта, создание дистрибутива, удаление результатов предыдущей компиляции, и всё в таком духе.

Если термины "Цель" и "Задача" запутывают вас, то попробуйте размышлять как-нибудь так: "Для того чтобы достичь какую-либо цель, нужно выполнить определенные задачи". Например, чтобы достичь цель "Скомпилировать проект", необходимо выполнить задачи: "Создать каталог для скомпилированных файлов", "Запустить компилятор javac" и "Создать JAR".

Кроме целей, задач и ресурсов есть свойства. К свойствам можно относится как к переменным, а точнее константам. У свойства есть имя и значение в виде строки. Значение свойства устанавливается один раз и любая попытка изменить это значение игнорируется. Свойства обычно используются в атрибутах задач. Установка значения может происходить в самом файле build.xml или во внешнем properties-файле, что очень удобно и позволяет менять процесс сборки не изменяя build.xml.

Свойства

В файле build.xml значение свойства может быть установлено с помощью элемента property. Этот элемент может использоваться как внутри корневого элемента project, так и внутри target'ов:

Как уже было сказано, свойства могут быть определены и во внешнем файле properties. Записи свойств в этом файле могут выглядеть так:
myProp=myValue
my.text = Very useful text
Чтобы свойства, записанные в файле, стали доступны в сценарии сборки, нужно его подключить:

Для получения доступа к значению свойства используется конструкция ${myProp}. Следующий код выводит на экран значение свойства с именем myProp:


Ресурсы

Примером ресурсов могут быть, например, элементы file и url:

Думаю, что тут всё понятно без лишних слов.

Однако, чаще удобнее пользоваться не отдельными ресурсами, а коллекциями ресурсов - элементами, которые представляют группу схожих ресурсов. Например, элемент fileset представляет группу файлов, которые могут быть описаны множеством разных способов.

В этом примере в fileset войдут все файлы java из каталога src и его подкаталогов, кроме файлов, в имени которых есть слово Test с учётом регистра.

Помимо описанных вариантов ресурсов, также существуют, так называемые, path-like структуры. Не знаю, стоит ли относить их к коллекциям ресурсов, но лучше места для них я не придумал, поэтому упомяну эти структуры тут. Из названия, думаю, понятно, что это элементы которые описывают такие вещи как пути (PATH). Их наполнением могут быть различные ресурсы и их коллекции.

Обратите внимание, что в данном примере так же используются свойства mylib.dir и extlib.dir, значением которых являются пути к каталогам с собственными и внешнимим библиотеками, соответственно.

В описанных path-like структурах, Ant будет автоматически использовать правильный символ сепаратора для разделения ресурсов, соответствующий операционной системе, на которой он выполняется.

Очень удобной возможностью в path-like структурах является возможность одной структуры ссылаться на другую, используя атрибуты id и refid:

Более подробно о ресурсах можно почитать тут.

Задачи

Задачи представляют действия, которые могут осуществляться с какими-либо ресурсами или без их участия. Наиболее часто встречающиеся задачи представлены в таблице.

Часто используемые задачи
Задача (xml-тег)Действие
javacкомпиляция файлов с исходным кодом java
jar создание java-архива
zip создание zip-архива
mkdir создание директории
copy копирование файлов
delete удаление файлов и директорий
exec выполнение внешней команды
mail отправка электронной почты
junit запуск unit-тестов

Ниже приведу простейшие примеры использования задач.

Создание каталога:

Копирование файла:

Компиляция файлов с кодом java:

Иногда для выполнения некоторых задач хватает лишь указания правильных атрибутов, но кроме этого внутри тега задачи могут находиться и вложенные элементы, которые, например, могут описывать ресурсы с которыми необходимо работать задаче. Более интересное применение различных задач будет продемонстрированно позже на простом примере сборки проекта.

Полный список задач представлен здесь.

Цели

Итак, мы подошли к цели :) На целях строится весь сценарий сборки. Вы ставите цели и достигаете их с помощью выполнения задач и использования ресурсов. Зачем использовать цели, если все задачи можно писать прямо в корневом элементе project? А затем, что цели позволяют разделить сценарий сборки на отдельные законченные логические этапы, которые могут быть зависимыми друг от друга. Т.е. одна цель может быть помечена как зависимая от другой цели, и для её достижения необходимо, чтобы сначала была достигнута цель от которой она зависит.

В случае, если задачи расположены в корневом элементе project, то ant, проходя по файлу build.xml, последовательно их выполняет. При таком подходе вы можете определить для себя только один вариант выполнения сборки. При использовании целей можно указать ant'у какую цель необходимо достичь, а он уже сам проверит её зависимости и выполнит сначала цели от которых зависит главная.

В приведенном примере описаны вполне реальные цели, только вместо элементов echo в них должны быть более полезные задачи. В элементе project установлено значение атрибута default равное compile, которое означает, что по умолчанию ant будет выполнять цель compile, которая зависит от цели make.build.dirs, поэтому сначала будет выполнена цель make.build.dirs, а затем compile. Если же ant'у при запуске указать цель dist, которая зависит одновременно от двух целей compile и make.dist.dirs, то сначала будет выполнен этап описанный выше, затем make.dist.dirs, а уже потом dist:
make.build.dirs:
     [echo] creating directories for build...

compile:
     [echo] compiling...

make.dist.dirs:
     [echo] creating directories for distribute...

dist:
     [echo] creating a distribution...
Кроме этого в примере также есть цель clean, которая не зависит ни от какой другой цели, и также нет цели которая зависила бы от clean, поэтому данная цель может быть только явно вызвана ant'ом: ant clean.

В дополнение к зависимостям цели от цели, для управлением процессом сборки существует зависимость цели от свойства, точнее в терминах ant'а это называется условиями. Суть условий заключается в том, что цель может выполняться или не выполняться, в зависимости от того, установлено ли значение какого-либо свойства или нет.

В данном примере цель build.full будет выполняться только, если установлено значение совйства full, в противном же случае будет выполняться build.simple. Абсолютно неважно какое значение установлено в свойство full, это может быть и true, и false, и любая другая строка, даже пустая.

Пример сборки проекта с помощью Apache Ant

Теперь разберем относительно простой процесс сборки маленького проекта.

Для уверенности в том, что сценарий сборки, приведённый ниже, собирает действительно рабочий и самодостаточный проект, я написал очень маленькую программу, код которой я приводить не буду, потому что это в данном случае не нужно. Программа состоит из трёх файлов java: ValuesGenerator.java, Statistic.java и Main.java. В ValuesGenerator.java описан метод для генерации случайного набора данных, в Statistic.java методы для нахождения минимального, среднего и максимального значений массива, а в Main.java находится метод main(), в котором происходит вызов методов классов ValuesGenerator и Statistic. Класс Statistic использует для рассчета значений библиотеку Apache Commons Math, а вся работа программы пишется в лог-файл с помощью библиотеки log4j. Также в каталоге с проектом есть небольшой файл readme.txt с описанием программы.

Задача заключается в том, чтобы иметь возможность автоматически создавать zip-файл с дистрибутивом программы.

Сразу привожу содержание файла свойств и файла build.xml c подробными комментариями, а потом опишу некоторые моменты подробно.

Файл build.properties
# имя каталога с дополнительными библиотеками
lib.dir=lib
# каталог проекта
project.dir=project
# каталог с исходниками проекта
project.src.dir=${project.dir}/src
# каталог для сборки
build.dir=build
# имя для jar-файла
build.jar.name=myprogram.jar
# имя главного класса
build.jar.mainclass=eqlbin.tutorials.ant.Main
# имя проекта
dist.name=myprogram
# каталог для создания дистрибутива
dist.dir=distribution
# имя каталога с библиотеками в дистрибутиве
dist.lib.dir=lib

Файл build.xml

На первый взгляд листинг build.xml может показаться сложным и громоздким. На самом деле, если потратить немного времени и внимательно просмотреть весь код, почитав комментарии, всё должно стать понятно. Ну а громоздкое оно, потому что XML, от этого никуда не деться. Рассмотрим теперь поподробнее приведённый пример.

Цели make.build.dirs и make.dist.dirs вам должны быть уже предельно понятны. Они не делают ничего умного, а только создают каталоги. Единственное, что можно отметить, в данном случае совсем не обязательно было прописывать в них по две задачи mkdir, а можно было бы обойтись одной, т.к. mkdir может создавать целую цепочку каталогов сразу.

Цель compile содержит задания, необходимые для компиляции исходного кода. Перед вызовом компилятора происходит создание элемента path, в котором будут содержаться пути к файлам библиотек, используемых в нашей программе. Файлы определяются с помощью элемента fileset, помимо которого в path могли бы содержаться и другие элементы коллекции ресурсов, например, dirset, содержащий пути к каталогам со скомпилированными java-классами или другой элемент path. Т.к. созданный элемент path представляет из себя необходимый для компиляции CLASSPATH, ему устанавливается соответствующий атрибут id, для того чтобы позже можно было на это path сослаться. После создания path, его текстовое представление записывается в свойство с помощью элемента pathconvert и выводится на экран. Цель compile указана в элементе project, как цель по умолчанию, поэтому если ant'у не указывать цель для выполнения, он будет выполнять именно её. Данная цель зависит от выполнения только одной цели make.build.dirs.

Цель dist.lib.copy тоже простая и осуществляет копирование всех jar-файлов, необходимых для запуска программы в каталог дистрибутива. В качестве источника файлов используется созданный ранее path. Атрибут flatten="true" в элементе copy отсекает всю информацию о пути к копируемым файлам и оставляет только их имя. Если его не использовать или указать равным false, то при копировании ant будет создавать всё дерево каталогов в каталоге todir, а т.к. в path у нас используются абсолютные пути, которые начинаются с корня (т.е. с символа "/"), то копирование вообще не выполнется. Это важный момент, который иногда является источником ошибок.

Цель make.jar занимается упаковыванием всех скомпилированных java-классов в исполняемый JAR. Для того чтобы создать првильно выполняющийся JAR, необходимо в первую очередь наличие файлов скомпилированных классов и система каталогов в которой также должны находиться файлы используемых программой библиотек. По этой причине цель make.jar зависит от целей compile и dist.lib.copy. Также для создания исполнимого JAR'а необходимо сгенерировать специальный файл MANIFEST.MF, в котором должны быть указаны имя главного класса, содержащего метод main() и CLASSPATH, благодаря которому программа сможет найти все необходимые ей библиотеки. У нас, на момент выполнения цели make.jar, уже есть элемент path, содержащий все пути до файлов используемых библиотек. Проблема в том, что эти пути абсолютные и указывают на конкретное место. Нам для распространения нужно чтобы пути были относительными и указывали на положение файлов библиотек, которые были скопированы с помощью цели dist.lib.copy в каталог с программой. Кроме этого, символом разделителя путей в манифесте, должен быть пробел, а не ":" или ";". Поэтому, с помощью элемента pathconvert происходит преобразование значения CLASSPATH, использовавшегося для компиляции, в значение пригодное для записи в манифесте. Элемент chainedmapper определяет цепочку преобразований, которые необходимо произвести с элементом path. В этой цепочке находится два действия: flattenmapper и globmapper. Эти действия совершают удаление информации о пути к файлам определенным в path, оставляя только имя файла, и добавляют перед именами файлов относительный путь к каталогу, содержащему эти библиотеки в рамках дистрибутива. Полученный результат преобразования сохраняется в свойстве dist.classpath и выводится на экран. Затем элемент jar используется для создания java-архива. Внутри этого элемента описывается манифест и набор скомпилированных классов для упаковки. В манифесте указываются атрибуты, содержащие имя главного класса и преобразованный CLASSPATH, а также имя того, кто создал этот JAR.

Цель javadoc осуществляет генерацию документации javadoc. Её подробно я описывать не буду, просто привел для демонстрации.

Конечной целью является dist. Её легко понять просто посмотрев на код. В ней происходит копирование файла с описанием программы в каталог с дистрибутивом и создание zip-архива из этого каталога.

Цель clean также проста до безобразия и просто производит удаление результатов предыдущих сборок.

Заключение

Хотел написать небольшую статью об основах Ant, а получилось так много букв, что вряд ли кто-то будет читать это до конца :) Зато, вроде бы, получилось очень доступно и подробно. Не смотря на объём, статья не является чем-то большим, чем введение в Ant, но её более чем достаточно для начала работы с этим замечательным инструментом. Более подробно обо всех возможностях Ant можно узнать из документации на официальном сайте проекта.

12 комментариев:

  1. спасибо за статью, нашел поиском по "использование apache ant"

    ОтветитьУдалить
    Ответы
    1. Пожалуйста. Рад, что она оказалась для вас полезной.

      Удалить
  2. В строке 80 забыли закрывающую фигурную скобку

    ОтветитьУдалить
  3. Анонимный14 мая 2014 г., 02:27

    Благодарю Вас за отличную статью! Очень легко читается, без лишнего, и все понятно! Еще раз благодарю!

    ОтветитьУдалить
  4. Анонимный20 мая 2014 г., 21:57

    Дякую за гарну статтю!

    ОтветитьУдалить
  5. Мерси за текст. Как раз искал про property файл )
    только вот в хроме не виден текст xml файла, пришлось в дев-консоль лезть и вытаскивать оттуда исходный код.

    ОтветитьУдалить
  6. Здравствуйте спасибо за содержательную статью. Подскажите пожалуйста как выполнить такое задание: Файл Файл сборки обновляет документацию, упаковывает ее в zip в названии, которого указано время создания. Спасибо за внимание.

    ОтветитьУдалить
    Ответы
    1. Пожалуйста, конечно, но поиском надо бы всё-таки научиться пользоваться прежде, чем Ant осваивать:
      build.xml to set date and time as file name

      Удалить
  7. Отлично указано, как использовать слово make на сайте http://preply.com/blog/2014/11/10/9-variantov-frazovogo-glagola-make-v-anglijskom-yazyke/

    ОтветитьУдалить