Многомодульный Java-проект с Gradle. Шаг за шагом

в 7:24, , рубрики: Без рубрики

Очень много статей о Gradle написано. И со своей стороны хотелось бы добавить в копилку такую пошаговую инструкцию, прочтение которой, я надеюсь, позволит тем, кто плохо знаком с Gradle, “распробовать” и продолжить самостоятельно изучать этот инструмент.

Данная статья не будет подробно описывать такие темы, как плагины gradle (plugin), задачи (task), зависимости (dependencies), автоматическое тестирование и прочие прелести этого сборщика проектов. Во-первых, каждая тема заслуживает отдельной статьи или даже серии статей, а во-вторых, на эти темы уже есть статьи на хабре, например: Gradle: Tasks Are Code, Gradle: Better Way To Build. А еще на официальном сайте Gradle есть прекрасно написанный Gradle User Guide. Я же cфокусирую внимание на непосредственном решении поставленной задачи, и все сопутствующие темы будут описаны в рамках этой самой задачи.
Сначала определимся с целью, что же мы хотим получить на выходе? А цель указана в заголовке статьи. Мы хотим получить проект с несколькими модулями, который собирается с помощью Gradle. И так, приступим.

Шаг 1. Установка gradle

Примечение: Если выхотите просто “поиграть” с gradle, скачав файлы для статьи, или вам достались чужие исходники с волшебным файлом gradlew (gradlew.bat) в корне проекта, то устанавливать gradle не обязательно.

Gradle можно поставить, скачав последнюю версию со страницы загрузок Gradle или воспользовавшись менеджером пакетов в вашей любимой ОС (прим. Я ставил на Mac OS через brew и на Debian через apt-get из стандартных источников)

Результат первого шага:

$ gradle -version

------------------------------------------------------------
Gradle 1.11
------------------------------------------------------------

Build time:   2014-02-11 11:34:39 UTC
Build number: none
Revision:     a831fa866d46cbee94e61a09af15f9dd95987421

Groovy:       1.8.6
Ant:          Apache Ant(TM) version 1.9.2 compiled on July 8 2013
Ivy:          2.2.0
JVM:          1.8.0_05 (Oracle Corporation 25.5-b02)
OS:           Mac OS X 10.9.3 x86_64
Шаг 2. Пустой проект, плагины (plugin), обертка (wrapper)

Создадим папку проекта и в ее корне сделаем файл build.gradle со следующим содержимым:

{project_path}/build.gralde

apply plugin: “java”
apply plugin: “application”

task wrapper(type: Wrapper) {
    gradleVersion = '1.12'
}

Давайте, рассмотрим подробнее, что мы написали в файле. Тут используется динамический язык Groovy. Использование полноценного языка программирования в gradle дает большую свободу в сравнении со сборщиками пакетов, использующих декларативные языки.
В этом файле мы подключаем плагины java и application. Плагин java содержит в себе такие полезные задачи, как jar — собрать jar архив, compileJava — скомпилировать исходные коды и др. Подробнее о плагине можно почитать тут. Плагин application содержит в себе задачи: run — запуск приложения; installApp — установка приложения на компьютер, эта задача создает исполняемые файлы для *nix и для windows (bat файл); distZip — собирает приложение в zip архив, помещая туда все jar файлы, а также специфические для операционной системы скрипты. Подробнее о плагине в документации.
Теперь остановимся подробней на задаче wrapper. Эта очень полезная задача, наверное, самое гениальное решение, призванное облегчить жизнь программистам. Выполнив $ gradle wrapper, получим следующий результат:

$ gradle wrapper
:wrapper

BUILD SUCCESSFUL

Total time: 7.991 secs

$ ls -a
.          ..          .gradle          build.gradle     gradle          gradlew          gradlew.bat

Мы видим, что скрипт создал нам исполняемые файлы gradlew для *nix, gradlew.bat для Windows, а также папки gradle и .gradle. Скрытую папку .gradle можно не включать в репозиторий, там содержатся библиотеки зависимостей. Все основное лежит в gradle и в самом файле gradlew. Теперь мы смело может отдавать наш проект любому человеку, имеющему jdk нужной версии, и он самостоятельно сможет скомпилировать, собрать, установить проект, используя ./gradlew. Заметьте, что моя версия gradle (см. результат команды $ gradle -version выше) отличается от той, которую я указал в файле build.gradle, но это не страшно, поскольку после запуска задачи wrapper, мы получим необходимую версию gradle.

