随着构建复杂度的增加,很难追踪特定值何时何地被配置。Gradle 提供了几种方法来通过**惰性配置**管理这个问题。

writing tasks 4

理解惰性属性

Gradle 提供了惰性属性,它将属性值的计算延迟到实际需要时才进行。

惰性属性提供三个主要好处:

  1. **延迟值解析:** 允许连接 Gradle 模型,而无需知道属性值何时可用。例如,您可能希望根据扩展的源目录属性设置任务的输入源文件,但扩展属性的值直到构建脚本或某个其他插件配置它们时才知道。

  2. **自动任务依赖管理:** 将一个任务的输出连接到另一个任务的输入,自动确定任务依赖。属性实例携带着关于哪个任务(如果有)产生其值的信息。构建作者无需担心任务依赖与配置更改保持同步。

  3. **提高构建性能:** 避免在配置期间进行资源密集型工作,从而积极影响构建性能。例如,当一个配置值来自解析文件,但只在运行功能测试时才使用时,使用属性实例捕获此信息意味着只有在运行功能测试时才解析文件(而不是在运行 `clean` 时,例如)。

Gradle 用两个接口表示惰性属性:

Provider

表示只能查询而不能更改的值。

  • 具有这些类型的属性是只读的。

  • 方法 Provider.get() 返回属性的当前值。

  • 可以使用 Provider.map(Transformer) 从另一个 `Provider` 创建一个 `Provider`。

  • 许多其他类型扩展了 `Provider`,可以在需要 `Provider` 的任何地方使用。

属性

表示可以查询和更改的值。

  • 具有这些类型的属性是可配置的。

  • `Property` 扩展了 `Provider` 接口。

  • 方法 Property.set(T) 为属性指定一个值,覆盖可能已存在的任何值。

  • 方法 Property.set(Provider) 为属性值指定一个 `Provider`,覆盖可能已存在的任何值。这允许您在值配置之前连接 `Provider` 和 `Property` 实例。

  • 可以通过工厂方法 ObjectFactory.property(Class) 创建 `Property`。

惰性属性旨在传递,并仅在需要时查询。这通常发生在执行阶段

以下示例演示了一个具有可配置 `greeting` 属性和只读 `message` 属性的任务:

build.gradle.kts
abstract class Greeting : DefaultTask() { (1)
    @get:Input
    abstract val greeting: Property<String> (2)

    @Internal
    val message: Provider<String> = greeting.map { it + " from Gradle" } (3)

    @TaskAction
    fun printMessage() {
        logger.quiet(message.get())
    }
}

tasks.register<Greeting>("greeting") {
    greeting.set("Hi") (4)
    greeting = "Hi" (5)
}
build.gradle
abstract class Greeting extends DefaultTask { (1)
    @Input
    abstract Property<String> getGreeting() (2)

    @Internal
    final Provider<String> message = greeting.map { it + ' from Gradle' } (3)

    @TaskAction
    void printMessage() {
        logger.quiet(message.get())
    }
}

tasks.register("greeting", Greeting) {
    greeting.set('Hi') (4)
    greeting = 'Hi' (5)
}
1 一个显示问候语的任务
2 可配置的问候语
3 从问候语计算的只读属性
4 配置问候语
5 调用 Property.set() 的替代表示法
$ gradle greeting

> Task :greeting
Hi from Gradle

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

`Greeting` 任务有一个类型为 `Property` 的属性,用于表示可配置的问候语;以及一个类型为 `Provider` 的属性,用于表示计算得到的只读消息。消息 `Provider` 是使用 `map()` 方法从问候语 `Property` 创建的;其值会随着问候语属性值的变化而保持最新。

创建 Property 或 Provider 实例

`Provider` 及其子类型(如 `Property`)都不打算由构建脚本或插件实现。Gradle 提供了工厂方法来创建这些类型的实例。

在前面的示例中,介绍了两种工厂方法:

有关所有可用类型和工厂的更多信息,请参阅快速参考

`Provider` 也可以通过工厂方法 ProviderFactory.provider(Callable) 创建。

没有特定的方法可以使用 `groovy.lang.Closure` 创建 Provider。

当使用 Groovy 编写插件或构建脚本时,您可以使用带有闭包的 `map(Transformer)` 方法,Groovy 会将闭包转换为 `Transformer`。

