vlambda博客
学习文章列表

读书笔记《gradle-effective-implementations-guide-second-edition》使用Gradle构建脚本

Chapter 3.  Working with Gradle Build Scripts

Gradle 脚本是一个程序。我们使用 Groovy DSL 来表达我们的构建逻辑。 Gradle 有几个有用的内置方法来处理文件和目录,因为我们经常在构建逻辑中处理文件和目录。

在本章中,我们将讨论如何使用 Gradle 的功能来处理文件和目录。我们还将了解如何在 Gradle 构建中设置属性并使用 Gradle 的日志框架。最后,我们将看到如何使用 Gradle Wrapper 任务通过我们的构建脚本分发可配置的 Gradle。

Working with files


在构建脚本中,我们必须处理文件和目录是很常见的。例如,当我们需要将文件从一个目录复制到另一个目录或创建一个目录来存储任务或程序的输出时。

Locating files

要定位相对于当前项目的文件或目录,我们可以使用 file() 方法。这个方法实际上是连接到我们的构建脚本的Project对象的方法。在上一章中,我们讨论了如何使用对 project变量的显式引用或简单地调用 Project的方法和属性代码> 对象隐式。

file() 方法将解析文件或目录相对于当前项目而不是当前工作目录的位置。这非常有用,因为我们可以从不同于实际构建脚本位置的目录运行构建脚本。  file() 方法返回的文件或目录引用随后会相对于项目目录进行解析。

我们可以将任何对象作为 file() 方法的参数传递。通常,我们将传递一个 String 或 java.io.File 对象。

在下一个示例中,我们将演示如何使用 file() 方法获取对 File 对象的引用:

// Use String for file reference. 
File wsdl = file('src/wsdl/sample.wsdl') 
 
// Use File object for file reference. 
File xmlFile = new File('xml/input/sample.xml') 
def inputXml = project.file(xmlFile) 

我们可以通过多种方式使用 file() 方法。我们可以传递一个 url 或 uri 实例作为参数。 Gradle 现在仅支持基于文件的 URL。我们也可以使用闭包来定义文件或目录。最后,我们还可以传递一个 java.util.concurrent.Callable接口的实例,其中 call()的返回值 方法是对文件或目录的有效引用:

import java.util.concurrent.Callable 
 
// Use URL instance to locate file. 
def url = new URL('file:/README') 
File readme = file(url) 
 
// Or a URI instance. 
def uri = new URI('file:/README') 
def readmeFile = file(uri) 
 
// Use a closure to determine the 
// file or directory name. 
def fileNames = ['src', 'web', 'config'] 
def configDir = file { 
    fileNames.find { fileName -> 
        fileName.startsWith('config') 
    } 
} 
 
// Use Callable interface. 
def source = file(new Callable<String>() { 
    String call() { 
        'src' 
    } 
}) 

使用 file() 方法,我们创建了一个新的 File 对象;此对象可以引用文件或目录。我们可以使用 File的 isFile()或 isDirectory()方法 对象来查看我们是在处理文件还是目录。如果我们想检查文件或目录是否真的存在,我们使用 exists() 方法。由于我们的 Gradle 构建脚本是用 Groovy 编写的,因此我们还可以使用 Groovy 为 File 类添加的额外属性和方法。例如,我们可以使用 text 属性来读取文件的内容。但是,我们只能在使用  file() 方法创建  File 对象后对其进行测试。如果我们想停止构建以防目录不存在或者我们正在处理一个文件并且我们希望处理一个目录怎么办?在 Gradle 中,我们可以将一个额外的参数传递给  org.gradle.api.PathValidationfile() 方法> 类型。 Gradle 然后验证创建的 File 对象是否对 PathValidation 实例有效;如果不是,则构建停止,我们会收到一条很好的错误消息,告诉我们出了什么问题。

假设,我们想在我们的构建脚本中使用一个名为 config 的目录。该目录必须存在,否则构建将停止:

def dir = project.file(new File('config'),    PathValidation.DIRECTORY) 

现在我们可以运行构建并从输出中看到该目录不存在:

$ gradle -q
FAILURE: Build failed with an exception.
* Where:
Build file '/Users/mrhaki/Projects/gradle-effective-implementation-guide-2/Code_Files/build.gradle' line: 1
* What went wrong:
A problem occurred evaluating root project 'files'.
> Directory '/Users/mrhaki/Projects/gradle-effective-implementation-guide-2/Code_Files/files/config' does not exist.
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

我们还可以使用 PathValidation 参数来测试 File 对象是否真的是文件而不是目录。最后,我们可以检查 File 对象是否引用了现有文件或目录。如果文件或目录不存在,则会引发异常并停止构建:

// Check file or directory exists. 
def readme = project.file('README', PathValidation.EXISTS) 
 
// Check File object is really a file. 
def license = project.file('License.txt', PathValidation.FILE) 

Using file collections

我们还可以使用一组文件或目录,而不仅仅是单个文件或目录。在 Gradle 中,一组文件由 ConfigurableFileCollection 接口表示。好消息是 Gradle API 中的很多类都实现了这个接口。

我们可以使用 files() 方法在我们的构建脚本中定义一个文件集合。此方法在我们可以在构建脚本中访问的Project 对象中定义。  files() 方法接受许多不同类型的参数,这使得它使用起来非常灵活。例如,我们可以使用 String和 File对象来定义一个文件集合。

file() 方法一样,路径被解析,相对于项目目录:

// Use String instances. 
def multiple = 
    files('README', 'licence.txt') 
 
// Use File objects. 
def userFiles = 
    files(new File('README'), new File('INSTALL')) 
 
// We can combine different argument types. 
def combined = files('README', new File('INSTALL')) 

然而,这些并不是我们可以使用的唯一论据。我们可以传递 URI 或 URL 对象,就像我们可以使用 文件() 方法:

def urlFiles = 
    files(new URI('file:/README'), 
        new URL('file:/INSTALL')) 

我们还可以使用数组、Collection 或 Iterable 对象与文件名或其他 ConfigurableFileCollection 实例作为参数:

// Use a Collection with file or directory names. 
def listOfFileNames = ['src', 'test'] 
def mainDirectories = files(listOfFileNames) 
 
// Use an array. 
// We use the Groovy as keyword to 
// force an object to a certain type. 
mainDirectories = files(listOfFileNames as String[]) 
 
// Or an implementation of the Iterable interface. 
mainDirectories = files(listOfFileNames as Iterable) 
 
// Combine arguments and pass another file collection. 
def allDirectories = files(['config'], mainDirectories) 

我们还可以使用 Callable 接口的闭包或实例来定义文件列表,如下所示:

import java.util.concurrent.Callable 
 
def dirs = files { 
    [new File('src'), file('README')] 
    .findAll { file -> 
        file.directory 
    } 
} 
 
def rootFiles = files(new Callable<List<File>>() { 
 
    def files = [new File('src'), 
            file('README'), 
            file('INSTALL')] 
 
    List<File> call() { 
        files.findAll { fileObject -> 
            fileObject.file 
        } 
    } 
 
}) 

最后,我们可以将 Task 对象作为参数传递给 files() 方法。任务的 outputs属性用于确定文件集合或者我们可以直接使用 TaskOutputs对象而不是让Gradle 通过Task 对象的 outputs 属性解决它。让我们看看我们在上一章创建的 convert 任务。这个任务有一个 outputs 属性和一个文件,但这也可以是多个文件或一个目录。要在我们的构建脚本中获取文件集合对象,我们只需将 Task 实例作为参数传递给 files() 方法:

task convert { 
    def source = new File('source.xml') 
    def output = new File('output.txt') 
 
    // Define input file 
    inputs.file source 
 
    // Define output file 
    outputs.file output 
 
    doLast { 
        def xml = new XmlSlurper().parse(source) 
        output.withPrintWriter { writer -> 
            xml.person.each { person -> 
                writer.println "${person.name},${person.email}" 
            } 
        } 
        println "Converted ${source.name} to ${output.name}" 
    } 
} 
 
// Get the file collection from 
// the task outputs property. 
def taskOutputFiles = files(convert) 
 
// Alternatively we could use 
// the outputs property directly. 
taskOutputFiles = files(convert.outputs) 

同样重要的是要注意文件收集是惰性的。这意味着当我们定义集合时,集合中的路径不会被解析。只有在构建脚本中实际查询和使用文件时,才会解析集合中的路径。

ConfigurableFileCollection 接口具有操作集合的有用方法,例如,我们可以使用  + 和  - 运算符分别从集合中添加或删除元素:

// Define collection. 
def fileCollection = files('README', 'INSTALL') 
 
// Remove INSTALL file from collection. 
def readme = fileCollection - files('INSTALL') 
 
// Add new collection to existing collection. 
def moreFiles = 
    fileCollection + 
    files(file('config', 
            PathValidation.DIRECTORY)) 

要获取 ConfigurableFileCollection 中元素的绝对路径名,我们可以使用 asPath 属性。路径名由操作系统的路径分隔符分隔。在 Microsoft Windows 操作系统中,分号 (;) 用作路径分隔符;在 Linux 或 Mac OS X 操作系统中,使用冒号 (:)。这意味着我们可以在任何操作系统上简单地使用 asPath 属性,Gradle 将自动使用正确的路径分隔符:

task collectionPath << { 
    def fileCollection = files('README', 'INSTALL') 
    println fileCollection.asPath 
} 

当我们在 Mac OS X 上运行构建脚本时,我们得到以下输出:

$ gradle -q collectionPath
/Users/mrhaki/gradle-book/Code_Files/files/README:/Users/mrhaki/gradle-book/Code_Files/files/INSTALL

要获取构成文件集合的 File 对象,我们可以使用 files 属性。我们还可以使用 as关键字将集合转换为 File对象列表;如果我们知道我们的集合仅由单个文件或目录组成,那么我们可以使用 singleFile 属性来获取 File< /code> 对象,如下:

def fileCollection = files('README', [new File('INSTALL')]) 
 
// Get all elements as File objects. 
def allFiles = fileCollection.files 
 
// Or use casting with as keyword. 
def fileObjects = fileCollection as File[] 
 
 
def singleFileCollection = files('INSTALL') 
 
// Get single file as File object. 
def installFile = singleFileCollection.singleFile 

最后,我们可以使用 filter() 方法对我们的文件集合应用过滤器。我们传递了一个闭包,它定义了要在过滤集合中的元素。过滤后的集合是实时集合。这意味着如果我们向原始集合添加新元素,过滤器闭包将再次应用于我们过滤的集合。在下面的示例中,我们有 filterFiles 任务,我们在其中定义两个文件的文件集合,其名称为 INSTALL.txt 和 README。接下来,我们定义一个带有过滤器的新文件集合,该过滤器包含所有具有 .txt 文件扩展名的文件。这个集合是一个实时的、过滤的集合,当我们向原始集合添加一个新文件时,过滤的集合也会更新:

task filterFiles << { 
    def rootFiles = files('INSTALL', 'README') 
 
    // Filter for files with a txt extension. 
    def smallFiles = rootFiles.filter { file -> 
        file.name.endsWith 'txt' 
    } 
 
    rootFiles = rootFiles + files('LICENSE.txt') 
    // smallFiles now contains 2 files: 
    // INSTALL and LICENSE 
} 

Working with file trees

在 Gradle 中,我们还可以处理组织为树的文件集合,例如磁盘上的目录树或 ZIP 文件中的分层内容。分层文件集合由 ConfigurableFileTree 接口表示。这个接口扩展了我们之前看到的 ConfigurableFileCollection接口。