$ ./gradlew -version

------------------------------------------------------------
Gradle 1.12
------------------------------------------------------------

Build time:   2014-04-29 09:24:31 UTC
Build number: none
Revision:     a831fa866d46cbee94e61a09af15f9dd95987421

Groovy:       1.8.6
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
Ivy:          2.2.0
JVM:          1.8.0_05 (Oracle Corporation 25.5-b02)
OS:           Mac OS X 10.9.3 x86_64

Теперь вместо gradle можно смело использовать gradlew. Кстати, выполнение команды $ ./gradlew без параметров создаст папку .gralde и скачает туда все зависимые библиотеки (о зависимостях ниже). Но выполнение этой команды не обязательно, так как при любом запуске gradle (gradlew), будут проверяться зависимости и скачиваться недостающие файлы. Поэтому, получив проект, в котором лежат файлы gradlew, можно сразу запускать нужную задачу, список которых можно получить по команде ./gradlew tasks

Итоги второго шага (вывод сокращен):

$ ./gradlew tasks
:tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Application tasks
-----------------
distTar - Bundles the project as a JVM application with libs and OS specific scripts.
distZip - Bundles the project as a JVM application with libs and OS specific scripts.
installApp - Installs the project as a JVM application along with libs and OS specific scripts.
run - Runs this project as a JVM application

...

Other tasks
-----------
wrapper

...

To see all tasks and more detail, run with --all.

BUILD SUCCESSFUL

Total time: 7.808 secs
Шаг 3. Заполняем пробелы

На данном этапе мы уже можем выполнять несколько задач gradle. Мы можем даже собрать jar файл, но ничего кроме пустого манифеста там не будет. Настало время написать код. Gradle использует по умолчанию такую же структуру каталогов, что и Maven, а именно

src
    -main
        -java
        -resources
    -test
        -java
        -resources

main/java — это java-файлы нашей программы, main/resources — это остальные файлы (*.properties, *.xml, *.img и прочие). В test находятся файлы необходимые для тестирования.
Поскольку тестирование в этой статье рассматриваться не будет, обойдемся созданием папки src/main со всеми вложенными и приступим к созданию нашего приложения. А приложение — это Hello World, в котором будем использовать библиотеку Log4j. Как раз и разберемся, как в gradle работают зависимости. Внесем изменения в файл build.gradle, создадим файл com/example/Main.java с главным классом приложения в папке src/main/java, а также файл с настройками Log4j src/main/resources/log4j.xml. И файл gradle.properties (не обязательно, подробности ниже)

{project_path}/build.gradle

apply plugin: "java"
apply plugin: "application"

mainClassName = "com.example.Main"

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

repositories {
	mavenCentral()
}

dependencies {
	compile "log4j:log4j:1.2.17"
}

jar {
	manifest.attributes("Main-Class": mainClassName);
}

task wrapper(type: Wrapper) {
	gradleVersion = "1.12"
}

{project_path}/gradle.properties

org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk1.7.0_55.jdk/Contents/Home/

{project_path}/src/main/java/com/example/Main.java

package com.example;

import org.apache.log4j.Logger;

public class Main {
	private static final Logger LOG = Logger.getLogger(Main.class);

	public static void main(String[] args) {
		LOG.info("Application started");
		System.out.println("I'm the main project");
		LOG.info("Application finished");
	}
}

{project_path}/src/main/resources/log4j.xml

<?xml version="1.0" encoding="UTF-8" ?> 
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> 
 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> 
<appender name="console" class="org.apache.log4j.ConsoleAppender"> 
	<param name="Target" value="System.out"/> 
	<layout class="org.apache.log4j.PatternLayout"> 
		<param name="ConversionPattern" value="%p %c: %m%n"/> 
	</layout> 
</appender>     

<root> 
	<priority value ="debug" /> 
	<appender-ref ref="console" /> 
</root> 
</log4j:configuration>