类似地,当使用 Kotlin 编写插件或构建脚本时,Kotlin 编译器会将 Kotlin 函数转换为 `Transformer`。

连接属性

惰性属性的一个重要特性是它们可以连接在一起,这样对一个属性的更改会自动反映在其他属性中。

以下是一个任务属性连接到项目扩展属性的示例:

build.gradle.kts
// A project extension
interface MessageExtension {
    // A configurable greeting
    abstract val greeting: Property<String>
}

// A task that displays a greeting
abstract class Greeting : DefaultTask() {
    // Configurable by the user
    @get:Input
    abstract val greeting: Property<String>

    // Read-only property calculated from the greeting
    @Internal
    val message: Provider<String> = greeting.map { it + " from Gradle" }

    @TaskAction
    fun printMessage() {
        logger.quiet(message.get())
    }
}

// Create the project extension
val messages = project.extensions.create<MessageExtension>("messages")

// Create the greeting task
tasks.register<Greeting>("greeting") {
    // Attach the greeting from the project extension
    // Note that the values of the project extension have not been configured yet
    greeting = messages.greeting
}

messages.apply {
    // Configure the greeting on the extension
    // Note that there is no need to reconfigure the task's `greeting` property. This is automatically updated as the extension property changes
    greeting = "Hi"
}
build.gradle
// A project extension
interface MessageExtension {
    // A configurable greeting
    Property<String> getGreeting()
}

// A task that displays a greeting
abstract class Greeting extends DefaultTask {
    // Configurable by the user
    @Input
    abstract Property<String> getGreeting()

    // Read-only property calculated from the greeting
    @Internal
    final Provider<String> message = greeting.map { it + ' from Gradle' }

    @TaskAction
    void printMessage() {
        logger.quiet(message.get())
    }
}

// Create the project extension
project.extensions.create('messages', MessageExtension)

// Create the greeting task
tasks.register("greeting", Greeting) {
    // Attach the greeting from the project extension
    // Note that the values of the project extension have not been configured yet
    greeting = messages.greeting
}

messages {
    // Configure the greeting on the extension
    // Note that there is no need to reconfigure the task's `greeting` property. This is automatically updated as the extension property changes
    greeting = 'Hi'
}
$ gradle greeting

> Task :greeting
Hi from Gradle

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

此示例调用 Property.set(Provider) 方法,将 `Provider` 附加到 `Property` 以提供属性的值。在这种情况下,`Provider` 恰好也是一个 `Property`,但您可以连接任何 `Provider` 实现,例如使用 `Provider.map()` 创建的实现。

处理文件

处理文件中,我们介绍了四种 `File` 类似对象的集合类型:

只读类型 可配置类型

FileCollection

ConfigurableFileCollection

FileTree

ConfigurableFileTree

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

所有这些类型也被认为是惰性类型。

还有更强类型化的模型用于表示文件系统元素:DirectoryRegularFile。这些类型不应与标准 Java File 类型混淆,因为它们用于告诉 Gradle 您期望更具体的值,例如目录或非目录的常规文件。

Gradle 提供了两种专门的 `Property` 子类型来处理这些类型的值:RegularFilePropertyDirectoryPropertyObjectFactory 有方法来创建它们:ObjectFactory.fileProperty()ObjectFactory.directoryProperty()

`DirectoryProperty` 也可以通过 DirectoryProperty.dir(String)DirectoryProperty.file(String) 分别用于为 `Directory` 和 `RegularFile` 创建惰性求值的 `Provider`。这些方法创建的 Provider 的值是相对于它们创建自的 `DirectoryProperty` 的位置计算的。这些 Provider 返回的值将反映 `DirectoryProperty` 的更改。

build.gradle.kts
// A task that generates a source file and writes the result to an output directory
abstract class GenerateSource : DefaultTask() {
    // The configuration file to use to generate the source file
    @get:InputFile
    abstract val configFile: RegularFileProperty

    // The directory to write source files to
    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction
    fun compile() {
        val inFile = configFile.get().asFile
        logger.quiet("configuration file = $inFile")
        val dir = outputDir.get().asFile
        logger.quiet("output dir = $dir")
        val className = inFile.readText().trim()
        val srcFile = File(dir, "${className}.java")
        srcFile.writeText("public class ${className} { }")
    }
}

