Освой программирование играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Задачи Gradle

Задача (task) является основным компонентом процесса сборки в файле build.gradle. Задачи представляют собой именованные наборы инструкций, которые Gradle запускает, выполняя сборку приложения. Задачи Gradle являются полнофункциональными объектами, которыми вы можете управлять программно.

Объявление задач

Запустите любой ваш проект в Android Studio для дальнейших опытов. Откройте файл build.gradle, который относится к модулю app.

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


task hello_kitty

После любого изменения файла синхронизуруйтесь.

Мы объявили задачу по одному только имени. На правой стороне Android Studio имеется вертикальная вкладка Gradle, которую можно развернуть. Она содержит список задач (task), которая выполняет Gradle при работе с текущим проектом. Вы можете выделить любую из этих задач и запустить её двойным щелчком. Можно выделить несколько задач.

Найдите свою задачу, она будет находиться по пути :app | Tasks | other. Задачи сортируются по алфавиту. Запустите задачу двойным щелчком.

Когда выполняется какая-то задача Gradle, то ход её выполнения можно увидеть в окне Gradle Console. Открыть её можно через вкладку Gradle Console в нижней правой части студии. После выполнения задачи вы увидите что-то типа такого:


Executing tasks: [hello_kitty]

Configuration on demand is an incubating feature.
:app:hello_kitty UP-TO-DATE

BUILD SUCCESSFUL

Total time: 1.569 secs

Теперь, поняв, как создаются и запускаются задачи, продолжим эксперименты.

Операция задачи (Task Action)

Выполнение задачи не произведёт никакого результата, поскольку мы не присвоили ей ни одной операции (action). Операцию можно присвоить используя оператор сдвиг влево. Перепишем пример:


task hello_kitty << {
    println 'Hello Kitty'
}

Операторы, такие как << («сдвиг влево» из Java), могут быть перегружены в Groovy для изменения поведения в зависимости от объектов с которыми они работают. В данном случае << перегружен в Gradle для добавления блока кода в список операций, которые выполняет задача. Сдвиг влево является эквивалентом метода doLast(), который мы рассмотрим ниже.

Теперь у нас есть гибкая возможность добавления кода операции аддитивным способом, ссылаясь на объект задачи, который мы создали. Пример последовательного добавления операций задачи по одной:


task hello_kitty

hello_kitty << {
    print 'Hello '
}

hello_kitty << {
    println 'Kitty'
}

Сначала мы объявили задачу, затем добавили первую операцию с методом print, следом добавили вторую операцию с методом println. Результат будет таким же.

Конфигурация задачи

Расширим предыдущий пример, добавив блок конфигурации:


task hello_kitty

hello_kitty << {
    println 'Соединяемся с базой данных'
}

hello_kitty << {
    println 'Обновляем данные'
}

hello_kitty {
    println 'Настраиваем базу данных'
}

Запустив файл, мы получим результат, который покажется нам нелогичным:


Настраиваем базу данных
:app:hello_kitty
Соединяемся с базой данных
Обновляем данные

Для обозначения блока кода между парой фигурных скобок, в Groovy используется термин «замкнутое выражение» или «замыкание» (closure). Функции-замыкания подобны объектам, которые можно передавать методу как параметр или присваивать переменной, с возможностью последующего выполнения. Они будут повсеместно встречаться вам в Gradle, поскольку в высшей степени подходят в роли блоков, где можно определить конфигурационный код и код операций билда.

Последнее замкнутое выражение выглядит как очередной блок операции билда, и мы ожидаем что вывод его сообщения будет последним, но не первым. Оказывается замыкание, добавленное к имени задачи без оператора сдвиг влево совсем не добавляет новую операцию. Вместо этого добавился блок конфигурации. Конфигурационный блок задачи выполняется во время конфигурационной фазы жизненного цикла Gradle, которая предшествует фазе выполнения, во время которой выполняются операции задачи.

Каждый раз, когда Gradle запускает билд, процесс проходит через три фазы жизненного цикла: инициализация, конфигурация и выполнение. Выполнение — фаза, во время которой задачи билда выполняются в порядке, указанном в настройках их зависимостей. Конфигурация — фаза в которой объекты задачи собираются во внутреннюю объектную модель, обычно называемую направленным ациклическим графом. Инициализация — фаза, в которой Gradle принимает решение, какие объекты будут принимать участие в билде. Последняя фаза важна в многопроектных билдах.

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


