Глава 20. Работа с файлами.

Большинство сборок работает с файлами. Gradle добавляет некоторые идеи и API, чтобы помочь вам работать с ними.

20.1. Нахождение файлов.

С помощью метода Project.file(java.lang.Object) вы можете определить местонахождение файла относительно рабочей папки.

Пример 20.1. Определение местонахождения файлов

build.gradle

// Using a relative path
File configFile = file('src/config.xml')

// Using an absolute path
configFile = file(configFile.absolutePath)

// Using a File object with a relative path
configFile = file(new File('src/config.xml'))
	  

Вы можете передать любой объект в метод file() и он предпримет попытку преобразовать значение в абсолютный объект File. Обычно, вы будете передавать туда экземпляры типа String или File. Если этот путь абсолютный, то он используется для создания экземпляра File. В противном случае, для создания экземпляра типа File будет использоваться путь, состоящий из пути к папке проекта в начале и переданного в метод далее. Метод file() понимает URL'ы, такие как file:/some/path.xml.

Использование этого метода - практичный способ преобразования некоторого значения, предоставленного пользователем, в абсолютный File. Предпочтительнее использовать new File(<какой-либо путь>), так как file() всегда вычисляет переданный путь относительно папки проекта, которая неизменна, а не текущей рабочей директории, которая может изменяться в зависимости от того, как пользователь запускает Gradle.

20.2. Коллекции файлов.

Коллеция файлов - это просто набора файлов. Она представлена интерфейсом FileCollection. Масса объектов в Gradle API реализуют этот интерфейс. Например, конфигурации зависимостей реализуют FileCollection.

Один из способов получить экземпляр FileCollection, использовать метод Project.files(java.lang.Object[]). Вы можете передать в этот метод любое количество объектов, которые затем конвертируются в набора объектов File. Метод files() принимает в качестве параметров любый типы объектов. Они вычисляются относительно папки проекта, согласно методу file(), описанному в Секции 20.1 Нахождение файлов. Вы можете передавать коллекции, итерируемые объекты, ассоциативные и просто массивы. Они преобразуются в плоский список и их содержимое конвертируется в экземпляры File.

Пример 20.2. Создание коллекции файлов

build.gradle

FileCollection collection = files('src/file1.txt',
                                  new File('src/file2.txt'),
                                  ['src/file3.txt', 'src/file4.txt'])
	  

Коллекция файлов итерируемая и может быть преобразована в другие типы с помощью оператора as. Вы можете совместить две коллекции файлов вместе посредством оператора + или вычесть из одной другую с помощью оператора -. Ниже несколько примеров того, чтобы вы можете делать с коллекцией файлов.

Пример 20.3. Использование коллекции файлов

build.gradle

// Iterate over the files in the collection
collection.each { File file ->
    println file.name
}

// Convert the collection to various types
Set set = collection.files
Set set2 = collection as Set
List list = collection as List
String path = collection.asPath
File file = collection.singleFile
File file2 = collection as File

// Add and subtract collections
def union = collection + files('src/file3.txt')
def different = collection - files('src/file3.txt')
	  

Также вы можете передать в метод files() замыкание или экземпляр Callable. Он вызовется, когда содержимое коллекции будет запрошено и возвращаемое значение преобразуется в набор экземпляров File. Возвращаемое значение может быть любого типа, поддерживаемого методом files(). Это простой способ 'реализовать' интерфейс FileCollection.

Пример 20.4. Реализация коллекции файлов

build.gradle

task list {
    doLast {
        File srcDir

        // Create a file collection using a closure
        collection = files { srcDir.listFiles() }

        srcDir = file('src')
        println "Contents of $srcDir.name"
        collection.collect { relativePath(it) }.sort().each { println it }

        srcDir = file('src2')
        println "Contents of $srcDir.name"
        collection.collect { relativePath(it) }.sort().each { println it }
    }
}
	  

Вывод команды gradle -q list

> gradle -q list
Contents of src
src/dir1
src/file1.txt
Contents of src2
src2/dir1
src2/dir2
	  

Вы можете передавать в метод files() другие типы вещей:

FileCollection

Они преобразуются в плоский список и их содержимое включается в коллекцию файлов.

Task

Выходные файлы задачи включаются в коллекцию файлов.

TaskOutputs

Выходные файлы TaskOutputs включаются в коллекцию файлов.

Важно обратить внимание на то, что содержимое коллекции файлов вычисляется 'лениво', когда оно требуется. Это означает, что вы можете, например, создать FileCollection, представляющую файлы, которые будут созданы в будущем, скажем, какой-либо задачей.