// Create the source generation task
tasks.register<GenerateSource>("generate") {
    // Configure the locations, relative to the project and build directories
    configFile = layout.projectDirectory.file("src/config.txt")
    outputDir = layout.buildDirectory.dir("generated-source")
}

// Change the build directory
// Don't need to reconfigure the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir("output")
build.gradle
// A task that generates a source file and writes the result to an output directory
abstract class GenerateSource extends DefaultTask {
    // The configuration file to use to generate the source file
    @InputFile
    abstract RegularFileProperty getConfigFile()

    // The directory to write source files to
    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @TaskAction
    def compile() {
        def inFile = configFile.get().asFile
        logger.quiet("configuration file = $inFile")
        def dir = outputDir.get().asFile
        logger.quiet("output dir = $dir")
        def className = inFile.text.trim()
        def srcFile = new File(dir, "${className}.java")
        srcFile.text = "public class ${className} { ... }"
    }
}

// Create the source generation task
tasks.register('generate', GenerateSource) {
    // Configure the locations, relative to the project and build directories
    configFile = layout.projectDirectory.file('src/config.txt')
    outputDir = layout.buildDirectory.dir('generated-source')
}

// Change the build directory
// Don't need to reconfigure the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir('output')
$ gradle generate

> Task :generate
configuration file = /home/user/gradle/samples/src/config.txt
output dir = /home/user/gradle/samples/output/generated-source

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
$ gradle generate

> Task :generate
configuration file = /home/user/gradle/samples/kotlin/src/config.txt
output dir = /home/user/gradle/samples/kotlin/output/generated-source

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

本例通过 Project.getLayout() 结合 ProjectLayout.getBuildDirectory()ProjectLayout.getProjectDirectory() 创建表示项目和构建目录中位置的 Provider。

为了完成这个循环,请注意 `DirectoryProperty` 或简单的 `Directory` 可以通过 DirectoryProperty.getAsFileTree()Directory.getAsFileTree() 转换为 `FileTree`,从而允许查询目录中包含的文件和目录。从 `DirectoryProperty` 或 `Directory`,您可以创建包含目录中一组文件的 `FileCollection` 实例,使用 DirectoryProperty.files(Object...)Directory.files(Object...)

使用任务输入和输出

许多构建有几个任务连接在一起,其中一个任务使用另一个任务的输出作为输入。

为了实现这一点,我们需要配置每个任务以知道在哪里查找其输入以及在哪里放置其输出。确保生产者任务和消费者任务配置在相同的位置,并在任务之间附加任务依赖。如果这些值中的任何一个可以由用户配置或由多个插件配置,这可能会变得繁琐和脆弱,因为任务属性需要以正确的顺序和位置配置,并且任务依赖需要随着值的更改保持同步。

`Property` API 通过跟踪属性的值以及产生该值的任务来简化此操作。

例如,考虑以下具有生产者和消费者任务并连接在一起的插件:

build.gradle.kts
abstract class Producer : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun produce() {
        val message = "Hello, World!"
        val output = outputFile.get().asFile
        output.writeText( message)
        logger.quiet("Wrote '${message}' to ${output}")
    }
}

abstract class Consumer : DefaultTask() {
    @get:InputFile
    abstract val inputFile: RegularFileProperty

    @TaskAction
    fun consume() {
        val input = inputFile.get().asFile
        val message = input.readText()
        logger.quiet("Read '${message}' from ${input}")
    }
}

val producer = tasks.register<Producer>("producer")
val consumer = tasks.register<Consumer>("consumer")

consumer {
    // Connect the producer task output to the consumer task input
    // Don't need to add a task dependency to the consumer task. This is automatically added
    inputFile = producer.flatMap { it.outputFile }
}

producer {
    // Set values for the producer lazily
    // Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
    outputFile = layout.buildDirectory.file("file.txt")
}

// Change the build directory.
// Don't need to update producer.outputFile and consumer.inputFile. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir("output")
build.gradle
abstract class Producer extends DefaultTask {
    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void produce() {
        String message = 'Hello, World!'
        def output = outputFile.get().asFile
        output.text = message
        logger.quiet("Wrote '${message}' to ${output}")
    }
}

abstract class Consumer extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getInputFile()

    @TaskAction
    void consume() {
        def input = inputFile.get().asFile
        def message = input.text
        logger.quiet("Read '${message}' from ${input}")
    }
}