task hello_kitty

hello_kitty << {
    println 'Соединяемся с базой данных'
}

hello_kitty << {
    println 'Обновляем данные'
}

hello_kitty {
    print 'Настраиваем '
}

hello_kitty {
    println 'базу данных'
}

Gradle, прежде чем выполнить билд, создаёт его внутреннюю объектную модель. Каждая задача, которую вы объявляете, в действительности, становится объектом-задачей в пределах всего проекта билда. У объекта-задачи, как и у любого другого объекта, есть свойства и методы. И ещё мы можем управлять типом каждого объекта-задачи, обращаясь к функциональности, определённой в нём.

По умолчанию, каждой новой задаче присваивается тип DefaultTask. Подобно тому, как каждый класс наследуется от Object в Java, в Gradle каждая задача наследуется от данного типа — даже те задачи, которые расширяют возможности DefaultTask путём создания нового типа. На самом деле, DefaultTask-задачи не делают ничего специфичного, вроде компиляции кода или копирования файлов. Однако они содержат функционал, который требуется для взаимодействия с программной моделью проекта Gradle. Рассмотрим методы и свойства, которые имеет каждая задача в Gradle.

Методы DefaultTask

dependsOn(task)

Для вызывающей задачи добавляет задачу-зависимость. Задача-зависимость всегда запускается перед задачей, которая от неё зависит. Метод можно вызывать несколькими способами. Пример кода ниже показывает, как мы можем определить зависимость задачи loadTestData от createSchema:


task createSchema

// Объявляем зависимость 'loadTestData' от 'createSchema'
// Остальные зависимости, определённые ранее, остаются неизменными
task loadTestData {
    dependsOn createSchema
}

// Альтернативный способ указания той же зависимости
task loadTestData {
    dependsOn << createSchema
}

// Делаем то же самое, используя одиночные кавычки (которые обычно не нужны)
task loadTestData {
    dependsOn 'createSchema'
}

// Явный вызов метода объекта-задачи
task loadTestData
loadTestData.dependsOn createSchema

// Краткая нотация для определения зависимостей
task loadTestData(dependsOn: createSchema)

Задача может зависеть от нескольких задач. Если задача loadTestData зависит от задач createSchema и compileTestClasses, мы пишем код следующим образом:


task compileTestClasses
task createSchema

// Объявление зависимостей по одной
task loadTestData {
    dependsOn << compileTestClasses
    dependsOn << createSchema
}

// Передаём зависимости, как список переменной длины
task loadTestData {
    dependsOn compileTestClasses, createSchema
}

// Явно вызываем метод объекта-задачи
task loadTestData
loadTestData.dependsOn compileTestClasses, createSchema

// Краткая нотация для определения зависимостей
// Обратите внимание на синтаксис списков Groovy
task loadTestData(dependsOn: [ compileTestClasses, createSchema ])

doFirst(closure)

Добавляет блок исполняемого кода в начало операции задачи. Во время фазы выполнения запускается блок операции каждой задачи, участвующей в билде. Метод doFirst позволяет вам добавлять части логики в начало существующей операции, даже если эта операция уже определена в файле билда или внешнем модуле (plug-in), к которому у вас нет доступа. Многократные вызовы doFirst добавляют новые блоки с кодом операций в начало последовательности выполнения задачи.

Метод doFirst можно вызывать напрямую для объекта-задачи, передавая ему замыкание, которое содержит код, который будет выполнен перед текущей операцией задачи.

Как мы уже говорили, замыкание — это блок Groovy кода, заключённый между парой фигурных скобок. Замыкание можно передавать методу как любой другой объект. Возможность передавать методам замкнутые выражения является стилевой особенностью Groovy.


task setupDatabaseTests << {
    // Здесь определена текущая операция задачи
    println 'load test data'
}

setupDatabaseTests.doFirst {
    println 'create schema'
}

Результат выполнения


:setupDatabaseTests
create schema
load test data

BUILD SUCCESSFUL