20.3. Деревья файлов.

Дерево файлов - это коллекция файлов расположенная с учетом иерархии. Например, дерево файлов может представлять дерево папок или содержимое zip-файла. Оно представлено интерфейсом FileTree. Интерфейс FileTree расширяет FileCollection, так что вы можете рассматривать дерево файлов так же, как и коллекцию файлов. Некоторые объекты в Gradle, такие как набор исходных кодов, реализуют интерфейс FileTree.

Один из способов получить экземпляр FileTree - использовать метод Project.fileTree(java.util.Map). Он создает FileTree определенное с основной папкой и, необязательно, некоторыми шаблонами включения и выключения в стиле Ant.

Пример 20.5. Создание дерева файлов

build.gradle

// Create a file tree with a base directory
FileTree tree = fileTree(dir: 'src/main')

// Add include and exclude patterns to the tree
tree.include '**/*.java'
tree.exclude '**/Abstract*'

// Create a tree using path
tree = fileTree('src').include('**/*.java')

// Create a tree using closure
tree = fileTree('src') {
    include '**/*.java'
}

// Create a tree using a map
tree = fileTree(dir: 'src', include: '**/*.java')
tree = fileTree(dir: 'src', includes: ['**/*.java', '**/*.xml'])
tree = fileTree(dir: 'src', include: '**/*.java', exclude: '**/*test*/**')
	  

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

Пример 20.6. Использование дерева файлов

build.gradle

// Iterate over the contents of a tree
tree.each {File file ->
    println file
}

// Filter a tree
FileTree filtered = tree.matching {
    include 'org/gradle/api/**'
}

// Add trees together
FileTree sum = tree + fileTree(dir: 'src/test')

// Visit the elements of the tree
tree.visit {element ->
    println "$element.relativePath => $element.file"
}
	  

20.4. Использование содержимого архива как дерева файлов.

Вы можете использовать содержимое архива, такого как zip- или tar-файла, как дерево файлов. Это можно сделать с помощью методов Project.zipTree(java.lang.Object) и Project.tarTree(java.lang.Object). Эти методы возвращают экземпляр FileTree, который можно использовать как любое другое дерево файлов или коллекцию файлов. Например, вы можете это использовать, чтобы распаковать архив, скопировав его содержимое, или объединить несколько архивов в одни.

Пример 20.7. Использование архива как дерева файлов

build.gradle

// Create a ZIP file tree using path
FileTree zip = zipTree('someFile.zip')

// Create a TAR file tree using path
FileTree tar = tarTree('someFile.tar')

//tar tree attempts to guess the compression based on the file extension
//however if you must specify the compression explicitly you can:
FileTree someTar = tarTree(resources.gzip('someTar.ext'))
	  

20.5. Указание набора входных файлов.

У множества объектов в Gradle есть свойства, которые принимают набор входных файлов. Например, задача JavaCompile содержит свойство source, которое определяет исходные файлы для компиляции. Вы можете установить значение этого свойства, используя любой из типов, поддерживаемых методом files(), который был показан выше. Это означает, что вы можете установить свойство, используя, например, File, String, коллекцию, FileCollection или даже замыкание. Вот несколько примеров:

Обычно, существует метод с таким же именем как у свойства, который добавляет набор файлов. И снова этот метод принимает типы, поддерживаемые методом files().

Пример 20.8. Указание набора файлов

build.gradle

task compile(type: JavaCompile)

// Use a File object to specify the source directory
compile {
    source = file('src/main/java')
}

// Use a String path to specify the source directory
compile {
    source = 'src/main/java'
}

// Use a collection to specify multiple source directories
compile {
    source = ['src/main/java', '../shared/java']
}

// Use a FileCollection (or FileTree in this case) to specify the source files
compile {
    source = fileTree(dir: 'src/main/java').matching { include 'org/gradle/api/**' }
}

// Using a closure to specify the source files.
compile {
    source = {
        // Use the contents of each zip file in the src dir
        file('src').listFiles().findAll {it.name.endsWith('.zip')}.collect { zipTree(it) }
    }
}
	  

build.gradle

compile {
    // Add some source directories use String paths
    source 'src/main/java', 'src/main/groovy'

    // Add a source directory using a File object
    source file('../shared/java')

    // Add some source directories using a closure
    source { file('src/test/').listFiles() }
}
	  

20.6. Копирование файлов.

Для копирования файлов, вы можете воспользоваться задачей Copy. Задача копирования очень гибкая и позволяет вам, например, отфильтровывать содержимое файлов, пока они копируются и отображать на имена файлов.

