Статья-туториал от ведущего Java-разработчика "ITQ Group" Константина Киселевича.

Дорогой Junior и все, кто занимается copy-past конфигов Gradle.

В этой статье я хочу простым языком рассказать о gradl'овой конфигурации сборки проекта, чтобы вы не боялись использовать Gradle.

Давайте начнем с того, что после xml'ного Maven'а, действительно, непонятно, что значит каждая строчка из следующего примера:

buildscript {
    ext {
        springBootVersion = '2.7.4'
        lombokVersion = '1.18.24'
        h2Version = '2.1.214'
        orikaCoreVersion = '1.5.4'
        queryDslVersion = '4.2.2'
        javaxVersion = '1.3.2'
        sonarqubeVersion = '3.0'
    }
}
 
plugins {
    id('java-library')
    id('org.springframework.boot').version("${springBootVersion}").apply(false)
    id('org.sonarqube').version("${sonarqubeVersion}").apply(false)
}
 
allprojects {
    repositories {
        mavenCentral()
    }
}
 
subprojects {
    apply(plugin: 'java-library')
    apply(plugin: 'org.sonarqube')
 
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(11)
        }
        withSourcesJar()
        withJavadocJar()
    }
 
    dependencies {
        api(platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
        compileOnly("org.projectlombok:lombok:${lombokVersion}")
        annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    }
}
 
dependencies {
    implementation(project('deveducate-web'))
}

Почему один dependencies {} вложен в subprojects {}, а второй - не вложен. Что такое buildscript {} и toolchain {}. Вопросов много, ответы в статье, но самый главный вопрос - что за программа этот текст читает и интерпретирует ?

Groovy

Давайте начнем с последнего вопроса и немного углубимся в историю.

Где-то в середине нулевых у программистов Java появились вопросы к языку - почему же так медленно появляются новые фичи? Действительно, посмотрим историю версий Java:

  • J2SE 5.0 сентябрь 2004

  • Java 6 декабрь 2006

  • Java 7 июль 2011

  • Java 8 март 2014

За 10 лет всего 4 версии. Прямо скажем, не так и много, как хотелось.

Ответом на такое медленное развитие стал язык Groovy. Его девиз: release early, release often (внедряй фичи раньше и чаще других).

Некоторые фичи Groovy перекочевали в другие языки:

  • синтаксис для оператора Элвиса "?:" (теперь в PHP и Kotlin);

  • оператор безопасной навигации "?." (теперь в C#, Swift, Kotlin и Ruby - как &.);

  • оператор <=> (теперь в PHP и Ruby).

Как называется программа ?

Программа называется Gradle. Скачать ее вы можете с сайта по ссылке.

Установка Gradle.

На выбор доступны два варианта работы с Gradle в проекте:

  • через файл gradle;

  • через файл gradlew, где w означает wrapper (обертка).

В чем разница ?

Разница в удобстве для ваших коллег. В случае использования обертки gradlew версия gradle будет одинаковой у всей команды, каждому из коллег не придется вручную качать дистрибутив gradle с сайта и проделывать манипуляции с распаковкой.

Итак.

Представим, что на вас возложили инициативу начать писать код проекта и использовать gradle в качестве сборщика проекта. Это большое доверие :)
Выберем директорией проекта C:\doit.

Поскольку вы первый в команде, вам необходимо самостоятельно скачать Gradle. Зайти на сайт по ссылке, скачать релиз и распаковать в какую-нибудь временную директорию вне директории проекта, например, C:\tmp.

Примерное содержание директории должно быть таким:

/bin
/init.d
/lib
README

Нужный нам файл gradle находится в директории /bin.

Вы бережете время коллег, которым предстоит вместе с вами разрабатывать проект, и не хотите заставлять их так же вручную качать более 100мб дистрибутива Gradle и повторять ваши шаги.

Поэтому вы откроете терминал (cmd), зайдете в директорию проекта cd C:\doit и запустите C:\tmp\bin\gradle init.

В предложенных вариантах создания gradle проекта выберете basic. Это пустой проект.

