可操作任务描述了 Gradle 中的工作。这些任务有操作。在 Gradle 核心,compileJava 任务编译 Java 源代码。JarZip 任务将文件压缩到存档中。

writing tasks 3

可以通过扩展 DefaultTask 类并定义输入、输出和操作来创建自定义可操作任务。

任务输入和输出

可操作任务有输入和输出。输入和输出可以是文件、目录或变量。

在可操作任务中

  • 输入由文件、文件夹和/或配置数据集合组成。
    例如,javaCompile 任务获取输入,例如 Java 源文件和构建脚本配置,如 Java 版本。

  • 输出是指一个或多个文件或文件夹。
    例如,javaCompile 生成类文件作为输出。

然后,jar 任务将这些类文件作为输入并生成 JAR 存档。

明确定义的任务输入和输出有两个目的

  1. 它们向 Gradle 通知任务依赖关系。
    例如,如果 Gradle 了解 compileJava 任务的输出作为 jar 任务的输入,它将优先运行 compileJava

  2. 它们促进了增量构建。
    例如,假设 Gradle 识别到任务的输入和输出保持不变。在这种情况下,它可以利用以前构建运行或构建缓存的结果,完全避免重新运行任务操作。

当您应用 java-library 插件之类的插件时,Gradle 将自动注册一些任务并使用默认值配置它们。

让我们定义一个任务,将 JAR 和启动脚本打包到一个虚构示例项目中的存档中

gradle-project
├── app
│   ├── build.gradle.kts    // app build logic
│   ├── run.sh              // script file
│   └── ...                 // some java code
├── settings.gradle.kts     // includes app subproject
├── gradle
├── gradlew
└── gradlew.bat
gradle-project
├── app
│   ├── build.gradle    // app build logic
│   ├── run.sh          // script file
│   └── ...             // some java code
├── settings.gradle     // includes app subproject
├── gradle
├── gradlew
└── gradlew.bat

run.sh 脚本可以从构建中执行 Java 应用程序(一旦打包为 JAR)

app/run.sh
java -cp 'libs/*' gradle.project.app.App

让我们使用 task.register() 注册一个名为 packageApp 的新任务

app/build.gradle.kts
tasks.register<Zip>("packageApp") {

}
app/build.gradle
tasks.register(Zip, "packageApp") {

}

我们使用了 Gradle 核心中的现有实现,即 Zip 任务实现(即 DefaultTask 的子类)。由于我们在此处注册了一个新任务,因此它未预先配置。我们需要配置输入和输出。

定义输入和输出是使任务成为可操作任务的原因。

对于 Zip 任务类型,我们可以使用 from() 方法将文件添加到输入中。在我们的示例中,我们添加了运行脚本。

如果输入是我们直接创建或编辑的文件,例如运行文件或 Java 源代码,它通常位于我们项目目录中的某个位置。为了确保我们使用正确的位置,我们使用 layout.projectDirectory 并定义相对于项目目录根的相对路径。

我们提供 jar 任务的输出以及所有依赖项的 JAR(使用 configurations.runtimeClasspath)作为附加输入。

对于输出,我们需要定义两个属性。

首先,目标目录,它应该是构建文件夹中的一个目录。我们可以通过 layout 访问它。

其次,我们需要为 zip 文件指定一个名称,我们称之为 myApplication.zip

以下是完整任务的样子

app/build.gradle.kts
val packageApp = tasks.register<Zip>("packageApp") {
    from(layout.projectDirectory.file("run.sh"))                // input - run.sh file
    from(tasks.jar) {                                           // input - jar task output
        into("libs")
    }
    from(configurations.runtimeClasspath) {                     // input - jar of dependencies
        into("libs")
    }
    destinationDirectory.set(layout.buildDirectory.dir("dist")) // output - location of the zip file
    archiveFileName.set("myApplication.zip")                    // output - name of the zip file
}
app/build.gradle
def packageApp = tasks.register(Zip, 'packageApp') {
    from layout.projectDirectory.file('run.sh')                 // input - run.sh file
    from tasks.jar {                                            // input - jar task output
        into 'libs'
    }
    from configurations.runtimeClasspath {                      // input - jar of dependencies
        into 'libs'
    }
    destinationDirectory.set(layout.buildDirectory.dir('dist')) // output - location of the zip file
    archiveFileName.set('myApplication.zip')                    // output - name of the zip file
}

如果我们运行 packageApp 任务,则会生成 myApplication.zip

$./gradlew :app:packageApp

> Task :app:compileJava
> Task :app:processResources NO-SOURCE
> Task :app:classes
> Task :app:jar
> Task :app:packageApp

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed

Gradle 执行了构建 JAR 文件所需的许多任务,其中包括编译 app 项目的代码和编译代码依赖项。

查看新创建的 ZIP 文件,我们可以看到它包含运行 Java 应用程序所需的一切