要创建新的文件树,我们在项目中使用 fileTree() 方法。我们可以使用几种方法来定义文件树。

Note

如果我们不提供基目录,则将当前项目目录用作文件树的基目录。

我们可以使用 include 方法和 includes 属性来定义匹配模式以在文件中包含一个文件(或多个文件)树。使用 exclude方法和 excludes属性,我们可以使用相同的语法从文件中排除一个或多个文件树。匹配模式风格被描述为 Ant 风格的匹配模式,因为 Ant 构建工具使用这种风格来定义语法来匹配文件树中的文件名。可以使用以下模式:

  • * 匹配任意数量的字符

  • ? 匹配任何单个字符

  • ** 匹配任意数量的目录或文件

以下示例演示了如何创建文件树:

// Create file tree with base directory 'src/main' 
// and only include files with extension .java 
def srcDir = fileTree('src/main').include('**/*.java') 
 
// Use map with arguments to create a file tree. 
def resources = 
    fileTree(dir: 'src/main', 
        excludes: ['**/*.java', '**/*.groovy']) 
 
// Create file tree with project directory as base 
// directory and use method include() on tree 
// object to include 2 files. 
def base = fileTree('.') 
base.include 'README', 'INSTALL' 
 
// Use closure to create file tree. 
def javaFiles = fileTree { 
    from 'src/main/java' 
    exclude '*.properties' 
} 

要过滤文件树,我们可以像处理文件集合一样使用 filter() 方法,但我们也可以使用 matching( ) 方法。我们将闭包传递给 matching() 方法或 org.gradle.api.tasks.util.PatternFilterable 接口。我们可以使用 includeincludesexclude,和 excludes 方法从文件树中包含或排除文件,如下所示:

def sources = fileTree { 
    from 'src' 
} 
 
def javaFiles = sources.matching { 
    include '**/*.java' 
} 
 
def nonJavaFiles = sources.matching { 
    exclude '**/*.java' 
} 
 
def nonLanguageFiles = sources.matching { 
    exclude '**/*.scala', '**/*.groovy', '**/*.java' 
} 
 
def modifiedLastWeek = sources.matching { 
    lastWeek = new Date() - 7 
    include { file -> 
        file.lastModified > lastWeek.time 
    } 
} 

我们可以使用 visit() 方法来访问每个树节点。我们可以检查节点是目录还是文件。然后按广度顺序访问树,如以下代码所示:

def testFiles = fileTree(dir: 'src/test') 
 
testFiles.visit { fileDetails -> 
    if (fileDetails.directory) { 
        println "Entering directory ${fileDetails.relativePath}" 
    } else { 
        println "File name: ${fileDetails.name}" 
    } 
} 
 
 
def projectFiles = fileTree(dir: 'src/test') 
 
projectFiles.visit(new FileVisitor() { 
    void visitDir(FileVisitDetails details) { 
        println "Directory: ${details.path}" 
    } 
 
    void visitFile(FileVisitDetails details) { 
        println "File: ${details.path}, size: ${details.size}" 
    } 
}) 

Copying files

要在 Gradle 中复制文件,我们可以使用 Copy 任务。我们必须分配一组要复制的源文件和这些文件的目的地。这是用复制规范定义的。复制规范由 org.gradle.api.file.CopySpec 接口定义。该接口有一个 from() 方法,我们可以使用它来设置我们要复制的文件或目录。使用我们在目标目录或文件中指定的 into() 方法。

以下示例显示了一个名为 simpleCopy 的简单 Copy 任务,具有单个源 src/ xml 目录和目标 definitions 目录:

task simpleCopy(type: Copy) { 
    from 'src/xml' 
    into 'definitions' 
} 

from() 方法接受与 files() 方法相同的参数。当参数是一个目录时,该目录中的所有文件——€”但不是目录本身——都被复制到目标目录。如果参数是文件,则仅复制该文件。

into() 方法接受与 file() 方法相同的参数。要包含或排除文件,我们使用 CopySpec 接口。我们可以像使用  fileTree() 方法一样应用 Ant 样式的匹配模式。

以下示例定义了一个名为 copyTask 的任务,并使用了 include() 和 exclude() 方法来选择要复制的文件集:

// Define a closure with ANT-style 
// pattern for files. 
def getTextFiles = { 
    '**/*.txt' 
} 
 
task copyTask(type: Copy) { 
    // Copy from directory. 
    from 'src/webapp' 
 
    // Copy single file. 
    from 'README.txt' 
 
    // Include files with html extension. 
    include '**/*.html', '**/*.htm' 
 
    // Use closure to resolve files. 
    include getTextFiles 
 
    // Exclude file INSTALL.txt. 
    exclude 'INSTALL.txt' 
 
    // Copy into directory dist 
    // resolved via closure. 
    into { file('dist') } 
} 

另一种复制文件的方法是使用 Project.copy() 方法。  copy() 方法接受一个 CopySpec 接口实现,就像 Copy 任务。我们的simpleCopy 任务也可以写成如下:

task simpleCopy << { 
    // We use the project.copy() 
    // method in our task. We can 
    // leave out the project reference, 
    // because Gradle knows how to 
    // resolve it automatically. 
    copy { 
        from 'src/xml' 
        into 'definitions' 
    } 
} 

Archiving files

要创建归档文件,我们可以使用 ZipTarJar 、 War 和 Ear 任务。为了定义存档的源文件和存档文件中的目标,我们使用 CopySpec 接口,就像复制文件一样。我们可以使用 rename()filter()expand( )、 include() 和 exclude() 方法一样,这样你就不用不必学习任何新东西,你可以使用你已经学过的东西。

要设置存档的文件名,我们使用以下任何属性:baseNameappendix版本, 分类器,或 扩展。 Gradle 将使用以下模式创建文件名: [baseName]-[appendix]-[version]-[classifier]。 [扩展] 。如果未设置属性,则它不会包含在生成的文件名中。要覆盖默认文件名模式,我们可以设置  archiveName 属性并分配我们自己的完整文件名,用于生成的存档文件。