В директории проекта C:\doit появились самые необходимые файлы: gradlew, settings.gradle и build.gradle и даже .gitignore.

gradle init
gradle init

В C:\doit\gradle\wrapper\gradle-wrapper.properties указана версия gradle:

distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip

Если вы добавите сгенерированные файлы в git, то у всех коллег будет одна и та же версия gradle и дистрибутив автоматически скачается при выполнении любой команды, например,

gradlew help

Таким образом, обертка gradle wrapper позволит всей команде вести разработку под одинаковой версией gradle и сама скачает/распакует дистрибутив в домашнюю директорию пользователя: ~\.gradle\wrapper\dists.

Gradle написан на Groovy + Kotlin.

Выше мы поговорили о скорости развития Groovy. Это важно, но существует еще одна причина в пользу языка.

Почему же Groovy был выбран для написания сценариев сборки проектов?

Ответ заключается в DSL. Groovy позволяет легко создавать новые специфические языки программирования под нужную нам задачу.

В Groovy для создания DSL реализуется концепция Метапрограммирования (по ссылке с диаграммой исполнения missingMethod).

Один из классов этой концепции - DelegatingScript:

Посмотрите пример и вы все поймете. Документация.

Cобственный язык (DSL)

Пример абстрактного файла для нашего выдуманного скриптового языка my.best.dsl.

	foo(1,2) {
	   // код метода
	}
	 
	bar = "Hello world!";

А вот код класса на Groovy, который читает, парсит и выполняет скрипт my.best.dsl:

	/** 
     * класс с методом foo() и сеттером setBar()
     */
	class MyDSL {
		public void foo(int x, int y, Closure z) { ... }
		public void setBar(String a) { ... }
	}

	CompilerConfiguration cc = new CompilerConfiguration();
	cc.setScriptBaseClass(DelegatingScript.class.getName());
	GroovyShell sh = new GroovyShell(cl,new Binding(),cc);

	DelegatingScript script = (DelegatingScript)sh.parse(new File("my.best.dsl"))
	 
	script.setDelegate(new MyDSL());
	script.run();

DelegatingScript парсит наш выдуманный язык, на котором мы написали скрипт my.best.dsl.

Но у DelegatingScript нет метода foo или setBar. Поэтому мы подсказываем, что исполнение методов необходимо делегировать объекту new MyDSL() (в строке script.setDelegate(new MyDSL())). То есть вызвать эти методы у объекта new MyDSL()

Заметьте, что метод foo последним аргументом имеет замыкание Closure.

public void foo(int x, int y, Closure z) { ... }

А в нашем придуманном языке вызов метода следующий:

foo(1,2) {
   // код метода 
}

Вот так в Groovy можно передавать аргументы: 1, 2 в скобках () и замыкание вне скобок ()если аргумент типа Closure последний в списке аргументов.

Надеюсь, теперь стало чуть понятнее, что такое в build.gradle

dependencies { 
   implementation(project('deveducate-web'))
}

dependencies(Closure c) - это метод с одним аргументом - функцией, тело которой содержит вызов метода implementation(...).

Согласитесь, насколько легко реализовать свой скриптовый язык при помощи Groovy.

Создатели сборщика проектов Gradle так же оценили эту возможность и придумали свой довольно простой язык.

Он действительно несложный. В нем, по сути, всего три главных метода: plugins(), buildScript(), task(). Аргументами этих методов являются функции, тело которых вы пишете в фигурных скобках { ... }.

Чуть-чуть подробнее. В примере выше методы foo и setBar принадлежали классу MyDSL. А какому классу/интерфейсу принадлежат plugins и buildScript ?