doFirst можно также вызывать в конфигурационном блоке задачи. Как мы уже говорили, конфигурационный блок — это часть исполняемого кода, которая запускается во время конфигурационной фазы билда, перед тем как будут выполнены операции задачи. Когда мы рассматривали выше конфигурацию задач, у вас мог возникнуть вопрос: где можно использовать конфигурационные блоки? Следующий пример покажет вам, как можно вызывать методы задачи внутри конфигурационного блока, что в перспективе делает очень выразительным синтаксис формата изменения поведения задачи.

Пример метода doFirst внутри конфигурационного блока задачи


task setupDatabaseTests << {
    println 'load test data'
}

setupDatabaseTests {
    doFirst {
        println 'create schema'
    }
}

Повторные вызовы doFirst аддитивны. Код операции каждого предыдущего вызова сохраняется, и новое замыкание добавляется в начало списка, готовое выполниться в соответствующем порядке. Например, если нам нужно настроить базу данных для интеграционного тестирования (разбив этапы настройки по частям), можно использовать следующий код:


task setupDatabaseTests << {
    println 'load test data'
}

setupDatabaseTests.doFirst {
    println 'create database schema'
}

setupDatabaseTests.doFirst {
    println 'drop database schema'
}

Результат выполнения предыдущего примера


:setupDatabaseTests
drop database schema
create database schema
load test data

BUILD SUCCESSFUL

В предыдущем примере разделение инициализации на три отдельных замыкания с вызовом doFirst() было, конечно же, несколько искусственным шагом. Однако, бывают случаи, когда исходный код задачи недоступен. Например, задача определена в другом файле билда, модифицировать который невозможно или непрактично. Рассматриваемый способ программной модификации недоступной логики открывает нам широкие возможности.

До сих пор в наших примерах использовался очень простой синтаксис, который раскрывает принципы работы Gradle за счёт многократных добавлений замыканий. Вероятнее всего, в реальном билде мы организуем задачу следующим образом (всё так же, вместо настоящих тестовых операций мы используем операторы println):

Пример. Повторные вызовы doFirst после рефакторинга


// Исходное определение задачи (может быть недоступно для редактирования)
task setupDatabaseTests << {
    println 'load test data'
}

// Наши изменения задачи (в файле, где мы можем вносить изменения)
setupDatabaseTests {
    doFirst {
        println 'create database schema'
    }
    doFirst {
        println 'drop database schema'
    }
}

Обратите внимание на то что мы собрали вместе несколько вызовов doFirst внутри одного конфигурационного блока, после того как начальная операция уже была добавлена в задачу setupDatabaseTests.

doLast(closure)

Метод doLast очень похож на метод doFirst, с той лишь разницей, что он добавляет поведение в конец операции, а не в начало. Если вам нужно запустить блок кода после того как некоторая задача закончит выполнение, вы можете поступить следующим образом:


task setupDatabaseTests << {
    println 'create database schema'
}

setupDatabaseTests.doLast {
    println 'load test data'
}

Повторные вызовы doLast аддитивны


task setupDatabaseTests << {
    println 'create database schema'
}

setupDatabaseTests.doLast {
    println 'load test data'
}

setupDatabaseTests.doLast {
    println 'update version table'
}

Как уже говорилось ранее, оператор << является ещё одним способом вызова метода doLast().

onlyIf(closure)

Метод onlyIf представляет собой предикат, который определяет, будет ли выполнена задача. Значением предиката считается значение, возвращаемое замыканием. При помощи данного метода вы можете деактивировать выполнение задачи, которая иначе запустится в обычном порядке запуска последовательности зависимостей билда.

В Groovy последнее выражение внутри замыкания определяет его возвращаемое значение, даже если отсутствует оператор return. Метод Groovy, в котором определено только одно выражение, является функцией, возвращающей значение этого выражения.

Пример, в котором используется метод onlyIf


task createSchema << {
    println 'create database schema'
}

task loadTestData(dependsOn: createSchema) << {
    println 'load test data'
}

loadTestData.onlyIf {
    System.properties['load.data'] == 'true'
}

Два варианта запуска файла билда. Обратите внимание на разницу в результатах


d:\project>gradle loadTestData
:createSchema
create database schema
:loadTestData SKIPPED

BUILD SUCCESSFUL