在以下示例中,我们将使用 archiveZip 任务创建一个 ZIP 存档。我们将包含 dist 目录中的所有文件,并将它们放在存档的根目录中。文件的名称由遵循 Gradle 模式的各个属性设置:

task archiveDist(type: Zip) { 
    from 'dist' 
 
    // Create output filename. 
    // Final filename is: 
    // dist-files-archive-1.0-sample.zip 
    baseName = 'dist-files' 
    appendix = 'archive' 
    extension = 'zip' 
    version = '1.0' 
    classifier = 'sample' 
} 

当我们运行 archiveDist 任务时,一个新的dist-files-archive-1.0-sample.zip 文件会在我们项目的根。要更改存档文件的目标目录,我们必须设置 destinationDir 属性。在以下示例中,我们将目标目录设置为 build/zips。我们还将使用 into() 方法将文件放在归档文件中的 files 目录中。文件名现在由 archiveName 属性设置:

// By using task type Zip we instruct 
// Gradle to create an archive 
// in ZIP format. 
task archiveFiles(type: Zip) { 
    from 'dist' 
 
    // Copy files to a directory inside the archive. 
    into 'files' 
 
    // Set destination directory for ZIP file. 
    // $buildDir refers to default Gradle 
    // build directory 'build/'. 
    destinationDir = file("$buildDir/zips") 
 
    // Set complete filename at once. 
    archiveName = 'dist-files.zip' 
} 

要使用可选的 gzip 或 bzip2 压缩创建 TAR 存档,我们必须使用 tarFiles 任务。语法与  Zip 类型的任务相同,但我们有一个额外的 compression 属性可以使用设置我们想要使用的压缩类型 (gzip, bzip2)。如果我们不指定 compression 属性,则不使用压缩来创建归档文件。

在下面的示例中,我们创建了一个 Tar 类型的 tarFiles 任务。我们将 compression属性设置为 gzip。运行这个任务后,我们得到一个新的 dist/tarballs/dist-files.tar.gz文件:

task tarFiles(type: Tar) { 
    from 'dist' 
 
    // Set destination directory. 
    destinationDir = file("$buildDir/tarballs") 
 
    // Set filename properties. 
    baseName = 'dist-files' 
 
    // Default extension for tar files 
    // with gzip compression is tgz. 
    extension = 'tar.gz' 
 
    // Use gzip compression. 
    compression = Compression.GZIP // or Compression.BZIP2 
} 

JarWar 和 Ear 任务类型遵循与 Zip 和 Tar 任务类型的模式相同。每种类型都有一些额外的属性和方法来包含特定于该类型存档的文件。当我们了解如何在 Java 项目中使用 Gradle 时,我们将看到这些任务的示例。

Project properties


在 Gradle 构建文件中,我们可以访问 Gradle 定义的多个属性,但我们也可以创建自己的属性。我们可以直接在构建脚本中设置自定义属性的值,也可以通过命令行传递值来实现。

我们可以在 Gradle 构建中访问的默认属性如下表所示:

名称

类型

默认值

项目

项目

项目实例。

名称

字符串

项目目录的名称。该名称是只读的。

路径

字符串

项目的绝对路径。

描述

字符串

项目的描述。

projectDir

文件

包含构建脚本的目录。该值是只读的。

buildDir

文件

目录中具有 build 名称的目录,包含构建脚本。

rootDir

文件

项目结构根目录下的项目目录。

对象

未指定。

版本

对象

未指定。

蚂蚁

AntBuilder

AntBuilder 实例。

以下构建文件具有显示属性值的任务:

version = '1.0' 
group = 'Sample' 
description = 'Sample build file to show project properties' 
 
task defaultProperties << { 
    println "Project: $project" 
    println "Name: $name" 
    println "Path: $path" 
    println "Project directory: $projectDir" 
    println "Build directory: $buildDir" 
    println "Version: $version" 
    println "Group: $project.group" 
    println "Description: $project.description" 
    println "AntBuilder: $ant" 
} 

当我们运行构建时,我们得到以下输出:

$ gradle defaultProperties
:defaultProperties
Project: root project 'props'
Name: defaultProperties
Path: :defaultProperties
Project directory: /Users/mrhaki/gradle-book/Code_Files/props
Build directory: /Users/mrhaki/gradle-book/Code_Files/props/build
Version: 1.0
Group: Sample
Description: Sample build file to show project properties
AntBuilder: org.gradle.api.internal.project.DefaultAntBuilder@3c95cbbd
BUILD SUCCESSFUL
Total time: 1.458 secs

Defining custom properties in script

要添加我们自己的属性,我们必须在构建文件的  ext{} 脚本块中定义它们。在属性名称前加上 ext. 是另一种设置值的方法。要读取属性的值,我们不必使用 ext.前缀,我们可以简单地引用属性的名称。该属性也会自动添加到内部 project 属性中。

在下面的脚本中,我们添加了一个 customProperty 属性和一个 String 值 custom。在 showProperties 任务中,我们显示属性的值:

// Define new property. 
ext.customProperty = 'custom' 
 
// Or we can use ext{} script block. 
ext { 
  anotherCustomProperty = 'custom' 
} 
 
task showProperties { 
    ext { 
        customProperty = 'override' 
    } 
    doLast { 
        // We can refer to the property 
        // in different ways: 
        println customProperty 
        println project.ext.customProperty 


        println project.customProperty 
    } 
} 

运行脚本后,我们得到以下输出:

$ gradle showProperties
:showProperties
override
custom
custom
BUILD SUCCESSFUL
Total time: 1.469 secs

Defining properties using an external file