> unzip -l ./app/build/dist/myApplication.zip

Archive:  ./app/build/dist/myApplication.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       42  01-31-2024 14:16   run.sh
        0  01-31-2024 14:22   libs/
      847  01-31-2024 14:22   libs/app.jar
  3041591  01-29-2024 14:20   libs/guava-32.1.2-jre.jar
     4617  01-29-2024 14:15   libs/failureaccess-1.0.1.jar
     2199  01-29-2024 14:15   libs/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
    19936  01-29-2024 14:15   libs/jsr305-3.0.2.jar
   223979  01-31-2024 14:16   libs/checker-qual-3.33.0.jar
    16017  01-31-2024 14:16   libs/error_prone_annotations-2.18.0.jar
---------                     -------
  3309228                     9 files

可操作任务应连接到生命周期任务,以便开发人员只需要运行生命周期任务。

到目前为止,我们直接调用了我们的新任务。让我们将其连接到生命周期任务。

将以下内容添加到构建脚本中,以便使用 dependsOn()packageApp 可操作任务连接到 build 生命周期任务

app/build.gradle.kts
tasks.build {
    dependsOn(packageApp)
}
app/build.gradle
tasks.build {
    dependsOn(packageApp)
}

我们看到运行 :build 也运行 :packageApp

$ ./gradlew :app:build

> Task :app:compileJava UP-TO-DATE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar UP-TO-DATE
> Task :app:startScripts
> Task :app:distTar
> Task :app:distZip
> Task :app:assemble
> Task :app:compileTestJava
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses
> Task :app:test
> Task :app:check
> Task :app:packageApp
> Task :app:build

BUILD SUCCESSFUL in 1s
8 actionable tasks: 6 executed, 2 up-to-date

如有需要,您可以定义自己的生命周期任务。

通过扩展 DefaultTask 来实现任务

为了满足更多个性化需求,并且如果现有插件未提供您需要的构建功能,您可以创建自己的任务实现。

实现一个类意味着创建一个自定义类(即,类型),这是通过对 DefaultTask 进行子类化来完成的

让我们从 Gradle init 为一个简单的 Java 应用程序构建的示例开始,该应用程序的源代码位于 app 子项目中,通用构建逻辑位于 buildSrc

gradle-project
├── app
│   ├── build.gradle.kts
│   └── src                 // some java code
│       └── ...
├── buildSrc
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── src                 // common build logic
│       └── ...
├── settings.gradle.kts
├── gradle
├── gradlew
└── gradlew.bat
gradle-project
├── app
│   ├── build.gradle
│   └── src             // some java code
│       └── ...
├── buildSrc
│   ├── build.gradle
│   ├── settings.gradle
│   └── src             // common build logic
│       └── ...
├── settings.gradle
├── gradle
├── gradlew
└── gradlew.bat

我们在 ./buildSrc/src/main/kotlin/GenerateReportTask.kt./buildSrc/src/main/groovy/GenerateReportTask.groovy 中创建一个名为 GenerateReportTask 的类。

为了让 Gradle 知道我们正在实现一个任务,我们扩展了 Gradle 附带的 DefaultTask 类。让我们的任务类成为 abstract 也是有益的,因为 Gradle 会自动处理很多事情

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask

public abstract class GenerateReportTask : DefaultTask() {

}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask

public abstract class GenerateReportTask extends DefaultTask {

}

接下来,我们使用属性和注释来定义输入和输出。在此上下文中,Gradle 中的属性充当其背后实际值的引用,允许 Gradle 跟踪任务之间的输入和输出。

对于任务的输入,我们使用 Gradle 中的 DirectoryProperty。我们使用 @InputDirectory 对其进行注释,以表明它是任务的输入

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory

public abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    lateinit var sourceDirectory: File

}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory

public abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    File sourceDirectory

}

类似地,对于输出,我们使用 RegularFileProperty 并使用 @OutputFile 对其进行注释。

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile

public abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    lateinit var sourceDirectory: File

    @get:OutputFile
    lateinit var reportFile: File

}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile

public abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    File sourceDirectory

    @OutputFile
    File reportFile

}

在定义了输入和输出后,剩下的唯一内容就是实际的任务操作,它在使用 @TaskAction 进行注释的方法中实现。在此方法中,我们编写了使用特定于 Gradle 的 API 访问输入和输出的代码

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

public abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    lateinit var sourceDirectory: File

    @get:OutputFile
    lateinit var reportFile: File

    @TaskAction
    fun generateReport() {
        val fileCount = sourceDirectory.listFiles().count { it.isFile }
        val directoryCount = sourceDirectory.listFiles().count { it.isDirectory }

        val reportContent = """
            |Report for directory: ${sourceDirectory.absolutePath}
            |------------------------------
            |Number of files: $fileCount
            |Number of subdirectories: $directoryCount
        """.trimMargin()

        reportFile.writeText(reportContent)
        println("Report generated at: ${reportFile.absolutePath}")
    }
}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

public abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    File sourceDirectory

    @OutputFile
    File reportFile

    @TaskAction
    void generateReport() {
        def fileCount = sourceDirectory.listFiles().count { it.isFile() }
        def directoryCount = sourceDirectory.listFiles().count { it.isDirectory() }

        def reportContent = """
            Report for directory: ${sourceDirectory.absolutePath}
            ------------------------------
            Number of files: $fileCount
            Number of subdirectories: $directoryCount
        """.trim()

        reportFile.text = reportContent
        println("Report generated at: ${reportFile.absolutePath}")
    }
}

任务操作生成 sourceDirectory 中文件的一个报告。

在应用程序构建文件中,我们使用 task.register() 注册一个类型为 GenerateReportTask 的任务,并将其命名为 generateReport。同时,我们配置任务的输入和输出

app/build.gradle.kts
tasks.register<GenerateReportTask>("generateReport") {
    sourceDirectory = file("src/main")
    reportFile = file("${layout.buildDirectory}/reports/directoryReport.txt")
}

tasks.build {
    dependsOn("generateReport")
}
app/build.gradle
import org.gradle.api.tasks.Copy

tasks.register(GenerateReportTask, "generateReport") {
    sourceDirectory = file("src/main")
    reportFile = file("${layout.buildDirectory}/reports/directoryReport.txt")
}

tasks.build.dependsOn("generateReport")

generateReport 任务连接到 build 任务。

通过运行构建,我们观察到我们的启动脚本生成任务已执行,并且在后续构建中是 UP-TO-DATE。Gradle 的增量构建和缓存机制与自定义任务无缝协作

./gradlew :app:build
> Task :buildSrc:checkKotlinGradlePluginConfigurationErrors
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources NO-SOURCE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :app:compileJava UP-TO-DATE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar UP-TO-DATE
> Task :app:startScripts UP-TO-DATE
> Task :app:distTar UP-TO-DATE
> Task :app:distZip UP-TO-DATE
> Task :app:assemble UP-TO-DATE
> Task :app:compileTestJava UP-TO-DATE
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses UP-TO-DATE
> Task :app:test UP-TO-DATE
> Task :app:check UP-TO-DATE

> Task :app:generateReport
Report generated at: ./app/build/reports/directoryReport.txt

> Task :app:packageApp
> Task :app:build

BUILD SUCCESSFUL in 1s
13 actionable tasks: 10 executed, 3 up-to-date

任务操作

任务操作是实现任务所执行操作的代码,如前一节所述。例如,javaCompile 任务操作调用 Java 编译器将源代码转换为字节代码。

可以动态修改已注册任务的任务操作。这有助于测试、修补或修改核心构建逻辑。

我们来看一个简单的 Gradle 构建示例,其中一个 app 子项目构成了一个 Java 应用程序——包含一个 Java 类并使用 Gradle 的 application 插件。该项目在 buildSrc 文件夹中具有通用构建逻辑,其中包含 my-convention-plugin

app/build.gradle.kts
plugins {
    id("my-convention-plugin")
}

version = "1.0"

application {
    mainClass = "org.example.app.App"
}
app/build.gradle
plugins {
    id 'my-convention-plugin'
}

version = '1.0'

application {
    mainClass = 'org.example.app.App'
}

我们在 app 的构建文件中定义了一个名为 printVersion 的任务

buildSrc/src/main/kotlin/PrintVersion.kt
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

abstract class PrintVersion : DefaultTask() {

    // Configuration code
    @get:Input
    abstract val version: Property<String>

    // Execution code
    @TaskAction
    fun print() {
        println("Version: ${version.get()}")
    }
}
buildSrc/src/main/groovy/PrintVersion.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

abstract class PrintVersion extends DefaultTask {

    // Configuration code
    @Input
    abstract Property<String> getVersion()

    // Execution code
    @TaskAction
    void printVersion() {
        println("Version: ${getVersion().get()}")
    }
}

此任务执行一项简单操作:将项目的版本打印到命令行。

该类扩展了 DefaultTask,并且有一个 @Input,类型为 Property<String>。它有一个使用 @TaskAction 进行注释的方法,用于打印版本。

请注意,任务实现明确区分了“配置代码”和“执行代码”。

配置代码在 Gradle 的配置阶段执行。它在内存中构建一个项目的模型,以便 Gradle 知道它需要针对某个构建调用执行什么操作。任务操作周围的一切内容,如输入或输出属性,都是此配置代码的一部分。

任务操作方法内的代码是执行实际工作的执行代码。如果任务是任务图的一部分,并且因为它 UP-TO-DATE 或已 FROM-CACHE 而无法跳过,则它将访问输入和输出来执行一些工作。