Total time: 4.361 secs
d:\project>gradle -Dload.data=true loadTestData
:createSchema
create database schema
:loadTestData
load test data

BUILD SUCCESSFUL

Total time: 2.005 secs

При помощи метода onlyIf вы можете включать и отключать отдельные задачи, используя логику, выражаемую Groovy-кодом, что не ограничиваться одной лишь проверкой простого свойства System, которое мы использовали в примере. У вас есть возможности открывать файлы для чтения, вызывать Веб-сервисы, проверять логины-пароли и делать многое другое, что можно делать в коде.

Свойства DefaultTask

didWork

Свойство типа boolean, указывающее, завершилась ли задача успешно. Не все задачи устанавливают значение didWork к моменту завершения. Однако некоторые задачи, такие как Compile, Copy и Delete, устанавливают значение данного свойства для передачи информации о том что их операции выполнены либо успешно, либо с ошибками. Вычисление значения, указывающего на то, что задача уже выполнилась, специфично для разных задач. Вы можете установить значение didWork в вашей задаче для отражения результатов выполнения созданного вами кода сборки:

Отправка электронного письма для случая, когда компиляция прошла успешно


apply plugin: 'java'

task emailMe(dependsOn: compileJava) << {
    if (tasks.compileJava.didWork) {
        println 'SEND E-MAIL ANNOUNCING SUCCESS'
    }
}

enabled

Свойство типа boolean, указывающее на то, будет ли выполняться задача. Вы можете отключить выполнение задачи, установив свойству enabled значение false. Зависимости задачи выполняются в том же порядке, как если бы задача не была отключена.

Отключение задачи


task templates << {
    println 'process email templates'
}

task sendEmails(dependsOn: templates) << {
    println 'send emails'
}

sendEmails.enabled = false

path

Свойство строчного типа, содержащее полный путь задачи. По умолчанию, путём задачи является имя задачи с символом двоеточие впереди.

Пример. Одноуровневый файл билда, который отображает путь единственной задачи определённой в нём


task echoMyPath << {
    println "THIS TASK'S PATH IS ${path}"
}

Результат выполнения предыдущего файла билда


d:\project>gradle echoMyPath
:echoMyPath
THIS TASK'S PATH IS :echoMyPath

BUILD SUCCESSFUL

Двоеточие впереди указывает на то, что задача определена на верхнем уровне файла билда. Расположение задач на верхнем уровне, однако, не является обязательным. Gradle поддерживает зависимые подпроекты, или вложенные билды. Если задача определёна во вложенном билде с названием subProject, путь будет :subProject:echoMyPath.

logger

Ссылка на внутренний объект Gradle logger. В Gradle logger реализует интерфейс org.slf4j.Logger с несколькими дополнительными уровнями логирования. Ниже описаны уровни логирования, поддерживаемые объектом logger. Установка уровню логирования одного из значений ниже включает логирование на всех последующих уровнях, кроме WARN и QUIET: DEBUG. Для подробных сообщений логирования, которые нужны разработчику билда, однако не должны выводиться в момент выполнения билда в нормальном режиме. Если выбран данный уровень, Gradle автоматически использует расширенный формат, который в каждом сообщении вставляет метку времени, уровень логирования, и имя задачи, производящей логирование. Остальные уровни используют более краткий формат сообщений. INFO. Нужен для менее информативных сообщений билда, играющих второстепенную роль во время выполнения билда. LIFECYCLE. Малоинформативные сообщения об изменениях в жизненном цикле билда и процессе выполнениия самого инструмента, запустившего сборку проекта. Обычно генерируются самим Gradle. Данный уровень используется по умолчанию, когда Gradle запускается без опции командной строки -q. Данный уровень логирования назначается сообщениям, выводимым оператором println. WARN. Малоинформативные, но важные сообщения, информирующие о потенциальных проблемах билда. Когда уровень логирования установлен в WARN, сообщения уровня QUIET не выводятся. QUIET. Сообщения, которые выводятся даже если вывод сообщений был отключен параметром командной строки -q. (Выполнение билда с параметром -q делает QUIET уровнем логирования по умолчанию). Данный уровень логирования назначается сообщениям, выводимым оператором System.out.println. Когда уровень логирования установлен в QUIET, сообщения уровня WARN не выводятся. ERROR. Редкие, но важные сообщения, кототые выводятся на всех уровнях логирования. Сообщения информируют о завершении билда с ошибками. Если ERROR — текущий уровень логирования, вызовы System.out.println не будут выводиться в консольном окне.

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