Рассмотрим изменения в файле build.gradle. Мы добавили переменную mainClassName. Она указывает главный класс нашего приложения и используется плагином application в задаче run. Именно этот класс будет запущен. Также мы добавили переменные sourceCompatibility и targetCompatibility, присвоив им значение JavaVersion.VERSION_1_7. Это переменные из плагина java, показывают, какая версия jdk нам нужна при сборке. Следующий блок — repositories. В этом блоке мы подключаем репозиторий Maven. Gradle прекрасно с ним “дружит”. Блок dependencies содержит зависимости нашего приложения. Тонкости настройки смотрим в документации. Здесь мы указываем, что для задачи compile необходимо наличие log4j. В примере указан сокращенный синтаксис. Можно написать развернутый вариант и выглядеть он будет так:

complie group: 'log4j', name: 'log4j', version: '1.2.17'

Для сравнения аналогичный блок в maven:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

Также можно настраивать зависимости от файлов compile files('libs/a.jar', 'libs/b.jar') и от подпроектов compile project(':library_project').
Последнее добавление в build.gradle — это блок jar. Он также относится к плагину java. Содержит в себе дополнительную информацию для сборки jar-файла. В данном случае мы добавляем в манифест главный класс, воспользовавшись объявленной выше переменной mainClassName.
Далее необязательный файл gradle.properties. Описание этого файла разбросано по всей докментации, немного находится здесь и здесь. В данном случае мы фактически переопределяем переменную JAVA_HOME. Это актуально, когда у вас несколько версий jdk, как в моем случае, вы могли обратить внимание в начале статьи, $ gradle -version показывает, что моя версия JVM: 1.8.0_05 (Oracle Corporation 25.5-b02).
Я думаю, подробно останавливаться на файлах src/main/java/Main.java и src/main/resources/log4j.xml не имеет смысла, так как все предельно просто. Отправляем два сообщения в Logger, сообщение «I'm the main project» выводим в консоль. В файле настроек log4j написано, что наш logger будет выводить сообщения также в консоль.

Итоги третьего шага:

$ ./gradlew run
:compileJava
Download http://repo1.maven.org/maven2/log4j/log4j/1.2.17/log4j-1.2.17.jar
:processResources
:classes
:run
INFO com.example.Main: Application started
I'm the main project
INFO com.example.Main: Application finished

BUILD SUCCESSFUL

Total time: 14.627 secs

Видно, что скачивается недостающая библиотека, и продемонстрировано ее использование.

Шаг 4. Достижение цели

У нас уже есть проект, который работает, собирается и запускается через gradle. Осталось доделать самую малость: реализовать многомодульность, заявленную в заголовке статьи, или multi-project, если пользоваться терминологией gradle. Создадим две директории в корне проекта: main_project и library_project. Теперь переместим папку src и файл build.gradle в только что созданную директорию main_project, и создадим в корне новый файл settings.gradle с таким содержимым (об этом файле подробнее тут):

{project_path}/settings.gradle

rootProject.name = 'Gradle_Multiproject'

include 'main_project'

В этом файле мы говорим, как называется наш проект и какие папки подключать (фактически самостоятельные gradle проекты). На данном этапе нам нужна одна папка main_project. После таких изменений мы можем выполнить $ ./gradlew run или с указанием конкретного подпроекта $ ./gradlew main_project:run, и получим тот же результат, что и в конце шага 3. То есть работающий проект. Также можем выполнять все прочие команды jar, build, installApp и так далее. Gradle, если не указывать конкретного подпроекта, будет запускать задачу во всех подключенных подпроектах, у которых эта задача есть (например, если плагин application подключен только к одному подпроекту, у нас это будет main_project, команда $ ./gradlew run запустит run только этого подпроекта)
Теперь создадим код в нашем library_project. Создаем build.gradle и src/main/java/com/example/library/Simple.java

{project_path}/library_project/build.gradle

apply plugin: "java"

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

{project_path}/library_project/src/main/java/com/example/library/Simple.java

package com.example.library;

public class Simple {
	private int value;

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
} 

build.gradle для этого подпроекта намного проще. Используем плагин java и выставляем переменные с версией JDK. В рамках данной статьи этого достаточно. Теперь мы хотим, чтобы gradle узнал о подпроeкте library_project, опишем это в файле settings.gradle:

{project_path}/settings.gradle

rootProject.name = 'Gradle_Multiproject'