一旦任务实现完成,它就可以在构建设置中使用。在我们的约定插件 my-convention-plugin 中,我们可以注册一个使用新任务实现的新任务

app/build.gradle.kts
tasks.register<PrintVersion>("printVersion") {

    // Configuration code
    version = project.version as String
}
app/build.gradle
tasks.register(PrintVersion, "printVersion") {

    // Configuration code
    version = project.version.toString()
}

在任务的配置块中,我们可以编写配置阶段代码,修改任务的输入和输出属性的值。此处未以任何方式引用任务操作。

可以以更简洁的方式直接在构建脚本中编写像这样的简单任务,而无需为任务创建单独的类。

让我们注册另一个任务并将其称为 printVersionDynamic

这一次,我们不为任务定义类型,这意味着任务将为一般类型 DefaultTask。此一般类型未定义任何任务操作,这意味着它没有用 @TaskAction 注释的方法。此类型对于定义“生命周期任务”很有用

app/build.gradle.kts
tasks.register("printVersionDynamic") {

}
app/build.gradle
tasks.register("printVersionDynamic") {

}

但是,默认任务类型还可用于动态定义具有自定义操作的任务,而无需其他类。这是通过使用 doFirst{}doLast{} 构造完成的。类似于定义方法并用 @TaskAction 注释它,这会向任务添加一个操作。

这些方法被称为 doFirst{}doLast{},因为任务可以具有多个操作。如果任务已经定义了一个操作,则可以使用此区分来决定附加操作应在现有操作之前还是之后运行

app/build.gradle.kts
tasks.register("printVersionDynamic") {
    doFirst {
        // Task action = Execution code
        // Run before exiting actions
    }
    doLast {
        // Task action = Execution code
        // Run after existing actions
    }
}
app/build.gradle
tasks.register("printVersionDynamic") {
    doFirst {
        // Task action = Execution code
        // Run before exiting actions
    }
    doLast {
        // Task action = Execution code
        // Run after existing actions
    }
}

如果只有一个操作,这是这里的情况,因为我们从一个空任务开始,我们通常使用 doLast{} 方法。

在任务中,我们首先将要打印的版本动态声明为输入。我们不声明属性并用 @Input 注释它,而是使用所有任务都具有的常规输入属性。然后,我们在 doLast{} 方法中添加操作代码,即 println() 语句

app/build.gradle.kts
tasks.register("printVersionDynamic") {
    inputs.property("version", project.version.toString())
    doLast {
        println("Version: ${inputs.properties["version"]}")
    }
}
app/build.gradle
tasks.register("printVersionDynamic") {
    inputs.property("version", project.version)
    doLast {
        println("Version: ${inputs.properties["version"]}")
    }
}

我们看到了在 Gradle 中实现自定义任务的两种替代方法。

动态设置使其更紧凑。但是,编写动态任务时很容易混淆配置和执行时间状态。您还可以看到动态任务中的“输入”是未输入的,这可能会导致问题。当您将自定义任务实现为类时,您可以使用专用类型将输入明确定义为属性。

动态修改任务操作可以为已经注册但由于某种原因需要修改的任务提供价值。

我们以compileJava任务为例。

任务注册后,您无法将其删除。相反,您可以清除其操作

app/build.gradle.kts
tasks.compileJava {
    // Clear existing actions
    actions.clear()

    // Add a new action
    doLast {
        println("Custom action: Compiling Java classes...")
    }
}
app/build.gradle
tasks.compileJava {
    // Clear existing actions
    actions.clear()

    // Add a new action
    doLast {
        println("Custom action: Compiling Java classes...")
    }
}

同样,很难(在某些情况下不可能)删除您正在使用的插件已经设置的某些任务依赖项。相反,您可以修改其行为

app/build.gradle.kts
tasks.compileJava {
    // Modify the task behavior
    doLast {
        val outputDir = File("$buildDir/compiledClasses")
        outputDir.mkdirs()

        val compiledFiles = sourceSets["main"].output.files
        compiledFiles.forEach { compiledFile ->
            val destinationFile = File(outputDir, compiledFile.name)
            compiledFile.copyTo(destinationFile, true)
        }

        println("Java compilation completed. Compiled classes copied to: ${outputDir.absolutePath}")
    }
}
app/build.gradle
tasks.compileJava {
    // Modify the task behavior
    doLast {
        def outputDir = file("$buildDir/compiledClasses")
        outputDir.mkdirs()

        def compiledFiles = sourceSets["main"].output.files
        compiledFiles.each { compiledFile ->
            def destinationFile = new File(outputDir, compiledFile.name)
            compiledFile.copyTo(destinationFile)
        }

        println("Java compilation completed. Compiled classes copied to: ${outputDir.absolutePath}")
    }
}