task logLevel << {

    def levels = ['DEBUG', 'INFO', 'LIFECYCLE', 'QUIET', 'WARN', 'ERROR']

    levels.each { level ->
        logging.level = level
        def logMessage = "SETTING LogLevel=${level}"

        logger.error logMessage
        logger.error '-' * logMessage.size()
        logger.debug 'DEBUG ENABLED'
        logger.info  'INFO ENABLED'
        logger.lifecycle 'LIFECYCLE ENABLED'
        logger.warn  'WARN ENABLED'
        logger.quiet 'QUIET ENABLED'
        logger.error 'ERROR ENABLED'
        println 'THIS IS println OUTPUT'
        logger.error ' '
    }
}

logging

Свойство logging даёт нам возможность управлять уровнем логирования. Как уже было показано в примере для свойства logger, уровень логирования билда можно получать и изменять, используя свойство logging.level.

description

Свойство description описывает назначение задачи небольшим количеством метаданных, доступных для понимания человека. Значение description можно указать несколькими способами:


task helloWorld(description: 'Says hello to the world') << {
    println 'hello, world'
}

Два способа объявления поведения задачи и задания описания


task helloWorld << {
    println 'hello, world'
}

helloWorld {
    description 'Says hello to the world'
}

// Ещё один способ
helloWorld.description 'Says hello to the world'

temporaryDir

Свойство temporaryDir возвращает объект File, который ссылается на временную директорию текущего файла билда. Такая директория создаётся для временного хранения промежуточных результатов работы задачи, или для организации файлов, которые задача будет обрабатывать.

Динамические свойства

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

Рассмотрим пример: задача createArtifact зависит от задачи copyFiles. Цель copyFiles — собрать файлы из нескольких источников и скопировать их во временную директорию, которую createArtifact в дальнейшем преобразует в артифакт установки. Список файлов звисит от параметров билда, но для соответствия специфическим требованиям установленного приложения, в артифакте должен храниться манифест, перечисляющий файлы. Здесь очень удобно использовать динамическое свойство:

Пример. Билд-файл, в котором показан пример динамического свойства


task copyFiles {
    // Где угодно находим файлы, копируем их
    // (здесь для наглядности используем фиксированный список)
    fileManifest = [ 'data.csv', 'config.json' ]
}

task createArtifact(dependsOn: copyFiles) << {
    println "FILES IN MANIFEST: ${copyFiles.fileManifest}"
}

Типы задач

Как мы уже говорили ранее, Задачи являются объектами, каждая задача имеет тип. Кроме типа DefaultTask, есть ещё другие типы задач для копирования, архивирования, запуска программ и других действий. Объявление типа задачи во многом похоже на наследование от базового класса в объектно-ориентированном языке. Таким образом, реализовав наследование, вы тут же получаете определённые свойства и методы в вашей задаче. Подобный синтаксис значительно укорачивает определение задач, при том что возможности по-прежнему остаются большими.

Рассмотрим несколько важных типов с примерами использования.

Copy

Задача Copy копирует файлы из одного места в другое. В простейшем случае — копирует файлы из одной директории в другую, с некоторыми дополнительными ограничениями по включению или исключению файлов, используя маски имён:

Простейший пример использования задачи Copy


task copyFiles(type: Copy) {
    from 'resources'
    into 'target'
    include '**/*.xml', '**/*.txt', '**/*.properties'
}

Jar

Задача Jar создаёт Jar-файл из файлов ресурсов. Задача данного типа c известным названием Jar определена в модуле 'java'. Задача упаковывает *.class-файлы и ресурсы в Jar-файл с названием проекта, при этом использует обычный манифест. Результат сохраняется в директорию build/libs. Данная задача в высокой степени обладает гибкостью.

Пример использования задачи Jar


apply plugin: 'java'

task customJar(type: Jar) {
    manifest {
        attributes firstKey: 'firstValue', secondKey: 'secondValue'
    }
    archiveName = 'hello.jar'
    destinationDir = file("${buildDir}/jars")
    from sourceSets.main.output
}

