Понимание Градла ́

Written by elwood

Когда я сталкиваюсь с новым языком, мне очень важно знать, что означает та или иная запись, как её правильно разбирать мозгом. Без этого не достичь понимания языка, всё время придётся копаться и шаманить. В случае с градло ́м это недопустимо, так как тыкания и шаманство могут занимать очень уж много времени. И в конце концов всё же придется плюнуть и досконально разобраться с тем, как работают сценарии сборки. Что со мной и произошло. Целый день я провёл в документации, на сетевых местах типа переполнения стека и в открытых источниках программы градл. Вот краткий пересказ усвоенного.

Итак, основной вопрос градла звучит так: что делает этот кусок кода ? То, как отвечать на этот вопрос, мы и рассмотрим.

База

Для начала чуть-чуть перескажу официальную документацию по DSL. Каждый сценарий суть код конфигурации некоторого объекта. Глобально есть три вида сценариев: Build script, Init script и Settings script. И конфигурируют они соответственно объекты типа Project, Gradle и Settings.

То есть на входе обычного build.gradle мы имеем экземпляр класса Project. У него есть методы и свойства, которые мы можем вызывать. Например, так:

version = '1.0' // Эквивалентно setVersion('1.0)

Или так:

repositories {
    mavenCentral()
}

Что происходит здесь ? Здесь вызывается метод Project.repositories(Closure configureClosure), в качестве Closure передаётся блок кода в фигурных скобках. Но этот блок вызывается уже не в контексте экземпляра Project, а в контексте экземпляра класса RepositoryHandler (это описано в документации к методу). Таким образом, этот блок конфигурирует RepositoryHandler. А у него есть метод mavenCentral(Map<String, ?> args), который добавляет центральный мейвеновский репозиторий.

В этом кусочке заключена основная идея градла – вы через пробел пишете Closure, который конфигурирует что-то. Внутри этого Closure вам доступны все методы и свойства конфигурируемого объекта. А для того, чтобы знать, с объектом какого класса вы имеете дело в конкретном блоке, нужно пользоваться DSL Reference и JavaDocs.

Но кроме этого есть ещё некий дополнительный синтаксис, который определяется специально. Например здесь мы видим, что после task идет hello, хотя, судя по сигнатуре метода, здесь должна быть строка. К сожалению, в JavaDocs это не описано, и нам нужно смотреть DSL Reference.

Погружение

enter image description here

Разберём более сложный пример – сборку WAR с включённой фильтрацией XML-ресурсов. Если вы имели дело с ant или maven, то вы наверняка сталкивались с этой задачей. В градле она записывается так:

apply plugin: 'war'
 
war {
    filesMatching('WEB-INF/*.xml', { FileCopyDetails fileDetails ->
        logger.error("File filtered: " + fileDetails.path)
        filter(ReplaceTokens, tokens: [
                "host"    : project.property("host"),
                "port"    : project.property("port"),
                "username": project.property("username"),
                "password": project.property("password")
        ])
    })
}

Как это работает ? Смотрим документацию по плагину War. Она говорит о том, что при подключении плагина в project добавляется задача war типа War. К сожалению, не указано, что блок после war конфигурирует именно тип War, но из контекста примера можно об этом догадаться. В документации к классу находим метод filesMatching(String pattern, Action<? super FileCopyDetails> action). Action, как и Closure, Groovy позволяет нам определять блоком. Блок конфигурирует объект класса FileCopyDetails (обратите внимание на декларацию в начале блока – она позволяет нам оставить себе на будущее напоминание о том, с объектом какого типа мы работаем в этом блоке). Ну а у класса FileCopyDetails есть метод filter(Map<String,?> properties, Class<? extends FilterReader> filterType). На самом деле там три сигнатуры этого метода, и внимательный читатель заметит, что ни одна из сигнатур не подходит к тому, что мы написали. Здесь срабатывает хитрый алгоритм разрешения методов груви. Дело в том, что вторым параметром мы передали не мапу, а использовали синтаксис именованных параметров. А груви обрабатывает вызовы, в которых использованы именованные параметры, особым образом – выполняя объединение всех именованных параметров в одну мапу и пихая её в метод первым фактическим аргументом. А все остальные аргументы идут дальше (хотя записаны в вызове они могут быть первыми). Таким образом, вызов получается эквивалентен следующему:

filter([tokens: [
    "host"    : project.property("host"),
    "port"    : project.property("port"),
    "username": project.property("username"),
    "password": project.property("password")
]], ReplaceTokens)

Очень непривычно работать с такими вещами, но это груви, и придётся привыкать.

Откуда взялось свойство war ?

И снова внимательные телезрители в недоумении – откуда же взялось свойство war? Ведь нельзя же просто так добавить свойство к классу Project! Ответ мы найдём в документации к DSL :

A project has 5 property ‘scopes’, which it searches for properties. You can access these properties by name in your build file, or by calling the project’s Project.property() method. The scopes are:

The extensions added to the project by the plugins. Each extension is available as a read-only property with the same name as the extension.

Так что плагин просто добавил себя в дополнительный синтаксис, обеспечиваемый встроенным поведением класса Project. Чуть дальше мы видим

The extensions added to the project by the plugins. Each extension is available as a method which takes a closure or Action as a parameter.

Вот и ответ на наш вопрос. Мы можем писать war и потом блок для его конфигурации, что мы и сделали. Пример того, как плагин может себя зарегистрировать, можно найти в документации

Оффтоп: почему “градло”??

Локальный мемчик. Один из наших программистов невзлюбил мейвен и называл его “мавно”. Забавное название пошло в народ, и теперь мы не только обзываем мейвен, но и зовём ant антлом, а gradle – градлом.

Подводим итоги

Итак, тезисно:
– Каждый script block что-то конфигурирует
– Что именно – смотрим в DSL Reference и Java Docs
– Может встречаться дополнительный синтаксис (apply plugin:, task myTaskName), инфа по нему должна быть в DSL Reference
– Нужно что-то похакать / внедриться в процесс ? Смотрим документацию и джавадоки по плагину и таскам, шаманим, дергаем методы
– Знание Groovy желательно
– Документация, документация, документация, исходники, документация