Для использовать задачи Copy, вы должные предоставить набор исходных файлов для копирования и папку назначения, куда они будут скопированы. Также вы можете указать как трансформировать файлы во время копирования. Все это вы можете сделать с использованием спецификации копирования. Она представлена интерфейсом CopySpec. Задача Copy реализует этот интерфейс. Исходные файлы указываются методом CopySpec.from(java.lang.Object[]). Для указания папки назначения, используйте метод CopySpec.into(java.lang.Object).

Пример 20.9. Копирование файлов с помощью задачи Copy

build.gradle

task copyTask(type: Copy) {
    from 'src/main/webapp'
    into 'build/explodedWar'
}
	  

Метод from() принимает любой из аргументов, принимаемых методом files(). Если аргумент вычисляется в папку, все, находящееся в этой папке (но не она сама) рекурсивно копируется в папку назначения. Когда аргумент вычисляется в файл, этот файл копируется в папку назначения. Когда аргумент вычисляется в несуществующий файл, то он игнорируется. Если аргумент задача, выходные файлы (т.е. файлы, которые создает задача) копируются и задача автоматически добавляется как зависимость задачи Copy. Метод into() принимает любой из аргументов, принимаемых методом files(). Еще один пример:

Пример 20.10. Указание исходных файлов и папки назначения задачи копирования

build.gradle

task anotherCopyTask(type: Copy) {
    // Copy everything under src/main/webapp
    from 'src/main/webapp'
    // Copy a single file
    from 'src/staging/index.html'
    // Copy the output of a task
    from copyTask
    // Copy the output of a task using Task outputs explicitly.
    from copyTaskWithPatterns.outputs
    // Copy the contents of a Zip file
    from zipTree('src/main/assets.zip')
    // Determine the destination directory later
    into { getDestDir() }
}
	  

Вы можете выбрать файлы для копирования с использованием шаблонов включения и выключения в стиле Any или с использованием замыкания:

Пример 20.11. Выбор файлов для копирования

build.gradle

task copyTaskWithPatterns(type: Copy) {
    from 'src/main/webapp'
    into 'build/explodedWar'
    include '**/*.html'
    include '**/*.jsp'
    exclude { details -> details.file.name.endsWith('.html') &&
                         details.file.text.contains('staging') }
}
	  