Обратите внимание — имя архива и целевая папка легко конфигурируются. Таким же образом можно менять значения файла манифеста, используя простой синтаксис словарей Groovy. Содержимое JAR-файла определяется строкой from sourceSets.main.output, которая включает .class-файлы. Метод from идентичен методу, который используется в примере CopyTask, что обнаруживает одну интересную деталь: задача Jar наследуется от задачи Copy. Зная эту особенность, вы можете ещё не заглянув в документацию сделать некоторые выводы о широких возможностях и порядке структуры классов, лежащей в основе задачи Jar.

destinationDir присваивается очень простое выражение. Было бы естественнее, если бы свойству destinationDir присваивалась строка. Но свойство работает с объектами java.io.File. На помощь приходит метод file(), который всегда доступен в коде билд файла Gradle. Он конвертирует строку в объект File.

Помните, вы всегда можете найти документацию, где описаны стандартные возможности Gradle, такие как задача Jar.

JavaExec

Задача JavaExec запускает Java-класс c методом main(). Запуск консольного Java-приложения может быть сопряжён с неудобствами. Однако данная задача избавляет от неудобств, интегрируя консольные Java-приложения в ваш билд:

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

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

Определение пользовательского типа задачи в файле билда

Допустим, в вашем билде нужно выполнить различные запросы к базе данных MySQL. В Gradle такая задача решается несколькими способами, но вы пришли к выводу, что создание пользовательской задачи будет наиболее выразительным решением. Простейший способ создания задачи — объявить её так, как показано в примере ниже:

Пользовательская задача для выполнения запросов в базе данных MySQL (из примера custom-task)


task createDatabase(type: MySqlTask) {
  sql = 'CREATE DATABASE IF NOT EXISTS example'
}

task createUser(type: MySqlTask, dependsOn: createDatabase) {
  sql = "GRANT ALL PRIVILEGES ON example.* TO exampleuser@localhost IDENTIFIED BY 'passw0rd'"
}

task createTable(type: MySqlTask, dependsOn: createUser) {
  username = 'exampleuser'
  password = 'passw0rd'
  database = 'example'
  sql = 'CREATE TABLE IF NOT EXISTS users (id BIGINT PRIMARY KEY, username VARCHAR(100))'
}

class MySqlTask extends DefaultTask {
  def hostname = 'localhost'
  def port = 3306
  def sql
  def database
  def username = 'root'
  def password = 'password'
  
  @TaskAction
  def runQuery() {
    def cmd
    if(database) {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} ${database} -e "
    }
    else {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} -e "
    }
    project.exec {
      commandLine = cmd.split().toList() + sql
    }
  }
}

Пользовательская задача MySqlTask наследуется от класса DefaultTask. Все пользовательские задачи должны наследоваться от класса DefaultTask, либо производного от него класса. (Пользовательская задача может наследоваться и от другого типа задачи, отличного от DefaultTask. См. выше параграф Типы задач, где описаны наиболее важные встроенные типы задач.) В терминах Groovy, в задаче объявлены свойства (такие как hostname, database, sql и т.д.). Далее объявлен метод runQuery(), который помечен аннотацией @TaskAction. При выполнении задачи runQuery() запустится.

Фактические задачи билда, определённые в начале файла билда, объявлены как задачи типа MySqlTask. Таким образом, они автоматически наследуют свойства и операцию базового класса задач. Для большинства свойств определены значения по умолчанию (однако для таких свойств как username и password значения, конечно же, специфичны для билда), потому остаётся лишь небольшая часть того, что нужно сконфигурировать, прежде чем выполнить каждую из задач. Для задач createDatabase и createUser конфигурируется всего лишь один SQL-запрос, остальные же значения в дальнейшем используются по умолчанию.

Задача createTable переопределяет свойства username, password и database. Таким образом, зависимости задачи создают новую базу данных и пользователя, отличные от административных настроек по умолчанию. Паттерн, который при необходимости переопределяет настройки конфигурации по умолчанию, широко применяется В Gradle.

Определение пользовательского типа задачи в дереве исходников

