可操作的任务描述了 Gradle 中的工作。这些任务有行动。在 Gradle 核心中,该compileJava
任务编译 Java 源代码。Jar
和任务Zip
将 zip 文件压缩到存档中。
可以通过扩展DefaultTask
类并定义输入、输出和操作来创建自定义可操作任务。
任务输入和输出
可操作的任务有输入和输出。输入和输出可以是文件、目录或变量。
在可操作的任务中:
-
输入由文件、文件夹和/或配置数据的集合组成。
例如,该javaCompile
任务接受 Java 源文件和构建脚本配置(例如 Java 版本)等输入。 -
输出指一个或多个文件或文件夹。
例如,javaCompile
生成类文件作为输出。
然后,该jar
任务将这些类文件作为输入并生成 JAR 存档。
明确定义的任务输入和输出有两个目的:
-
它们向 Gradle 通报任务依赖性。
例如,如果 Gradle 理解任务的输出compileJava
作为任务的输入jar
,它将优先考虑compileJava
首先运行。 -
它们促进增量建设。
例如,假设 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):
java -cp 'libs/*' gradle.project.app.App
让我们注册一个名为packageApp
using 的新任务task.register()
:
tasks.register<Zip>("packageApp") {
}
tasks.register(Zip, "packageApp") {
}
我们使用 Gradle 核心的现有实现,它是Zip
任务实现(即 的子类DefaultTask
)。因为我们在这里注册了一个新任务,所以它没有预先配置。我们需要配置输入和输出。
定义输入和输出使任务成为可操作的任务。
如果输入是我们直接创建或编辑的文件,例如运行文件或 Java 源代码,它通常位于项目目录中的某个位置。为了确保我们使用正确的位置,我们使用layout.projectDirectory
并定义了项目目录根的相对路径。
我们提供任务的输出jar
以及所有依赖项的 JAR(使用)作为附加输入。configurations
.runtimeClasspath
对于输出,我们需要定义两个属性。
首先,目标目录,它应该是构建文件夹内的目录。我们可以通过 访问它layout
。
其次,我们需要为 zip 文件指定一个名称,我们称之为myApplication.zip
完整的任务如下所示:
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
}
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
命令将可操作任务连接到生命周期任务:build
dependsOn()
tasks.build {
dependsOn(packageApp)
}
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
我们创建一个名为GenerateReportTask
in ./buildSrc/src/main/kotlin/GenerateReportTask.kt
or 的类./buildSrc/src/main/groovy/GenerateReportTask.groovy
。
为了让 Gradle 知道我们正在实现一个任务,我们扩展了DefaultTask
Gradle 附带的类。创建我们的任务类也是有益的,abstract
因为 Gradle 会自动处理很多事情:
import org.gradle.api.DefaultTask
public abstract class GenerateReportTask : DefaultTask() {
}
import org.gradle.api.DefaultTask
public abstract class GenerateReportTask extends DefaultTask {
}
接下来,我们使用属性和注释定义输入和输出。在这种情况下,Gradle 中的属性充当对其背后实际值的引用,允许 Gradle 跟踪任务之间的输入和输出。
对于我们任务的输入,我们使用DirectoryProperty
Gradle 中的一个。我们用 注释它以@InputDirectory
表明它是任务的输入:
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
public abstract class GenerateReportTask : DefaultTask() {
@get:InputDirectory
lateinit var sourceDirectory: File
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
public abstract class GenerateReportTask extends DefaultTask {
@InputDirectory
File sourceDirectory
}
同样,对于输出,我们使用 aRegularFileProperty
并用 注释它@OutputFile
。
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
}
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 编写访问输入和输出的代码:
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}")
}
}
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
.
在应用程序构建文件中,我们注册一个类型为GenerateReportTask
using的任务task.register()
并将其命名为generateReport
.同时,我们配置任务的输入和输出:
tasks.register<GenerateReportTask>("generateReport") {
sourceDirectory = file("src/main")
reportFile = file("${layout.buildDirectory}/reports/directoryReport.txt")
}
tasks.build {
dependsOn("generateReport")
}
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
:
plugins {
id("my-convention-plugin")
}
version = "1.0"
application {
mainClass = "org.example.app.App"
}
plugins {
id 'my-convention-plugin'
}
version = '1.0'
application {
mainClass = 'org.example.app.App'
}
printVersion
我们定义一个在构建文件中调用的任务app
:
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()}")
}
}
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
,我们可以注册一个使用新任务实现的新任务:
tasks.register<PrintVersion>("printVersion") {
// Configuration code
version = project.version as String
}
tasks.register(PrintVersion, "printVersion") {
// Configuration code
version = project.version.toString()
}
在任务的配置块内,我们可以编写配置阶段代码来修改任务的输入和输出属性的值。此处不以任何方式提及任务操作。
可以以更紧凑的方式直接在构建脚本中编写像这样的简单任务,而无需为该任务创建单独的类。
让我们注册另一个任务并将其命名为printVersionDynamic
。
这次,我们没有为任务定义类型,这意味着任务将是通用类型DefaultTask
。这种通用类型不定义任何任务操作,这意味着它没有用 注释的方法@TaskAction
。这种类型对于定义“生命周期任务”很有用:
tasks.register("printVersionDynamic") {
}
tasks.register("printVersionDynamic") {
}
但是,默认任务类型也可用于动态定义具有自定义操作的任务,而无需额外的类。这是通过使用doFirst{}
ordoLast{}
结构来完成的。与定义方法并注释 this 类似@TaskAction
,这会向任务添加一个操作。
调用这些方法是doFirst{}
因为doLast{}
任务可以有多个操作。如果任务已经定义了一个操作,您可以使用这种区别来决定您的附加操作是应该在现有操作之前还是之后运行:
tasks.register("printVersionDynamic") {
doFirst {
// Task action = Execution code
// Run before exiting actions
}
doLast {
// Task action = Execution code
// Run after existing actions
}
}
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{}
:
tasks.register("printVersionDynamic") {
inputs.property("version", project.version.toString())
doLast {
println("Version: ${inputs.properties["version"]}")
}
}
tasks.register("printVersionDynamic") {
inputs.property("version", project.version)
doLast {
println("Version: ${inputs.properties["version"]}")
}
}
我们看到了在 Gradle 中实现自定义任务的两种替代方法。
动态设置使其更加紧凑。但是,在编写动态任务时很容易混合配置和执行时间状态。您还可以看到动态任务中的“输入”是无类型的,这可能会导致问题。当您将自定义任务实现为类时,您可以清楚地将输入定义为具有专用类型的属性。
动态修改任务操作可以为已注册但由于某种原因需要修改的任务提供价值。
我们compileJava
以任务为例。
任务一旦注册,就无法删除。相反,您可以清除其操作:
tasks.compileJava {
// Clear existing actions
actions.clear()
// Add a new action
doLast {
println("Custom action: Compiling Java classes...")
}
}
tasks.compileJava {
// Clear existing actions
actions.clear()
// Add a new action
doLast {
println("Custom action: Compiling Java classes...")
}
}
删除您正在使用的插件已经设置的某些任务依赖项也很困难,在某些情况下是不可能的。相反,您可以修改其行为:
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}")
}
}
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}")
}
}