可执行任务描述了 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

abstract class GenerateReportTask : DefaultTask() {

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

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.file.DirectoryProperty
import org.gradle.api.tasks.InputDirectory

abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    abstract val sourceDirectory: DirectoryProperty

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

abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    abstract DirectoryProperty getSourceDirectory()

}

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

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

abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    abstract val sourceDirectory: DirectoryProperty

    @get:OutputFile
    abstract val reportFile: RegularFileProperty

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

abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    abstract DirectoryProperty getSourceDirectory()

    @OutputFile
    abstract RegularFileProperty getReportFile()

}

定义了输入和输出后,剩下的唯一事情就是实际的任务操作,它在一个使用 @TaskAction 注解的方法中实现。在这个方法内部,我们编写代码,使用 Gradle 特定的 API 访问输入和输出。

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

abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    abstract val sourceDirectory: DirectoryProperty

    @get:OutputFile
    abstract val reportFile: RegularFileProperty

    @TaskAction
    fun generateReport() {
        val sourceDirectory = sourceDirectory.asFile.get()
        val reportFile = reportFile.asFile.get()
        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.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    abstract DirectoryProperty getSourceDirectory()

    @OutputFile
    abstract RegularFileProperty getReportFile()

    @TaskAction
    void generateReport() {
        def sourceDirectory = sourceDirectory.asFile.get()
        def reportFile = reportFile.asFile.get()
        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 = layout.projectDirectory.dir("src/main")
    reportFile = layout.buildDirectory.file("reports/directoryReport.txt")
}

tasks.build {
    dependsOn("generateReport")
}
app/build.gradle
tasks.register("generateReport", GenerateReportTask) {
    sourceDirectory = layout.projectDirectory.dir("src/main")
    reportFile = layout.buildDirectory.file("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 构建示例,其中包含一个构成 Java 应用程序的 app 子项目——它包含一个 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}")
    }
}

理解输入和输出

任务输入和输出对于以下方面很重要:

  1. 最新检查 (Up-to-date checks) - Gradle 的增量构建特性通过查看任务输入和输出的变化,帮助您的构建避免重复工作。

  2. 链接任务输入和输出 (Linking task inputs and outputs) - 当一个任务的输出与另一个任务的输入关联时,Gradle 可以自动创建任务依赖关系。

  3. 使用依赖配置 (Using dependency configurations) - 任务输出可以用来告知 Gradle,某个任务生成的 artifact 应添加到特定的配置中。

声明输入和输出

您可以通过两种主要方式配置任务的输入和输出:

  1. 静态配置: 直接在任务类内部定义输入和输出。无论何时执行任务,这些输入和输出都将始终适用。

  2. 动态配置: 动态地向任务添加输入和输出,这意味着您可以根据特定条件或要求自定义任务每次执行时的输入和输出。这种方法提供了更大的灵活性和对任务行为的精细控制。

abstract class ConfigurableTask : DefaultTask() {

    @get:Input
    abstract val inputProperty: Property<String>

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    // Static Configuration: Inputs and Outputs defined in the task class
    init {
        group = "custom"
        description = "A configurable task example"
    }

    @TaskAction
    fun executeTask() {
        println("Executing task with input: ${inputProperty.get()} and output: ${outputFile.asFile.get()}")
    }

}

// Dynamic Configuration: Adding inputs and outputs to a task instance
tasks.register("dynamicTask", ConfigurableTask::class) {
    // Set the input property dynamically
    inputProperty = "dynamic input value"

    // Set the output file dynamically
    outputFile = layout.buildDirectory.file("dynamicOutput.txt")
}
abstract class ConfigurableTask extends DefaultTask {

    @Input
    abstract Property<String> getInputProperty()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    // Static Configuration: Inputs and Outputs defined in the task class
    ConfigurableTask() {
        group = 'custom'
        description = 'A configurable task example'
    }

    @TaskAction
    void executeTask() {
        println "Executing task with input: ${inputProperty.get()} and output: ${outputFile.asFile.get()}"
    }

}

// Dynamic Configuration: Adding inputs and outputs to a task instance
tasks.register('dynamicTask', ConfigurableTask) {
    // Set the input property dynamically
    inputProperty = 'dynamic input value'

    // Set the output file dynamically
    outputFile = layout.buildDirectory.file('dynamicOutput.txt')
}

创建惰性输入和输出

Gradle 有惰性配置的概念,它允许在实际设置任务输入和输出之前引用它们。这通过 Property 类类型来实现。

abstract class MyTask : DefaultTask() {
    // Avoid Java Bean properties
    @Input
    var myEagerProperty: String = "default value"

    // Use Gradle managed properties instead
    @get:Input
    abstract val myLazyProperty: Property<String>

    @TaskAction
    fun myAction() {
        println("Use ${myLazyProperty.get()} and do NOT use $myEagerProperty")
    }
}
abstract class MyTask extends DefaultTask {
    // Avoid Java Bean properties
    @Input
    String myEagerProperty = "default value"

    // Use Gradle managed properties instead
    @Input
    abstract Property<String> getMyLazyProperty()

    @TaskAction
    void myAction() {
        println("Use ${myLazyProperty.get()} and do NOT use $myEagerProperty")
    }
}

这种机制的一个优点是,您可以在文件名尚未确定之前,将一个任务的输出文件链接到另一个任务的输入文件。Property 类也知道它链接到哪个任务,因此以这种方式使用输入和输出使 Gradle 能够自动添加所需的任务依赖关系。

writing tasks 7

为确保正确的惰性配置,应避免使用 Java Bean 类型。让我们探讨 Gradle 惰性类型可声明为任务输入和输出的常见选项:

Gradle 惰性类型 Java Bean 类型 输入 输出

Property<String>

String

X

RegularFileProperty

File

X

X

文件的 Iterable (Property<Iterable<File> 例如 ConfigurableFileCollection, ConfigurableFileTree)

文件的 Iterable (Iterable<File> 例如 FileTreeFileCollection)

X

X

文件的 Map (MapProperty<String, File>)

文件的 Map (Map<String, File>)

X

DirectoryProperty

Directory

X

X

目录的 Iterable (Property<Iterable<File>)

目录的 Iterable (Iterable<File>)

X

目录的 Map (MapProperty<String, File>)

目录的 Map (Map<String, File>)

X

注意

  • 字符串仅支持任务输入,不支持输出。它们通常用于配置选项,例如 compileJava 任务类型的 sourceCompatibility

  • 任务输出只能是文件或目录。

  • 除了 String, Boolean, String 和其他标准类型外,使用 Property<T>

  • 除了 List<T> 外,使用 ListProperty<T>

  • 除了 Set<T> 外,使用 SetProperty<T>

当顺序很重要时,避免使用 FileTree——它没有保证稳定的文件顺序,可能导致不可预测的行为。

注解输入和输出

一个好的实践是为自定义任务创建一个任务类。该类封装了任务操作逻辑,同时也应该声明任务期望的任何输入和输出。为此,我们使用注解。

对于任务输入,我们可以使用 @Input, @InputFile, @InputDirectory, @InputFiles, @Classpath@CompileClasspath。对于任务输出,我们有 @OutputFile, @OutputDirectory, @OutputFiles, @OutputDirectories

以下是所有可用注解的详细信息:

注解 用途

@Input

属性是任务的输入值

@InputFile

属性是任务的输入文件

@InputFiles

属性是任务的一个或多个输入文件

@InputDirectory

属性是任务的输入目录

@Classpath

属性是一个或多个表示 Java classpath 的输入文件或目录

@CompileClasspath

属性是一个或多个表示 Java 编译 classpath 的输入文件或目录

@OutputFile

属性是任务的输出文件[1]

@OutputFiles

属性是任务的一个或多个输出文件

@OutputDirectory

属性是任务的输出目录

@OutputDirectories

属性是任务的一个或多个输出目录

@Destroys

属性是一个或多个任务销毁(删除/移除)的文件或目录(来自其他任务)[2]

@LocalState

属性是任务的本地状态

@Nested

属性是嵌套的 bean,应该检查其是否有其他注解

@Console

属性不是输入或输出,不应在最新检查中考虑

@ReplacedBy

属性在内部使用,不应在最新检查中考虑

@SkipWhenEmpty

属性是文件或目录,当属性值为空时,任务应被跳过

@Incremental

属性是文件或目录,其更改可以使用 @InputChanges.getFileChanges() 查询

@Optional

属性可以是任何类型,其值不必指定,并且禁用验证检查

@PathSensitive

属性是一个或多个文件,只有文件路径的给定部分是重要的

@IgnoreEmptyDirectories

@InputFiles@InputDirectory 一起使用,指示 Gradle 仅跟踪目录内容的更改,而不跟踪目录本身的差异。

@NormalizeLineEndings

@InputFiles@InputDirectory@Classpath 一起使用,指示 Gradle 在计算最新检查或构建缓存键时规范化行结束符。

请注意,在 Kotlin 中,注解以 get: 为前缀,因此 @InputFile 变为 @get:InputFile

build.gradle.kts
abstract class AllTypes : DefaultTask() {

    //inputs
    @get:Input
    abstract val inputString: Property<String>
    @get:InputFile
    abstract val inputFile: RegularFileProperty
    @get:InputDirectory
    abstract val inputDirectory: DirectoryProperty
    @get:InputFiles
    abstract val inputFileCollection: ConfigurableFileCollection
    @get:Classpath
    abstract val inputClasspath: ConfigurableFileCollection

    // outputs
    @get:OutputFile
    abstract val outputFile: RegularFileProperty
    @get:OutputDirectory
    abstract val outputDirectory: DirectoryProperty
    @get:OutputFiles
    abstract val outputFiles: ConfigurableFileCollection
    @get:OutputDirectories
    abstract val outputDirectories: ConfigurableFileCollection
}
build.gradle
abstract class AllTypes extends DefaultTask {

    //inputs
    @Input
    abstract Property<String> getInputString()
    @InputFile
    abstract RegularFileProperty getInputFile()
    @InputDirectory
    abstract DirectoryProperty getInputDirectory()
    @InputFiles
    abstract ConfigurableFileCollection getInputFileCollection()
    @Classpath
    abstract ConfigurableFileCollection getInputClasspath()

    // outputs
    @OutputFile
    abstract RegularFileProperty getOutputFile()
    @OutputDirectory
    abstract DirectoryProperty getOutputDirectory()
    @OutputFiles
    abstract ConfigurableFileCollection getOutputFiles()
    @OutputDirectories
    abstract ConfigurableFileCollection getOutputDirectories()
}

1. 此标记用于声明任务可能会生成特定的输出文件。此注解通常应用于任务类中表示输出文件的属性。当执行带有 @OutputFile 注解属性的任务时,Gradle 将检查指定的输出文件是否存在。如果文件不存在,或者任务的输入(由 @Input 注解定义)发生了更改,Gradle 将认为任务已过期,并执行它以重新生成输出文件。
2. 此标记用于指定任务将始终删除的位置/文件,这些位置/文件通常属于其他任务。此注解对于清理其他任务后留下的文件(如 clean 任务)非常有用。