Если пользовательская задача очень велика, её код может существенно усложнять файл билда. Как было показано в примере выше, задача может состоять из нескольких строк простого кода. Однако на определённом этапе задача может развиться в свою собственную иерархию классов c зависимостями от внешнего API и необходимостью применить автоматизированное тестирование. Билд является кодом, а сложный код билда нужно рассматривать, как полноправного обитателя мира разработки кода. Такая задача в Gradle решается просто.

Когда логика пользовательской задачи перерастает разумные пределы файла билда, мы можем её перенести в директорию buildSrc, которая находится в корне проекта. Директория эта автоматически компилируется и добавляется в classpath билда. Мы изменим предыдущий пример, в котором будем использовать buildSrc:

Пример. Билд-файл использующий пользовательскую задачу, определённый во внешнем файле


task createDatabase(type: MySqlTask) {
  sql = 'CREATE DATABASE IF NOT EXISTS example'
}

task createUser(type: MySqlTask, dependsOn: createDatabase) {
  sql = "GRANT ALL PRIVILEGES ON example.* TO exampleuser@localhost IDENTIFIED BY 'passw0rd'"
}

task createTable(type: MySqlTask, dependsOn: createUser) {
  username = 'exampleuser'
  password = 'passw0rd'
  database = 'example'
  sql = 'CREATE TABLE IF NOT EXISTS users (id BIGINT PRIMARY KEY, username VARCHAR(100))'
}

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


import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class MySqlTask extends DefaultTask {
  def hostname = 'localhost'
  def port = 3306
  def sql
  def database
  def username = 'root'
  def password = 'password'
  
  @TaskAction
  def runQuery() {
    def cmd
    if(database) {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} ${database} -e "
    }
    else {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} -e "
    }
    project.exec {
      commandLine = cmd.split().toList() + sql
    }
  }
}

Заметим, что определение задачи в директории buildSrc полностью совпадает с кодом, включённым в скрипт билда в позапрошлом примере. Тем не менее, теперь у нас появляется работоспособная структура проекта, пригодная для совершенствования кода простой задачи, наращивания объектной модели, написания тестов и всего остального, что мы обычно делаем разрабатывая код.

Есть четыре способа, куда вы можете поместить пользовательский билд-код Gradle. Первый — добавить код собственно, в билд-скрипт, в блок операции задачи. Второй — создать внешний файл в директории buildSrc, как было только что сделано в последнем примере. Третий способ — импортировать внешний файл с билд-скриптом в наш основной билд-скрипт. Четвёртый — импорт внешнего модуля, написанного на Java или Goovy. Создание модулей в Gradle — отдельная тема, которую мы затрагивать не будем.

Структура проекта Gradle, использующего пользовательский код, помещённый в директорию buildSrc


.
├── build.gradle
├── buildSrc
│   └── src
│       └── main
│           └── groovy
│               └── org
│                   └── gradle
│                       └── example
│                           └── task
│                               └── MySqlTask.groovy

Откуда берутся задачи

До настоящего момента мы создавали задачи путём непосредственного написания кода в билд-скриптах Gradle, либо в директории buildSrc в виде кода Groovy. Такой подход хорош для изучения задач, так как даёт подробный обзор всех их особенностей. Всё же, большинство задач, которые вы будете использовать, не будут написаны вами. Они будут импортироваться из внешних модулей.

В простейшем примере сборки консольного приложения HelloWorld на Java, файл билда выглядит следующим образом:


apply plugin: 'java'

Применив модуль Java, билд-скрипт автоматически наследует набор задач, код которых вам не виден. Вы можете изменять поведение наследованных задач в блоках конфигурации, либо используя рассмотренные выше методы doFirst() и doLast(), для которых вам придётся писать код. Ключевой стратегией Gradle являются широкие возможности для расширения при малой сложности. Gradle предлагает вам большой набор функциональности посредством задач, подробности реализации которых вам не нужно знать, которые вы запускаете используя Gradle DSL (DSL — Domain Specific Language), а не множество запутанных инструкций кода Groovy.

Кроме того, в Gradle есть несколько встроенных задач, таких как tasks и properties. Такие задачи не импортируются из модулей или вашего кода. Они являются стандартом Gradle DSL.

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

Использованные материалы

Подробно о задачах Gradle

Реклама