Они принадлежат интерфейсу org.gradle.api.Project: https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#buildscript-groovy.lang.Closure-
(Реализует же этот интерфейс класс org.gradle.api.internal.project.DefaultProject. Рекомендую клонировать сорцы gradle https://github.com/gradle/gradle и покопаться в них)

Прокрутите вверх интерфейс Project, указанный в ссылке выше. Посмотрите, сколько еще методов вы можете использовать в своем build.gradle. Документация по методам Gradle доступна на https://docs.gradle.org/current/dsl/index.html

В общем, если в build.gradle вам непонятно какое-то ключевое слово, открывайте интерфейс Project и ищите это ключевое слово там. Если не найдете, то, скорее всего, это название метода/свойства из подключенного вами плагина.

Про плагины Gradle. Зачем они нужны?

Можно, например, в build.gradle вручную на языке Groovyнаписать скрипт сборки jar, прописать директории, в которых ожидается исходный код, запустить команду компиляции, команду тестов и так далее.

Но, к счастью, все это уже написано за нас и упаковано в плагин java-library, который поставляется вместе с Gradle. (Плагин java-library расширяет возможности плагина java, наследует его возможности)

Чистый Gradle без плагинов позволяет запускать следующие задачи:

	c:\doit>gradlew tasks

	> Task :tasks

	------------------------------------------------------------
	Tasks runnable from root project 'doit'
	------------------------------------------------------------

	Build Setup tasks
	-----------------
	init - Initializes a new Gradle build.
	wrapper - Generates Gradle wrapper files.

	Help tasks
	----------
	buildEnvironment - Displays all buildscript dependencies declared in root project 'doit'.
	dependencies - Displays all dependencies declared in root project 'doit'.
	dependencyInsight - Displays the insight into a specific dependency in root project 'doit'.
	help - Displays a help message.
	javaToolchains - Displays the detected java toolchains.
	outgoingVariants - Displays the outgoing variants of root project 'doit'.
	projects - Displays the sub-projects of root project 'doit'.
	properties - Displays the properties of root project 'doit'.
	resolvableConfigurations - Displays the configurations that can be resolved in root project 'doit'.
	tasks - Displays the tasks runnable from root project 'doit'.

	To see all tasks and more detail, run gradlew tasks --all

	To see more detail about a task, run gradlew help --task <task>

	BUILD SUCCESSFUL in 1s
	1 actionable task: 1 executed

Давайте подключим плагин java-library в наш проект: в самом начале build.gradle пропишем:

plugins {
    id('java-library')
}

После подключения плагина появились новые таски для работы с проектом:

	assemble - Assembles the outputs of this project.
	build - Assembles and tests this project.
	buildDependents - Assembles and tests this project and all projects that depend on it.
	buildNeeded - Assembles and tests this project and all projects it depends on.
	check - Runs all checks.
	classes - Assembles main classes.
	clean - Deletes the build directory.
	compileJava - Compiles main Java source.
	compileTestJava - Compiles test Java source.
	jar - Assembles a jar archive containing the main classes.
	javadoc - Generates Javadoc API documentation for the main source code.
	processResources - Processes main resources.
	processTestResources - Processes test resources.
	test - Runs the test suite.
	testClasses - Assembles test classes.

Итак, подключили плагин. А что такое плагин - это класс, реализующий интерфейс Plugin<Project>. Пример плагина HelloPlugin:

class HelloPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.task('hello') {
            doLast {
                println 'Hello from the HelloPlugin'
            }
        }
    }
}

У объекта Project появился новый метод/task hello. Точно так же плагин Java-library добавляет новые методы/таски и многое другое (в том числе и соглашения, например, что исходный код ищется по пути src/main/java).

Смотрите документацию по Java-library на странице https://docs.gradle.org/current/userguide/building_java_projects.html и https://docs.gradle.org/current/userguide/java_library_plugin.html

Плагины Java и Java-library немного отличаются. Второй плагин наследует первый и добавляет, например к implementation() новый метод api().

Теперь вы можете указать в подпроекте implementation("org.apache.commons:commons-lang3:3.5"), если ни один тип или метод из commons-lang не станет частью публичного API.

Теперь, если версия commons-lang изменится на 3.6, то подпроекты, зависимые от текущего подпроекта, не будут перекомпилироваться.

И наоборот для зависимости в api().

Все это ускоряет сборку на больших проектах.

Пример!