def producer = tasks.register("producer", Producer)
def consumer = tasks.register("consumer", Consumer)

consumer.configure {
    // Connect the producer task output to the consumer task input
    // Don't need to add a task dependency to the consumer task. This is automatically added
    inputFile = producer.flatMap { it.outputFile }
}

producer.configure {
    // Set values for the producer lazily
    // Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
    outputFile = layout.buildDirectory.file('file.txt')
}

// Change the build directory.
// Don't need to update producer.outputFile and consumer.inputFile. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir('output')
$ gradle consumer

> Task :producer
Wrote 'Hello, World!' to /home/user/gradle/samples/output/file.txt

> Task :consumer
Read 'Hello, World!' from /home/user/gradle/samples/output/file.txt

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
$ gradle consumer

> Task :producer
Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/output/file.txt

> Task :consumer
Read 'Hello, World!' from /home/user/gradle/samples/kotlin/output/file.txt

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

在上面的例子中,任务输出和输入在定义任何位置之前就已经连接起来了。setter 可以在任务执行之前的任何时间调用,并且更改将自动影响所有相关的输入和输出属性。

本例中需要注意的另一点是没有任何显式任务依赖。使用 `Providers` 表示的任务输出会跟踪产生其值的任务,并且将它们用作任务输入将隐式添加正确的任务依赖。

隐式任务依赖也适用于非文件输入属性。

build.gradle.kts
abstract class Producer : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun produce() {
        val message = "Hello, World!"
        val output = outputFile.get().asFile
        output.writeText( message)
        logger.quiet("Wrote '${message}' to ${output}")
    }
}

abstract class Consumer : DefaultTask() {
    @get:Input
    abstract val message: Property<String>

    @TaskAction
    fun consume() {
        logger.quiet(message.get())
    }
}

val producer = tasks.register<Producer>("producer") {
    // Set values for the producer lazily
    // Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
    outputFile = layout.buildDirectory.file("file.txt")
}
tasks.register<Consumer>("consumer") {
    // Connect the producer task output to the consumer task input
    // Don't need to add a task dependency to the consumer task. This is automatically added
    message = producer.flatMap { it.outputFile }.map { it.asFile.readText() }
}
build.gradle
abstract class Producer extends DefaultTask {
    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void produce() {
        String message = 'Hello, World!'
        def output = outputFile.get().asFile
        output.text = message
        logger.quiet("Wrote '${message}' to ${output}")
    }
}

abstract class Consumer extends DefaultTask {
    @Input
    abstract Property<String> getMessage()

    @TaskAction
    void consume() {
        logger.quiet(message.get())
    }
}

def producer = tasks.register('producer', Producer) {
    // Set values for the producer lazily
    // Don't need to update the consumer.inputFile property. This is automatically updated as producer.outputFile changes
    outputFile = layout.buildDirectory.file('file.txt')
}
tasks.register('consumer', Consumer) {
    // Connect the producer task output to the consumer task input
    // Don't need to add a task dependency to the consumer task. This is automatically added
    message = producer.flatMap { it.outputFile }.map { it.asFile.text }
}
$ gradle consumer

> Task :producer
Wrote 'Hello, World!' to /home/user/gradle/samples/build/file.txt

> Task :consumer
Hello, World!

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed
$ gradle consumer

> Task :producer
Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/build/file.txt

> Task :consumer
Hello, World!

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

使用集合

Gradle 提供了两种惰性属性类型,用于帮助配置 `Collection` 属性。

它们的工作方式与其他 `Provider` 完全相同,并且像文件 Provider 一样,它们还有额外的模型支持:

这种类型的属性允许您使用 HasMultipleValues.set(Iterable)HasMultipleValues.set(Provider) 覆盖整个集合值,或通过各种 `add` 方法添加新元素:

就像每个 `Provider` 一样,集合在调用 Provider.get() 时计算。以下示例展示了 ListProperty 的实际应用:

build.gradle.kts
abstract class Producer : DefaultTask() {
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun produce() {
        val message = "Hello, World!"
        val output = outputFile.get().asFile
        output.writeText( message)
        logger.quiet("Wrote '${message}' to ${output}")
    }
}

abstract class Consumer : DefaultTask() {
    @get:InputFiles
    abstract val inputFiles: ListProperty<RegularFile>

