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

  • Tutorial
Очень много статей о 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.

Дополнение от MiniM: В gradle символ ":" используется вместо "/" и для более ветвистой структуры ссылки на проект могут выглядеть так ":loaders:xml-loader"

{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, и побудила к более подробному изучению и последующему использованию в своих проектах этого инструмента.

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

Дополнения:
MiniM: В gradle символ ":" используется вместо "/" и для более ветвистой структуры ссылки на проект могут выглядеть так ":loaders:xml-loader"
leventov: c плагином idea есть проблемы. issues.gradle.org/browse/GRADLE-1041, в последнем комментарии есть решение.

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

Исходники проекта, созданного в статье, на bitbucket (zip архив)
Gradle
Gradle User Guide
Apache Logging Services
Apache Maven
Groovy Language
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 18

    +1
    Хотелось бы добавить немного к статье.

    1. В gradle символ ":" используется вместо "/" и для более ветвистой структуры ссылки на проект могут выглядеть так ":loaders:xml-loader"
    2. Если не ошибаюсь, в последних версиях таск wrapper можно не описывать в build.gradle
    3. Т.к. файлы gradlew и gradlew.bat расположены в корневой папке проекта, запускать сборку приходится используя "../gradlew". Либо можно использовать специальные скрипты (необходимо положить в папку прописанную в PATH): forums.gradle.org/gradle/topics/gradlew_scripts_in_gradle_bin_to_find_gradlew_scripts_upwards_within_project_space
      0
      Спасибо!

      1. Абсолютно согласен с дополнением, статью обновил.

      2. Да, таск wrapper можно не создавать, но я бы рекомендовал это делать. Почему? Потому что может возникнуть необходимость использовать разные версии gradle для разных проектов. И тут я не задумываюсь, выполнил gradle wrapper, а потом запускаю нужную мне версию через gradlew.

      3. Это очень полезно для тех, кто часто пользуется командной строкой для сборки. Лично для себя я в этом проблемы не вижу, потому что мне крайне редко приходится запускать задачи из консоли, в основном через IDE, а когда приходится использовать консоль, когда лень загружать тяжелую IDE или ее просто нет или вообще компьютер не мой, то написать лишнюю точку не трудно, а иногда и быстрее
      0
      Добавлю, что плагин idea генерирует устаревший формат файлов, поэтому подмодули цепляются криво или не цепляются. См. issues.gradle.org/browse/GRADLE-1041, в последнем комментарии есть решение
        0
        Я, кстати, вот чего не понимаю. 2-3 года назад еще понятно, но сейчас зачем вообще нужен этот плагин? имхо, идея очень даже хорошо импортит gradle project.
          0
          Ну у меня, например, проект собирается полчаса. Проще застрелиться, чем отдавать его на откуп Идее. И обычный gradle build из корня не работает, не уверен, что там это можно подпилить…
            0
            Если честно, не очень понял о чем Вы, раскройте мысль пожалуйста.

            На всякий случай, я говорил исключительно про альтернативу «apply plugin: „idea“» vs «File -> Import project -> Gradle».
              0
              По правде говоря, я просто не пробовал так, потому что когда начинал проект, градл в Идее у меня почему-то не работал совсем, а сейчас все работает — смысла трогать нет. Иногда при старте Идея пробует сама собрать проект и быстро падает, но это меня не очень волнует.
            0
            13 Идея хорошо импортирует, но у меня возникали проблемы в 12 версии, какие, к сожалению вспомнить не могу. Idea вываливалась с каким-то исключением. Еще пробовал открывать в NetBeans. Он тоже хорошо понимает. А остальные IDE?
          0
          Спасибо за замечание, учтено.
          0
          Gradle отличная штука, но не без проблем.
          Например плагин application, не может корректно завершать свою работу.
          Поясню.
          Делаю проект (простенький пример на netty), запускаю его стандартно из IDEA.
          Хочу остановить его работу, подправить код и по новой запустить.
          В общем типичный юзкейс.
          Нажимаю кнопочку stop, как обычно. Правлю код, запускаю по новой… и фиг там.
          От предыдущего запуска в памяти висит процесс, который блочит ресурсы.
          Maven нормально прибивает за собой процессы, а гредл не умеет.
          При чем если это web приложение, то все ок, запуск например jettyRun корректно завершается.
            0
            В идее можно попробовать в настройках конфигурации запуска (Edit Configurations… в выпадающем списке рядом с кнопками Run и Debug) поставить галочку Single Instance only, тогда можно будет даже не останавливать процесс. При попытке запустить еще один, IDE предупредит и предложит остановить предыдущий процесс и запустить новый.
            Может, это решит проблему с «висячими» процессами.
              0
              Попробовал сейчас.
              Нет, не помогает. IDE предупреждает, предлагает остановить предыдущий процесс… и думая что остановила его запускает новый.
              А старый продолжает висеть.
                0
                Ни разу с таким не сталкивался, может быть дело в самом приложении?
                  0
                  Было бы приложение))
                  Там буквально несколько классов. Тестовый пример Netty.
                  При чем стопарнуть запущенное приложение не может ни нетбинс ни эклипс.
                  Походу проблема именно в самом гредле.
                    0
                    Видимо, плагин application не подходит для запуска netty-приложений.
                    Для jetty, war и даже javafx свои плагины, со своими тасками *run: jettyRun, jfxRun или без них (в плагине war)

                    Может, стоит написать issue команде gradle на их форуме с ошибками, логами, юзкейсом.
                      0
                      Скорее всего, дело все-таки в приложении))

                      Netty внутри запускает потоки-не-демоны. Остановить такую штуку принудительно можно либо через System.exit, либо kill -9. Первое gradle не может сделать, т.к. запускает приложение в отдельном процессе (да и если бы запускал в своей jvm, все равно нельзя, т.к. run может быть не листовой задачей в дереве). А за «kill -9» их бы просто сообщество повесило.

                      Так что ставьте на main-thread Runtime.getRuntime().addShutdownHook и тормозите Netty руками.
                        0
                        Не, похоже я таки нагнал. Тогда действительно непонятно, как работает этот плагин.

            Only users with full accounts can post comments. Log in, please.