我们还可以在外部文件中设置项目的属性。该文件需要命名为 gradle.properties,并且它应该是一个纯文本文件,属性名称及其值位于不同的行中。我们可以将文件放在项目目录或 Gradle 用户主目录中。默认的 Gradle 用户主目录是 $USER_HOME/.gradle。 Gradle 用户主目录中的 properties 文件中定义的属性会覆盖项目目录中属性文件中定义的属性值。

我们现在将在我们的项目目录中创建一个 gradle.properties 文件,其内容如下。

我们使用我们的构建文件来显示属性值:

task showProperties { 
    doLast { 
        println "Version: $version" 
        println "Custom property: $customProperty" 
    } 
} 

如果我们运行构建文件,我们不需要传递任何命令行选项,Gradle 将使用 gradle.properties 来获取属性值:

$ gradle showProperties
:showProperties
Version: 4.0
Custom property: Property value from gradle.properties
BUILD SUCCESSFUL
Total time: 1.676 secs

Passing properties via the command line

我们可以使用 -P 命令行选项为构建添加额外的属性,而不是直接在构建脚本或外部文件中定义属性。我们还可以使用 -P 命令行选项来设置现有属性的值。如果我们使用 -P命令行选项定义一个属性,我们可以覆盖一个在外部 gradle中定义的同名属性.properties 文件。

以下构建脚本有一个 showProperties 任务,它显示现有属性和新属性的值:

task showProperties { 
    doLast { 
        println "Version: $version" 
        println "Custom property: $customProperty" 
    } 
} 

让我们运行我们的脚本并传递现有 version 属性和不存在的  customProperty 的值:

$ gradle -Pversion=1.1 -PcustomProperty=custom showProperties
:showProperties
Version: 1.1
Custom property: custom
BUILD SUCCESSFUL
Total time: 1.412 secs

Defining properties via system properties

我们还可以使用 Java 系统属性为我们的 Gradle 构建定义属性。我们使用 -D 命令行选项,就像在普通 Java 应用程序中一样。系统属性的名称必须以 org.gradle.project开头,后跟我们要设置的属性的名称,然后是值。

我们可以使用之前创建的相同构建脚本:

task showProperties { 
    doLast { 
        println "Version: $version" 
        println "Custom property: $customProperty" 
    } 
} 

但是,这次我们使用不同的命令行选项来获得结果:

$ gradle -Dorg.gradle.project.version=2.0 -Dorg.gradle.project.customProperty=custom showProperties
:showProperties
Version: 2.0
Custom property: custom
BUILD SUCCESSFUL
Total time: 1.218 secs

Adding properties via environment variables

使用命令行选项提供了很大的灵活性;但是,有时由于环境限制或我们不想在每次调用 Gradle 构建时重新键入完整的命令行选项,我们无法使用命令行选项。 Gradle 还可以使用在操作系统中设置的环境变量将属性传递给 Gradle 构建。

环境变量名称以 ORG_GRADLE_PROJECT_ 开头,后跟属性名称。我们使用我们的构建文件来显示属性:

task showProperties { 
    doLast { 
        println "Version: $version" 
        println "Custom property: $customProperty" 
    } 
} 

首先,我们设置 ORG_GRADLE_PROJECT_version 和 ORG_GRADLE_PROJECT_customProperty 环境变量,然后我们运行我们的 showProperties< /code> 任务,如下:

$ ORG_GRADLE_PROJECT_version=3.1 \
ORG_GRADLE_PROJECT_customProperty="Set by environment variable" \
gradle showProp
:showProperties
Version: 3.1
Custom property: Set by environment variable
BUILD SUCCESSFUL
Total time: 1.373 secs

Using logging


第 1 章 中, 从 Gradle 开始 ,我们讨论了几个命令行选项,我们可以在运行 Gradle 构建时使用它们来显示更多或更少的日志消息。这些消息来自 Gradle 内部任务和类。我们在 Gradle 构建脚本中使用了一个println 方法来查看一些输出,但我们也可以使用 Gradle 的日志机制来拥有一种更可定制的方式来定义日志消息。

Gradle 支持多个日志级别,我们可以将这些级别用于我们自己的消息。我们的消息级别很重要,因为我们可以使用命令行选项来过滤日志级别的消息。

下表显示了 Gradle 支持的日志级别:

级别

用于

调试

调试消息

INFO

信息消息

生命周期

进度信息消息

警告

警告信息

安静

导入信息消息

错误

错误信息