    @TaskAction
    fun consume() {
        inputFiles.get().forEach { inputFile ->
            val input = inputFile.asFile
            val message = input.readText()
            logger.quiet("Read '${message}' from ${input}")
        }
    }
}

val producerOne = tasks.register<Producer>("producerOne")
val producerTwo = tasks.register<Producer>("producerTwo")
tasks.register<Consumer>("consumer") {
    // Connect the producer task outputs to the consumer task input
    // Don't need to add task dependencies to the consumer task. These are automatically added
    inputFiles.add(producerOne.get().outputFile)
    inputFiles.add(producerTwo.get().outputFile)
}

// Set values for the producer tasks lazily
// Don't need to update the consumer.inputFiles property. This is automatically updated as producer.outputFile changes
producerOne { outputFile = layout.buildDirectory.file("one.txt") }
producerTwo { outputFile = layout.buildDirectory.file("two.txt") }

// Change the build directory.
// Don't need to update the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir("output")
build.gradle
abstract class Producer extends DefaultTask {
    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void produce() {
        String message = 'Hello, World!'
        def output = outputFile.get().asFile
        output.text = message
        logger.quiet("Wrote '${message}' to ${output}")
    }
}

abstract class Consumer extends DefaultTask {
    @InputFiles
    abstract ListProperty<RegularFile> getInputFiles()

    @TaskAction
    void consume() {
        inputFiles.get().each { inputFile ->
            def input = inputFile.asFile
            def message = input.text
            logger.quiet("Read '${message}' from ${input}")
        }
    }
}

def producerOne = tasks.register('producerOne', Producer)
def producerTwo = tasks.register('producerTwo', Producer)
tasks.register('consumer', Consumer) {
    // Connect the producer task outputs to the consumer task input
    // Don't need to add task dependencies to the consumer task. These are automatically added
    inputFiles.add(producerOne.get().outputFile)
    inputFiles.add(producerTwo.get().outputFile)
}

// Set values for the producer tasks lazily
// Don't need to update the consumer.inputFiles property. This is automatically updated as producer.outputFile changes
producerOne.configure { outputFile = layout.buildDirectory.file('one.txt') }
producerTwo.configure { outputFile = layout.buildDirectory.file('two.txt') }

// Change the build directory.
// Don't need to update the task properties. These are automatically updated as the build directory changes
layout.buildDirectory = layout.projectDirectory.dir('output')
$ gradle consumer

> Task :producerOne
Wrote 'Hello, World!' to /home/user/gradle/samples/output/one.txt

> Task :producerTwo
Wrote 'Hello, World!' to /home/user/gradle/samples/output/two.txt

> Task :consumer
Read 'Hello, World!' from /home/user/gradle/samples/output/one.txt
Read 'Hello, World!' from /home/user/gradle/samples/output/two.txt

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 executed
$ gradle consumer

> Task :producerOne
Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/output/one.txt

> Task :producerTwo
Wrote 'Hello, World!' to /home/user/gradle/samples/kotlin/output/two.txt

> Task :consumer
Read 'Hello, World!' from /home/user/gradle/samples/kotlin/output/one.txt
Read 'Hello, World!' from /home/user/gradle/samples/kotlin/output/two.txt

BUILD SUCCESSFUL in 0s
3 actionable tasks: 3 executed

使用映射

Gradle 提供了一种惰性 MapProperty 类型,允许配置 `Map` 值。您可以使用 ObjectFactory.mapProperty(Class, Class) 创建 `MapProperty` 实例。

与其他属性类型类似,`MapProperty` 有一个 set() 方法,您可以使用它来指定属性的值。一些额外的方法允许将带有惰性值的条目添加到映射中。

build.gradle.kts
abstract class Generator: DefaultTask() {
    @get:Input
    abstract val properties: MapProperty<String, Int>

    @TaskAction
    fun generate() {
        properties.get().forEach { entry ->
            logger.quiet("${entry.key} = ${entry.value}")
        }
    }
}

// Some values to be configured later
var b = 0
var c = 0

tasks.register<Generator>("generate") {
    properties.put("a", 1)
    // Values have not been configured yet
    properties.put("b", providers.provider { b })
    properties.putAll(providers.provider { mapOf("c" to c, "d" to c + 1) })
}

