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

写作任务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):

应用程序/run.sh
java -cp 'libs/*' gradle.project.app.App

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

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

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

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

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

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

我们看到 running: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

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

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

让我们从 Gradle 为一个简单的 Java 应用程序构建的示例开始,该应用init程序的源代码位于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

我们创建一个名为GenerateReportTaskin ./buildSrc/src/main/kotlin/GenerateReportTask.ktor 的类./buildSrc/src/main/groovy/GenerateReportTask.groovy

为了让 Gradle 知道我们正在实现一个任务,我们扩展了DefaultTaskGradle 附带的类。创建我们的任务类也是有益的,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 跟踪任务之间的输入和输出。

对于我们任务的输入,我们使用DirectoryPropertyGradle 中的一个。我们用 注释它以@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

}

同样,对于输出,我们使用 aRegularFileProperty并用 注释它@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.

在应用程序构建文件中,我们注册一个类型为GenerateReportTaskusing的任务task.register()并将其命名为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'
}

printVersion我们定义一个在构建文件中调用的任务app

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 知道对于特定的构建调用需要做什么。任务操作周围的所有内容(例如输入或输出属性)都是此配置代码的一部分。

任务操作方法内的代码是执行实际工作的执行代码。如果任务是任务图的一部分,并且由于它是最新的或从缓存中获取而无法跳过,则它会访问输入和输出来执行某些工作。

任务实现完成后,就可以在构建设置中使用它。在我们的约定插件中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{}ordoLast{}结构来完成的。与定义方法并注释 this 类似@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,而是使用所有任务都具有的通用输入属性。然后,我们println()在方法内添加操作代码(一条语句) doLast{}

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}")
    }
}