include 'main_project', 'library_project'

Теперь мы может собрать jar файл, содержащий нашу библиотеку, командой $ ./gradlew library_project:jar.

$ ./gradlew library_project:jar
:library_project:compileJava
:library_project:processResources UP-TO-DATE
:library_project:classes
:library_project:jar

BUILD SUCCESSFUL

Total time: 10.061 secs

Полученный файл можно найти по адресу: {project_path}/library_project/build/libs/library_project.jar.
А теперь давайте добавим использование класса Simple в main_project. Для этого нужно в файл {project_path}/main_project/build.gradle добавить строчку compile project(":library_project") в блок dependencies, которая сообщает, что для выполнения задачи compile в этом модуле нужен проект library_project.

{project_path}/main_project/build.gradle (блок dependencies)

dependencies {
	compile "log4j:log4j:1.2.17"
	compile project(":library_project")
}

{project_path}/main_project/src/main/java/com/example/Main.java

package com.example;

import org.apache.log4j.Logger;
import com.example.library.Simple;

public class Main {
	private static final Logger LOG = Logger.getLogger(Main.class);

	public static void main(String[] args) {
		LOG.info("Application started");
		System.out.println("I'm the main project");
		Simple simple = new Simple();
		simple.setValue(10);
		System.out.println("Value from Simple: " + simple.getValue());
		LOG.info("Application finished");
	}
}

Можно проверять.

Итог четвертого шага:

$ ./gradlew run
:library_project:compileJava UP-TO-DATE
:library_project:processResources UP-TO-DATE
:library_project:classes UP-TO-DATE
:library_project:jar UP-TO-DATE
:main_project:compileJava
:main_project:processResources UP-TO-DATE
:main_project:classes
:main_project:run
INFO com.example.Main: Application started
I'm the main project
Value from Simple: 10
INFO com.example.Main: Application finished

BUILD SUCCESSFUL

Total time: 11.022 secs
Шаг 5 (заключительный). Убираем мусор

Основная цель достигнута, но на данном этапе могли возникнуть вполне закономерные вопросы о дублировании информации в build файлах, более глубокой настройке gradle, а также о том, что изучать дальше. Для самостоятельного изучения, я советую ознакомиться с содержимым ссылок в конце статьи. А пока, давайте приведем в порядок наши build файлы, создав build.gradle в корне проекта и изменив содержимое остальных build файлов

{project_path}/build.gradle

apply plugin: "idea"
apply plugin: "eclipse"

subprojects {
    apply plugin: "java"

    tasks.withType(JavaCompile) {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }

    repositories {
        mavenCentral()
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = "1.12"
}

{project_path}/main_project/build.gradle

apply plugin: "application"

version = '1.0'

mainClassName = "com.example.Main"

dependencies {
	compile "log4j:log4j:1.2.17"
	compile project(":library_project")
}

jar {
	manifest.attributes("Main-Class": mainClassName);
}

{project_path}/build.gradle

version = "1.1_beta"

В корневом build.gradle мы будем хранить то, что относится ко всем проектам (на самом деле, можно хранить вообще все настройки, но я предпочитаю разделять большие файлы) и то, что не нужно в подпроектах, например, wrapper нам нужен только один, в корне.
В блок subprojects мы помещаем настройки подпроектов, а именно: подключаем плагин java — он нужен всем; выставляем версию jdk; подключаем maven-репозиторий. Также в этом файле мы подключаем плагины idea и eclipse. Эти плагины содержат задачи для генерации файлов проектов для соответствующих IDE. И сюда же переносим задачу wrapper. Она нужна только в корне, чтобы создать общие для всех файлы gradlew.
В подпроектах мы убрали все лишнее и добавили переменную version. Значение этой переменной будет добавляться к jar файлам, например, вместо library_project.jar теперь будет library_project-1.1.beta.jar.
Помимо блока subprojects, можно использовать allprojects или project(':project_name'). Подробнее тут.

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

Спасибо за внимание.

Полезные ссылки

Исходники проекта, созданного в статье, на bitbucket (zip архив)
Gradle
Gradle User Guide
Apache Logging Services
Apache Maven
Groovy Language

Автор: evsan

Источник

Поделиться

* - обязательные к заполнению поля