// Configure the values. There is no need to reconfigure the task
b = 2
c = 3
build.gradle
abstract class Generator extends DefaultTask {
    @Input
    abstract MapProperty<String, Integer> getProperties()

    @TaskAction
    void generate() {
        properties.get().each { key, value ->
            logger.quiet("${key} = ${value}")
        }
    }
}

// Some values to be configured later
def b = 0
def c = 0

tasks.register('generate', Generator) {
    properties.put("a", 1)
    // Values have not been configured yet
    properties.put("b", providers.provider { b })
    properties.putAll(providers.provider { [c: c, d: c + 1] })
}

// Configure the values. There is no need to reconfigure the task
b = 2
c = 3
$ gradle generate

> Task :generate
a = 1
b = 2
c = 3
d = 4

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

为属性应用约定

通常,您希望为属性应用一些**约定**或默认值,以便在未配置任何值时使用。您可以使用 `convention()` 方法来实现这一点。此方法接受一个值或一个 `Provider`,这将用作值,直到配置了其他值。

build.gradle.kts
tasks.register("show") {
    val property = objects.property(String::class)

    // Set a convention
    property.convention("convention 1")

    println("value = " + property.get())

    // Can replace the convention
    property.convention("convention 2")
    println("value = " + property.get())

    property.set("explicit value")

    // Once a value is set, the convention is ignored
    property.convention("ignored convention")

    doLast {
        println("value = " + property.get())
    }
}
build.gradle
tasks.register("show") {
    def property = objects.property(String)

    // Set a convention
    property.convention("convention 1")

    println("value = " + property.get())

    // Can replace the convention
    property.convention("convention 2")
    println("value = " + property.get())

    property.set("explicit value")

    // Once a value is set, the convention is ignored
    property.convention("ignored convention")

    doLast {
        println("value = " + property.get())
    }
}
$ gradle show
value = convention 1
value = convention 2

> Task :show
value = explicit value

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

从何处应用约定?

在配置时(即执行之前)为属性设置约定有几个合适的地点。

build.gradle.kts
// setting convention when registering a task from plugin
class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.getTasks().register<GreetingTask>("hello") {
            greeter.convention("Greeter")
        }
    }
}

apply<GreetingPlugin>()

tasks.withType<GreetingTask>().configureEach {
    // setting convention from build script
    guest.convention("Guest")
}

abstract class GreetingTask : DefaultTask() {
    // setting convention from constructor
    @get:Input
    abstract val guest: Property<String>

    init {
        guest.convention("person2")
    }

    // setting convention from declaration
    @Input
    val greeter = project.objects.property<String>().convention("person1")

    @TaskAction
    fun greet() {
        println("hello, ${guest.get()}, from ${greeter.get()}")
    }
}
build.gradle
// setting convention when registering a task from plugin
class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.getTasks().register("hello", GreetingTask) {
            greeter.convention("Greeter")
        }
    }
}

apply plugin: GreetingPlugin

tasks.withType(GreetingTask).configureEach {
    // setting convention from build script
    guest.convention("Guest")
}

abstract class GreetingTask extends DefaultTask {
    // setting convention from constructor
    @Input
    abstract Property<String> getGuest()

    GreetingTask() {
        guest.convention("person2")
    }

    // setting convention from declaration
    @Input
    final Property<String> greeter = project.objects.property(String).convention("person1")

    @TaskAction
    void greet() {
        println("hello, ${guest.get()}, from ${greeter.get()}")
    }
}

从插件的 `apply()` 方法

插件作者可以在插件的 `apply()` 方法中为惰性属性配置约定,同时对定义该属性的任务或扩展执行初步配置。这适用于常规插件(旨在分发和普遍使用)和内部约定插件(通常以统一的方式为整个构建配置第三方插件定义的属性)。

build.gradle.kts
// setting convention when registering a task from plugin
class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.getTasks().register<GreetingTask>("hello") {
            greeter.convention("Greeter")
        }
    }
}
build.gradle
// setting convention when registering a task from plugin
class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.getTasks().register("hello", GreetingTask) {
            greeter.convention("Greeter")
        }
    }
}

从构建脚本

构建工程师可以从共享的构建逻辑中为惰性属性配置约定,该逻辑以标准方式为整个构建配置任务(例如,来自第三方插件)。

build.gradle.kts
apply<GreetingPlugin>()