Еще вы можете использовать метод Project.copy(org.gradle.api.Action) для копирования файлов. Он работает таким же образом как и задача копирования, хотя и с несколькими значительными ограничениями. Во-первых, метод copy() не инкрементальный (смотрите Секцию 19.9 Проверки на устаревание (также известные как Инкрементная Сборка).

Пример 20.12. Копирование файлов с использованием метода copy() без проверки на устаревание

build.gradle

task copyMethod {
    doLast {
        copy {
            from 'src/main/webapp'
            into 'build/explodedWar'
            include '**/*.html'
            include '**/*.jsp'
        }
    }
}
	  

Во-вторых, метод copy() не может поддерживать зависимости задач, когда задача используется как источник копирования (т.е. как аргумент для from()), потому что это метод, а не задача. По существу, если вы используете метод copy() как часть действия задачи, вы должны явно объявлять входные и выходные данные, чтобы добиться правильного поведения.

Пример 20.13. Копирование файлов с использованием метода copy() с проверкой на устаревание

build.gradle

task copyMethodWithExplicitDependencies{
    // up-to-date check for inputs, plus add copyTask as dependency
    inputs.file copyTask
    outputs.dir 'some-dir' // up-to-date check for outputs
    doLast{
        copy {
            // Copy the output of copyTask
            from copyTask
            into 'some-dir'
        }
    }
}
	  

Предпочтительнее использовать задачу Copy, когда это возможно, так как она поддерживает инкрементную сборку и выведение зависимости задачи без дополнительных усилий с вашей стороны. Метод copy() может быть использован для копирования файлов как часть реализации задачи. То есть, этот метод нацелен на использование пользовательскими задачами (смотрите Главу 40 Написание пользовательских классов задач), которым необходимо копировать файлы, в качестве части их назнаения. В таком сценарии, пользовательской задаче должно быть достаточно объявить входы/выходы относящиеся к ее действию.

20.6.1. Переименование файлов

Пример 20.14. Переименование файлов во время копирования

build.gradle

task rename(type: Copy) {
    from 'src/main/webapp'
    into 'build/explodedWar'
    // Use a closure to map the file name
    rename { String fileName ->
        fileName.replace('-staging-', '')
    }
    // Use a regular expression to map the file name
    rename '(.+)-staging-(.+)', '$1$2'
    rename(/(.+)-staging-(.+)/, '$1$2')
}
	  

20.6.2. Фильтрация файлов

Пример 20.15. Фильтрация файлов во время копирования

build.gradle

import org.apache.tools.ant.filters.FixCrLfFilter
import org.apache.tools.ant.filters.ReplaceTokens

task filter(type: Copy) {
    from 'src/main/webapp'
    into 'build/explodedWar'
    // Substitute property tokens in files
    expand(copyright: '2009', version: '2.3.1')
    expand(project.properties)
    // Use some of the filters provided by Ant
    filter(FixCrLfFilter)
    filter(ReplaceTokens, tokens: [copyright: '2009', version: '2.3.1'])
    // Use a closure to filter each line
    filter { String line ->
        "[$line]"
    }
    // Use a closure to remove lines
    filter { String line ->
        line.startsWith('-') ? null : line
    }
    filteringCharset = 'UTF-8'
}
	  

При использовании класса ReplaceTokens с операцией 'filter', в результате вы используете движок шаблонов, который заменяет маркеры в форме '@имяМаркера@' (маркер в стиле Apache Ant) заданным набором значений. Операция 'expand' делает то же самое, за одним исключением, она рассматривает исходные файлы как шаблоны Groovy, в которых маркеры принимают вид '${имяМаркера}'. Знайте, что вам может понадобиться экранировать часть ваших исходных файлов при использовании этой опции, например, если в них содержатся строковые литералы '$' или '<%'.

Хорошая практика - указывать кодировку при чтении или записи файла, используя свойство filteringCharset. Если оно не указано, используется кодировка JVM по умолчанию, которая может не соответствовать действительной кодировке файла для фильтрации и может отличать от машины к машине.

20.6.3. Использование класса CopySpec

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

Пример 20.16. Вложенные спецификации копирования

build.gradle

task nestedSpecs(type: Copy) {
    into 'build/explodedWar'
    exclude '**/*staging*'
    from('src/dist') {
        include '**/*.html'
    }
    into('libs') {
        from configurations.runtime
    }
	  

20.7. Использование задачи Sync.

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

Вот пример, в котором поддерживается копия зависимостей времени выполнения проекта в папке build/libs.

Пример 20.17. Использование задачи Sync для копирования зависимостей.

build.gradle

task libs(type: Sync) {
    from configurations.runtime
    into "$buildDir/libs"
}
	  

20.8. Создание архивов.

В проекте может быть столько jar-архивов, сколько вы захотите. Также вы можете добавить war-, zip- и tar-архивы в ваш проект. Архивы создаются различными задачами архивирования: Zip, Tar, Jar, War и Ear. Все они работают одинаково, так что давайте взглянем на создание zip-файла.

Пример 20.18. Создание zip-архива.

build.gradle

apply plugin: 'java'

task zip(type: Zip) {
    from 'src/dist'
    into('libs') {
        from configurations.runtime
    }
}
	  

Задачи архивирования работают так же как задача Copy и реализуют тот же самый интерфейс CopySpec. Так же как с задачей Copy, вы указываете входные файлы используя метод from() и можете указать где они будут находится в архиве методом into(). Вы можете фильтровать содержимое файла, переименовывать файлы и делать другие вещи, которые вы делаете со спецификацией копирования.

20.8.1. Именование архива

Зачем использовать плагин Java?

Плагин Java добавляет множество значений по умолчанию к задачам архивирования. Вы можете их использовать и без этого плагина, если желаете. Для этого вам понадобится предоставить значения для некоторых дополнительных свойств.

Формат projectName-version.type используется для генерации имена файлов архивов. Например:

Пример 20.19. Создание zip-архива.

build.gradle

apply plugin: 'java'

version = 1.0

task myZip(type: Zip) {
    from 'somedir'
}

println myZip.archiveName
println relativePath(myZip.destinationDir)
println relativePath(myZip.archivePath)
	  

Вывод команды gradle -q myZip

> gradle -q myZip
zipProject-1.0.zip
build/distributions
build/distributions/zipProject-1.0.zip
	  

Здесь создается задача архивирования Zip с именем myZip, которая создает zip-файл zipProject-1.0.zip. Важно отличать имя задачи архивирования от имени сгенерированного ей архива. Имя архива по умолчанию может быть изменено с помощью свойства archivesBaseName. Также имя архива может быть изменено позже в любое время.

У задачи архивирования есть множество свойств, которые вы можете установить. Они перечислены в Таблице 20.1 Задачи архивирования - свойства именования. Вы можете, например, изменить имя архива:

Пример 20.20. Настройка задачи архивирования - пользовательское имя архива.

build.gradle

apply plugin: 'java'
version = 1.0

task myZip(type: Zip) {
    from 'somedir'
    baseName = 'customName'
}

println myZip.archiveName
	  

Вывод команды gradle -q myZip

> gradle -q myZip
customName-1.0.zip
	  

Вы можете дальше изменять имена архивов:

Пример 20.21. Настройка задачи архивирования - appendix и classifier.

build.gradle

apply plugin: 'java'
archivesBaseName = 'gradle'
version = 1.0

task myZip(type: Zip) {
    appendix = 'wrapper'
    classifier = 'src'
    from 'somedir'
}

println myZip.archiveName
	  

Вывод команды gradle -q myZip

> gradle -q myZip
gradle-wrapper-1.0-src.zip
	  
Таблица 20.1. Задачи архивирования - свойства именования
Имя свойстваТипЗначение по умолчанияюОписание
archiveNameString

'baseName-appendix-version-classifier.extension'

Если любой из этих свойст пустое - оно не добавляется к имени

Базовое имя файла сгенерированного архива
archivePathFile'destinationDir/archiveName'Абсолютный путь сгенерированного архива
destinationDirFileЗависит от типа архива. Jar- и War-архивы идут в 'project.buildDir/libraries'. Zip- и Tar-архивы в 'project.buildDir/distributions'Папка в которую будет помещен сгенерированный архив
baseNameString'project.name'Часть baseName в имени файла архива
appendixStringnullЧасть appendix в имени файла архива
versionString'project.version'Часть version в имени файла арихва
classifierStringnullЧасть classifier в имени файла архива
extensionStringЗависит от типа архива и для tar-файлов также и от типа сжатия: zip, jar, war, tar, tgz или tbz2.Расширение имени файла архива

20.8.2. Использование общего содержимого несколькими архивами

Вы можете использовать метод Project.copySpec(org.gradle.api.Action), чтобы у нескольких архивов было общее содержимое.

20.8.3. Повторяемые архивы

Иногда бывает желательно создать архив байт в байт, но на других машинах. Вы хотите быть уверены, что сборка артефактов из исходного кода дает один и тот же результат, байт к байту, независимо от того, когда и где она была. Такое поведение необходимо для проектов типа reproducible-builds.org.

Воспроизведение такого архива байт к байту является небольшим вызовов, так как на порядок файлов в архиве влияет файловая система. Каждый раз, когда zip, tar, jar, war или ear собираются из исходных файлов, порядок этих файлов может меняться. Файлы, у которых отличаются только метки времени, также служат причиной небольших отличий архивов между сборками. Все задачи AbstractArchiveTask (например, jar, zip) поставляемые с Gradle, включают инкубационную поддержку создания повторяемых архивов.

Например, чтобы сделать задачу Zip повторяемой, вам необходимо установить Zip.isReproducibleFileOrder() в true и Zip.isPreserveFileTimestamps() в false. Для того, чтобы сделать все задачи архивирования в вашей сборке повторяемыми, подумайте о добавлении следующей конфигурации в ваш сборочный файл:

Пример 20.22. Активация повторяемых архивов.

build.gradle

tasks.withType(AbstractArchiveTask) {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}
	  

Часто вам будет необходимо опубликовать архив, чтобы им можно было воспользоваться из другого проекта. Этот процесс описан в Главе 32 Публикация артефактов.

20.9. Файлы свойств.

Файлы свойств используются во многих местах во время Java-разработки. Gradle делает создание файлов свойств как обычной части вашей сборки легким. Вы можете использовать задачу WriteProperties для создания файлов свойств.

Задача WriteProperties также исправляет хорошо известную проблему связанную с Properties.store(), которая снижает пользу от инкрементной сборки (смотрите Секцию 19.9 Проверки на устаревание (также известные как Инкрементная Сборка). Стандартный способ записи файлов свойств в Java выдает уникальный файл каждый раз, даже когда используются те же свойства и значения, потому что он включает метку времени в комментарии. Задача Gradle WriteProperties генерирует точно такой выход байт к байту, если ни одно из свойств не изменилось. Это достигается несколькими тонкими настройками того, как генерируется файл свойств:

  • На выходе не добавляется метка времени в комментарии
  • Разделитель линий не зависит от системы, но может быть настроен явно (по умолчанию '\n')
  • Свойства отсортированы в алфавитном порядке