每个 Gradle 构建文件和任务都有一个 logger 对象。  logger 对象是 Java 的简单日志门面的 Gradle 特定扩展的实例span> (SLF4J记录器界面。 SLF4J 是一个 Java 日志库。这个库提供了一个独立于底层日志框架的日志 API。可以在部署时或运行时使用特定的日志框架来输出实际的日志消息。

要在我们的 Gradle 构建文件中使用 logger 对象,我们只需要引用 logger 并调用日志级别的方法我们想要使用,或者我们可以使用常见的 log()方法,并将日志级别作为参数传递给该方法。

让我们创建一个简单的任务并使用不同的日志级别:

// Simple logging sample. 
task logLevels << { 
    logger.debug 'debug: Most verbose logging level' 
    logger.log LogLevel.DEBUG, 'debug: Most verbose logging level' 
 
    logger.info 'info: Use for information messages' 
    logger.log LogLevel.INFO, 'info: Use for information messages' 
 
    logger.lifecycle 'lifecycle: Progress information messages' 
    logger.log LogLevel.LIFECYCLE, 
      'lifecycle: Progress information messages' 
 
    logger.warn 'warn: Warning messages like invalid        configuration' 
    logger.log LogLevel.WARN, 
      'warn: Warning messages like invalid configuration' 
 
    logger.quiet 'quiet: This is important but not an error' 
    logger.log LogLevel.QUIET, 
      'quiet: This is important but not an error' 
 
    logger.error 'error: Use for errors' 
    logger.log LogLevel.ERROR, 'error: Use for errors' 
} 

当我们从命令行运行这个 logLevels 任务时,我们得到以下输出:

$ gradle logLevels
:logLevels
lifecycle: Progress information messages
lifecycle: Progress information messages
warn: Warning messages like invalid configuration
warn: Warning messages like invalid configuration
quiet: This is important but not an error
quiet: This is important but not an error
BUILD SUCCESSFUL
Total time: 1.523 secs

我们注意到只有 LIFECYCLEWARNQUIET , 如果我们不添加任何额外的命令行选项,则会显示ERROR 日志级别。要查看 INFO 消息,我们必须使用 --info 命令行选项。然后我们得到以下输出:

$ gradle --info logLevels
...
Starting Build
Settings evaluated using empty settings script.
...
Selected primary task 'logLevels' from project :
Tasks to be executed: [task ':logLevels']
...
:logLevels (Thread[Daemon worker Thread 15,5,main]) started.
:logLevels 
Executing task ':logLevels' (up-to-date check took 0.0   secs) due  to:
  Task has not declared any outputs.
info: Use for information messages
info: Use for information messages
lifecycle: Progress information messages
lifecycle: Progress information messages
warn: Warning messages like invalid configuration
warn: Warning messages like invalid configuration
quiet: This is important but not an error
quiet: This is important but not an error
:logLevels (Thread[Daemon worker Thread 15,5,main]) completed. Took 0.001 secs.
BUILD SUCCESSFUL
Total time: 1.465 secs

请注意,我们还从 Gradle 本身获得了更多消息。早些时候,我们只看到了来自脚本的日志消息,但这次显示了很多关于构建过程本身的额外日志。

要获得更多输出和我们的 DEBUG 级别的日志消息,我们必须使用 --debug 命令行选项来调用 logLevels 任务,如下:

$ gradle --debug logLevels
...
08:16:46.334 [DEBUG] [org.gradle.initialization.buildsrc.BuildSourceBuilder] Starting to build the build sources.
08:16:46.335 [DEBUG] [org.gradle.initialization.buildsrc.BuildSourceBuilder] Gradle source dir does not exist. We leave.
08:16:46.335 [DEBUG] [org.gradle.initialization.DefaultGradlePropertiesLoader] Found env project properties: []
08:16:46.335 [DEBUG] [org.gradle.initialization.DefaultGradlePropertiesLoader] Found system project properties: []
08:16:46.336 [DEBUG] [org.gradle.initialization.ScriptEvaluatingSettingsProcessor] Timing: Processing settings took: 0.001 secs
08:16:46.338 [DEBUG] [org.gradle.initialization.buildsrc.BuildSourceBuilder] Starting to build the build sources.
08:16:46.339 [DEBUG] [org.gradle.initialization.buildsrc.BuildSourceBuilder] Gradle source dir does not exist. We leave.
08:16:46.339 [DEBUG] [org.gradle.initialization.DefaultGradlePropertiesLoader] Found env project properties: []
08:16:46.339 [DEBUG] [org.gradle.initialization.DefaultGradlePropertiesLoader] Found system project properties: []
08:16:46.340 [DEBUG] [org.gradle.initialization.ScriptEvaluatingSettingsProcessor] Timing: Processing settings took: 0.001 secs
08:16:46.340 [INFO] [org.gradle.BuildLogger] Settings evaluated using empty settings script.
08:16:46.341 [DEBUG] [org.gradle.model.internal.registry.DefaultModelRegistry] Transitioning model element 'tasks' from state Known to Created
08:16:46.341 [DEBUG] [org.gradle.model.internal.registry.DefaultModelRegistry] Running model element 'tasks' creator rule action Project.<init>.tasks()
08:16:46.341 [DEBUG] [org.gradle.model.internal.registry.DefaultModelRegistry] Creating tasks using Project.<init>.tasks()
...
08:16:46.355 [INFO] [org.gradle.execution.taskgraph.AbstractTaskPlanExecutor] :logLevels (Thread[Daemon worker Thread 16,5,main]) started.
08:16:46.356 [LIFECYCLE] [class org.gradle.TaskExecutionLogger] :logLevels
08:16:46.356 [DEBUG] [org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter] Starting to execute task ':logLevels'
08:16:46.356 [DEBUG] [org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter] Determining if task ':logLevels' is up-to-date
08:16:46.356 [INFO] [org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter] Executing task ':logLevels' (up-to-date check took 0.0        secs) due to:
  Task has not declared any outputs.
08:16:46.356 [DEBUG] [org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter] Executing actions for task ':logLevels'.
08:16:46.357 [DEBUG] [org.gradle.api.Task] debug: Most verbose logging level
08:16:46.357 [DEBUG] [org.gradle.api.Task] debug: Most verbose logging level
08:16:46.357 [INFO] [org.gradle.api.Task] info: Use for information messages
08:16:46.357 [INFO] [org.gradle.api.Task] info: Use for information messages
08:16:46.357 [LIFECYCLE] [org.gradle.api.Task] lifecycle: Progress information messages
08:16:46.357 [LIFECYCLE] [org.gradle.api.Task] lifecycle: Progress information messages
08:16:46.357 [WARN] [org.gradle.api.Task] warn: Warning messages like invalid configuration
08:16:46.357 [WARN] [org.gradle.api.Task] warn: Warning messages like invalid configuration
08:16:46.357 [QUIET] [org.gradle.api.Task] quiet: This is important but not an error
08:16:46.358 [QUIET] [org.gradle.api.Task] quiet: This is important but not an error
08:16:46.358 [DEBUG] [org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter] Finished executing task ':logLevels'
08:16:46.358 [INFO] [org.gradle.execution.taskgraph.AbstractTaskPlanExecutor] :logLevels (Thread[Daemon worker Thread 16,5,main]) completed. Took 0.002 secs.
08:16:46.358 [DEBUG] [org.gradle.execution.taskgraph.AbstractTaskPlanExecutor] Task worker [Thread[Daemon worker Thread 16,5,main]] finished, busy: 0.002 secs, idle: 0.001 secs
08:16:46.358 [DEBUG] [org.gradle.execution.taskgraph.DefaultTaskGraphExecuter] Timing: Executing the DAG took 0.004 secs
08:16:46.358 [LIFECYCLE] [org.gradle.BuildResultLogger]
08:16:46.359 [LIFECYCLE] [org.gradle.BuildResultLogger] BUILD SUCCESSFUL
08:16:46.359 [LIFECYCLE] [org.gradle.BuildResultLogger]
08:16:46.359 [LIFECYCLE] [org.gradle.BuildResultLogger] Total time: 1.424 secs

这一次,我们收到了很多信息,我们真的必须仔细寻找我们自己的信息。日志的输出格​​式也发生了变化,请注意,虽然之前只显示了日志消息,但现在还显示了日志消息的时间、日志级别和原始类。

因此,我们知道每个 Gradle 项目和任务都有一个我们可以使用的记录器。但是,我们也可以使用Logging 类显式创建一个logger 实例。例如,如果我们定义自己的类并希望在 Gradle 构建中使用它,我们可以使用  Logging 类来获取 Gradle logger 对象。我们可以在这个 上使用额外的 lifecycle()和 quiet()方法logger 实例,就像在项目和任务中一样。

我们现在将在我们的构建文件中添加一个类定义,并使用这个类的一个实例来查看输出:

class Simple { 
 
    // Create new logger using the Gradle 
    // logging support. 
    private static final Logger logger = Logging.getLogger('Simple') 
 
    int square(int value) { 
        int square = value * value 
        logger.lifecycle "Calculate square for ${value} = ${square}" 
        return square 
    } 
 
} 
 
logger.lifecycle 'Running sample Gradle build.' 
 
task useSimple { 
    doFirst { 
        logger.lifecycle 'Running useSimple' 
    } 
    doLast { 
        new Simple().square(3) 
    } 
} 

我们使用了项目和任务的logger;在类 Simple中,我们使用 Logging.getLogger()来创建一个Gradle 记录器实例。当我们运行我们的脚本时,我们得到以下输出:

$ gradle useSimple
Running sample Gradle build.
:useSimple
Running useSimple
Calculate square for 3 = 9
BUILD SUCCESSFUL
Total time: 1.363 secs

要查看记录器的原始类,我们可以使用 --debug(或 -d)命令行选项.然后,我们不仅会看到记录消息的时间,还会看到记录器的名称:

$ gradle useSimple -d
...
08:28:18.390 [LIFECYCLE] [org.gradle.api.Project] Running sample Gradle build.
...
08:28:18.405 [LIFECYCLE] [org.gradle.api.Task] Running useSimple
08:28:18.406 [LIFECYCLE] [Simple] Calculate square for 3 = 9
...

请注意,我们的 logger 项目名为 org.gradle.api.Project,即  logger任务被命名为 org.gradle.api.Task,而我们的 logger在 Simple 类被命名为 Simple

Controlling output

在前面的示例中,我们使用 logger 实例和 println() 方法来记录消息。 Gradle 将发送的输出重定向到 System.out—€”这是我们在使用时所做的 println()—到具有 quiet 日志级别的记录器。这就是为什么我们在运行 Gradle 构建时会看到 println() 输出的原因。 Gradle 拦截输出并使用其日志记录支持。

当我们使用 --debug 选项运行以下简单的 Gradle 构建时,我们可以看到 Gradle 已将输出重定向到 QUIET 日志级别:

println 'Simple logging message' 

如果我们运行构建,让我们看看输出:

$ gradle --debug
...
08:34:13.497 [QUIET] [system.out] Simple logging message
...

Gradle 使用 ERROR 日志级别将标准错误重定向到日志消息。这也适用于我们在 Gradle 构建中从外部库中使用的类。如果这些库中的代码使用标准输出和错误,Gradle 将捕获输出和错误消息并将它们重定向到 logger 实例。

如果我们想更改用于重定向输出和错误消息的日志级别,我们可以自己配置。每个项目和任务都有一个名为 logging 的 org.gradle.api.logging.LoggingManager 类的实例。 LoggingManager 类有方法 captureStandardOutput()captureStandardError()我们可以使用来设置输出和错误消息的日志级别。 请记住,Gradle 默认使用 QUIET日志级别来输出消息和&n​​bsp;ERROR 错误消息的日志级别。在以下脚本中,我们将输出消息的日志级别更改为 INFO

logging.captureStandardOutput LogLevel.INFO 
println 'This message is now logged with log level info instead of quiet' 
 
task redirectLogging { 
    doFirst { 
      // Use default redirect log level quiet. 
      println 'Start task redirectLogging' 
    } 
    doLast { 
      logging.captureStandardOutput LogLevel.INFO 


      println 'Finished task redirectLogging' 
    } 
} 

首先,我们在没有任何额外命令行选项的情况下运行构建:

$ gradle redirectLogging
:redirectLogging
Start task redirectLogging
BUILD SUCCESSFUL
Total time: 1.448 secs

请注意,我们在任务的 doFirst 方法中定义的 println 语句已显示,但其他 的输出;println 语句未显示。我们将这些 println 语句的输出重定向到具有 INFO 日志级别的 Gradle 日志记录。  INFO 日志级别现在默认显示。

让我们再次运行脚本,但现在我们添加 --info 命令行选项,以便我们可以看到我们的 println 的所有输出 语句:

$ gradle --info redirectLogging
...
Start task redirectLogging
Finished task redirectLogging
...

Using the Gradle Wrapper


通常,如果我们想运行 Gradle 构建,我们必须在我们的计算机上安装 Gradle。此外,如果我们将项目分发给其他人并且他们想要构建项目,他们必须在他们的计算机上安装 Gradle。 Gradle Wrapper 可用于允许其他人构建我们的项目,即使他们的计算机上没有安装 Gradle。

包装器是 Microsoft Windows 操作系统上的批处理脚本或其他操作系统上的 shell 脚本,它将下载 Gradle 并使用下载的 Gradle 运行构建。

通过使用包装器,我们可以确保使用项目的正确 Gradle 版本。我们可以定义 Gradle 版本,如果我们通过包装脚本文件运行构建,则使用我们定义的 Gradle 版本。

Creating wrapper scripts

要创建 Gradle Wrapper 批处理和 shell 脚本,我们可以调用内置的 wrapper 任务。如果我们在计算机上安装了 Gradle,则此任务已经可用。让我们从命令行调用 wrapper 任务:

$ gradle wrapper
:wrapper
BUILD SUCCESSFUL
Total time: 0.61 secs

任务执行后,我们有两个脚本文件——€”gradlew.batgradlew——€”在我们项目目录的根目录。这些脚本包含运行 Gradle 所需的所有逻辑。如果 Gradle 尚未下载,Gradle 发行版将被下载并安装在本地。

gradle/wrapper 目录中,相对于我们的项目目录,我们找到了 gradle-wrapper.jar和 gradle-wrapper.properties 文件。  gradle-wrapper.jar 文件包含下载和调用 Gradle 所需的几个类文件。  gradle-wrapper.properties 文件包含下载 Gradle 的设置,例如 URL。  gradle-wrapper.properties 文件还包含 Gradle 版本号。如果发布了新的 Gradle 版本,我们只需要在 gradle-wrapper.properties 文件中更改版本,Gradle Wrapper 就会下载新版本以便我们使用它来构建我们的项目。

所有生成的文件现在都是我们项目的一部分。如果我们使用版本控制系统,那么我们必须将这些文件添加到版本控制中。检查我们项目的其他人可以使用 gradlew 脚本来执行项目中的任务。下载指定的 Gradle 版本并用于运行构建文件。

如果我们想使用另一个 Gradle 版本,我们可以使用 --gradle-version 选项调用 wrapper 任务。我们必须指定为其生成 Wrapper 文件的 Gradle 版本。默认情况下,用于调用 wrapper 任务的 Gradle 版本是包装文件使用的 Gradle 版本。

要为 Gradle 安装文件指定不同的下载位置,我们必须使用 wrapper--gradle-distribution-url 选项代码>任务。例如,我们可以在本地 Intranet 上安装自定义的 Gradle,使用此选项,我们可以生成将使用 Intranet 上的 Gradle 分发的 Wrapper 文件。

在以下示例中,我们为 Gradle 2.12 显式生成包装文件:

$ gradle wrapper --gradle-version=2.12
:wrapper
BUILD SUCCESSFUL
Total time: 0.61 secs

Customizing the Gradle Wrapper

如果我们想自定义内置 wrapper 任务的属性,我们必须使用 org. 添加一个新任务到我们的 Gradle 构建文件中。 gradle.api.tasks.wrapper.Wrapper 类型。我们不会更改默认的 wrapper 任务,而是使用我们想要应用的新设置创建一个新任务。我们需要使用我们的新任务来生成 Gradle Wrapper shell 脚本和支持文件。

我们可以更改使用 Wrapper 任务的 scriptFile 属性生成的脚本文件的名称。要更改生成的 JAR 和属性文件的名称和位置,我们可以更改 jarFile 属性:

task createWrapper(type: Wrapper) { 
    // Set Gradle version for wrapper files. 
    gradleVersion = '2.12' 
 
    // Rename shell scripts name to 
    // startGradle instead of default gradlew. 
    scriptFile = 'startGradle' 
 
    // Change location and name of JAR file 
    // with wrapper bootstrap code and 
    // accompanying properties files. 
    jarFile = "${projectDir}/gradle-bin/gradle-bootstrap.jar" 
} 

如果我们运行 createWrapper 任务,我们会得到一个 Windows 批处理文件和 shell 脚本,并且带有属性文件的 Wrapper 引导 JAR 文件存储在 gradle-bin 目录:

$ gradle createWrapper
:createWrapper
BUILD SUCCESSFUL
Total time: 0.605 secs
$ tree .
.
├── gradle-bin
│ ├── gradle-bootstrap.jar
│ └── gradle-bootstrap.properties
├── startGradle
├── startGradle.bat
└── build.gradle
2 directories, 5 files

要更改必须下载 Gradle 版本的 URL,我们可以更改 distributionUrl 属性。例如,我们可以在我们公司的 Intranet 上发布一个固定的 Gradle 版本,并使用 distributionUrl 属性来引用我们 Intranet 上的下载 URL。这样我们可以确保公司的所有开发人员都使用相同的 Gradle 版本:

task createWrapper(type: Wrapper) { 
    // Set URL with custom Gradle distribution. 
    distributionUrl = 'http://intranet/gradle/dist/gradle-custom-       2.12.zip' 
} 

Summary


在本章中,我们讨论了 Gradle 在处理文件时提供的支持。我们看到了如何创建文件或目录以及文件和目录的集合。文件树表示一组分层的文件。

我们可以将日志消息添加到我们的项目和任务中,并在运行 Gradle 构建时查看输出。我们讨论了如何使用不同的日志级别来影响输出中显示的信息量。我们还使用 LoggingManager 来捕获标准输出和错误消息,并将它们重定向到自定义日志级别。

我们讨论了如何使用 Gradle Wrapper 来允许用户构建我们的项目,即使他们没有安装 Gradle。我们讨论了如何自定义 Wrapper 以下载特定版本的 Gradle 并使用它来运行我们的构建。

在下一章中,我们将创建一个 Java 项目并使用 Java 插件添加一组默认任务,我们可以使用这些任务来编译、测试和打包我们的 Java 代码。