tasks.withType<GreetingTask>().configureEach {
    // setting convention from build script
    guest.convention("Guest")
}
build.gradle
tasks.withType(GreetingTask).configureEach {
    // setting convention from build script
    guest.convention("Guest")
}

请注意,对于项目特定值,您应该优先设置显式值(例如,使用 `Property.set(…​)` 或 `ConfigurableFileCollection.setFrom(…​)`),而不是约定,因为约定仅用于定义默认值。

从任务初始化

任务作者可以在任务构造函数或(如果是 Kotlin)初始化块中为惰性属性配置约定。此方法适用于具有简单默认值的属性,但如果需要额外上下文(任务实现外部)才能设置合适的默认值,则不适用。

build.gradle.kts
// setting convention from constructor
@get:Input
abstract val guest: Property<String>

init {
    guest.convention("person2")
}
build.gradle
// setting convention from constructor
@Input
abstract Property<String> getGuest()

GreetingTask() {
    guest.convention("person2")
}

在属性声明旁边

您可以在声明属性的位置旁边配置惰性属性的约定。请注意,此选项不适用于托管属性,并且与从任务构造函数配置约定具有相同的注意事项。

build.gradle.kts
// setting convention from declaration
@Input
val greeter = project.objects.property<String>().convention("person1")
build.gradle
// setting convention from declaration
@Input
final Property<String> greeter = project.objects.property(String).convention("person1")

使属性不可修改

任务或项目的大多数属性旨在由插件或构建脚本配置,以便它们可以为该构建使用特定值。

例如,指定编译任务输出目录的属性可能最初由插件指定一个值。然后构建脚本可能会将该值更改为某个自定义位置,然后当任务运行时,该值被任务使用。然而,一旦任务开始运行,我们希望阻止进一步的属性更改。这样,我们就可以避免由于不同的消费者(例如任务操作、Gradle 的最新检查、构建缓存或其他任务)对属性使用不同的值而导致的错误。

惰性属性提供了几种方法,您可以使用它们来禁止在值配置后更改其值。 finalizeValue() 方法计算属性的**最终**值并阻止对属性的进一步更改。

libVersioning.version.finalizeValue()

当属性的值来自 `Provider` 时,会查询 Provider 的当前值,结果成为属性的最终值。此最终值将替换 Provider,并且属性不再跟踪 Provider 的值。调用此方法还会使属性实例不可修改,任何进一步尝试更改属性值的操作都将失败。Gradle 会在任务开始执行时自动使任务的属性最终化。

finalizeValueOnRead() 方法类似,只是属性的最终值不会在查询属性值之前计算。

modifiedFiles.finalizeValueOnRead()

换句话说,此方法在需要时惰性地计算最终值,而 `finalizeValue()` 则急切地计算最终值。当值计算成本高昂或尚未配置时,可以使用此方法。您还希望确保属性的所有消费者在查询值时都能看到相同的值。

使用 Provider API

成功使用 Provider API 的指导原则:

  1. PropertyProvider 类型具有查询或配置值所需的所有重载。因此,您应该遵循以下准则:

    • 对于可配置属性,通过单个 getter 直接公开 Property

    • 对于不可配置属性,通过单个 getter 直接公开 Provider

  2. 不要试图通过引入额外的 getter 和 setter 来简化代码中的 `obj.getProperty().get()` 和 `obj.getProperty().set(T)` 等调用。使用此类包装方法会破坏 `Property` 的目的,并阻止属性之间的连接。它会导致立即获取当前值(而不是惰性评估)。

  3. 将您的插件迁移到使用 Provider 时,请遵循以下准则:

    • 如果是新属性,请通过单个 getter 将其作为 PropertyProvider 公开。

    • 如果是孵化中的属性,请将其更改为使用 PropertyProvider,并使用单个 getter。

    • 如果是稳定属性,请添加新的 PropertyProvider 并弃用旧的。您应该酌情将旧的 getter/setter 连接到新属性。

惰性集合 API 参考

将这些类型用于*可变*值

ListProperty<T>

值为 `List` 的属性

SetProperty<T>

值为 `Set` 的属性

惰性对象 API 参考

将这些类型用于*只读*值

Provider<T>

其值是 `T` 实例的属性

工厂

将这些类型用于*可变*值

Property<T>

其值是 `T` 实例的属性