Пришла пора создать маленький Java проект из одного класса, выводящего в консоль стандартное Hello world.

Очистим рабочую C:\doit от старых файлов. Запустим в командной строке C:\tmp\bin\gradle init, которая добавит Gradle Wrapper, а так же создаст build.gradle.

gradle init
gradle init

В C:\doit появился gradle wrapper.

после gradle init
после gradle init

Осталось добавить Java класс в C:\doit\src\main\java.

java класс с приветствием
java класс с приветствием

Класс HelloWorld:

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

И подключить плагин java-library в build.gradle, как делали выше.

plugins {
    id('java-library')
}

Неплохо, уже можно и собрать наше маленькое приложение. Вспомним про документацию к Java-library https://docs.gradle.org/current/userguide/building_java_projects.html и https://docs.gradle.org/current/userguide/java_library_plugin.html или https://docs.gradle.org/current/dsl/. Поищем что-то по слову jar. Найдем таск jar.

jar - Собирает jar-архив с main классом.

Запускаем:

C:\doit>.\gradlew jar

Готово - появился C:\doit\build\libs\doit.jar, который можем запустить в командной строке через C:\doit>java -cp "C:\doit\build\libs\doit.jar" HelloWorld

Приходится указывать название класса при запуске jar'ника. Можно переместить название класса в файл манифеста MANIFEST.MF, согласно требованиям Java. Для этого откроем страницу building_java_projects или org.gradle.api.tasks.bundling.Jar и поищем по слову manifest. Находим пример:

jar {
    manifest {
        attributes("Implementation-Title": "Gradle",
                   "Implementation-Version": archiveVersion)
    }
}

Для запуска jar файла из командной строки без указания класса manifest должен содержать строку:

Main-Class: HelloWorld

Добавим в build.gradle:

jar {
    manifest {
        attributes 'Main-Class': 'HelloWorld'
        //или равнозначно
		//attributes('Main-Class': 'HelloWorld')
    }
}

Почему такая странная запись через двоеточие? Так представлен элемент HashMap в Groovy, как параметр метода https://docs.gradle.org/current/javadoc/org/gradle/api/java/archives/Manifest.html

пример документации Manifest
пример документации Manifest

Прочтите The Groovy Development Kit https://www.groovy-lang.org/groovy-dev-kit.html

c:\doit>.\gradlew jar
c:\doit>java -jar build\libs\doit.jar
Hello world!

Разобраться несложно. Главное - не копипастить, а проходить каждую строчку в чужих примерах по документации.

Рекомендую статьи:

Комментарии (8)


  1. engineit
    23.01.2023 13:29

    Спасибо. Полезная статья


  1. kovserg
    23.01.2023 18:19

    А как профилировать gradle и понять чем он там занимался 10минут?


    1. slonopotamus
      24.01.2023 10:07

      Точно так же как профилировать любое другое приложение на Java? И еще есть gradle build --scan


    1. YuNastasiya
      24.01.2023 14:02

      В статье https://developer.android.com/studio/build/profile-your-build рекомендуют использовать "gradle-profiler" или "gradlew --profile". Так же рекомендуют использовать Build Analyzer https://developer.android.com/studio/build/build-analyzer, по сути, графический вариант gradlew --profile


  1. Stingray42
    24.01.2023 11:31
    +6

    Я боюсь использовать gradle не из-за groovy dsl, а из-за того что каждый мажорный релиз в нем меняется формат конфигурации, и все гайды со stackoverflow перестают работать. Нельзя просто настроить и забыть, в отличие от великолепного во всех отношениях maven.


  1. Keirichs
    24.01.2023 14:03

    Большое спасибо, полезно


  1. Felan
    24.01.2023 18:46
    +1

    Отличная статья.
    А вот тут нет опечатки?


    "Теперь вы можете указать в подпроекте implementation("org.apache.commons:commons-lang3:3.5"), если не ни один тип или метод из commons-lang не станет частью публичного API. "


    1. YuNastasiya
      25.01.2023 13:17

      Спасибо! Исправлено.