随着构建变得越来越复杂,何时何地配置某个特定值变得难以追踪。Gradle 提供了几种使用 惰性配置 来管理这个问题的方法。

writing tasks 4

理解惰性属性

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

惰性属性提供三个主要优点

  1. 延迟值解析:允许连接 Gradle 模型,而无需事先知道属性的值何时确定。例如,您可能想根据扩展的源目录属性来设置任务的输入源文件,但在构建脚本或某些其他插件配置这些属性之前,扩展属性的值是未知的。

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

  3. 改进构建性能:避免在配置阶段进行资源密集型工作,从而对构建性能产生积极影响。例如,当配置值来自解析文件,但仅在运行功能测试时才使用时,使用属性实例来捕获此值意味着只有在功能测试运行时(而不是在例如运行 clean 时)才会解析该文件。

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

Provider

表示一个只能查询但不能更改的值。

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

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

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

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

Property

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

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

  • Property 扩展了 Provider 接口。

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

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

  • 可以使用工厂方法 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<String> 的属性用于表示可配置的问候语,还有一个类型为 Provider<String> 的属性用于表示计算出的只读消息。通过使用 map() 方法,从问候语 Property 创建了消息 Provider;其值随着问候语属性值的变化而保持最新。

创建 Property 或 Provider 实例

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

在前面的示例中,展示了两个工厂方法

有关所有可用类型和工厂,请参阅快速参考

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

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

使用 Groovy 编写插件或构建脚本时,可以将 map(Transformer) 方法与 Closure 一起使用,Groovy 会将 Closure 转换为 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() 创建的 Provider。

处理文件

处理文件中,我们介绍了用于 File-like 对象的四种集合类型

只读类型 可配置类型

FileCollection

ConfigurableFileCollection

FileTree

ConfigurableFileTree

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

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

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

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

DirectoryProperty 还可以通过 DirectoryProperty.dir(String)DirectoryProperty.file(String) 分别为 DirectoryRegularFile 创建惰性求值的 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 可以转换为 FileTree,从而可以使用 DirectoryProperty.getAsFileTree()Directory.getAsFileTree() 查询目录中包含的文件和目录。从 DirectoryPropertyDirectory 中,您可以使用 DirectoryProperty.files(Object...)Directory.files(Object...) 创建包含目录中一组文件的 FileCollection 实例。

处理任务输入和输出

许多构建都有多个相互连接的任务,其中一个任务将另一个任务的输出作为输入来消费。

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

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

处理 Map

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

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

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 时,Gradle 会查询该 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) 的调用。

  3. 将插件迁移到使用 Provider 时,请遵循以下指南

    • 如果是新属性,请使用单个 Getter 将其暴露为 PropertyProvider

    • 如果正在孵化中(incubating),请更改为使用单个 Getter 将其暴露为 PropertyProvider

    • 如果是稳定属性,请添加新的 PropertyProvider,并废弃旧的。您应将旧的 Getter/Setter 适当地连接到新属性中。

Property Files API 参考

可变 值使用这些类型

RegularFileProperty

磁盘上的文件

DirectoryProperty

磁盘上的目录

ConfigurableFileCollection

非结构化文件集合

ConfigurableFileTree

文件层级结构

SourceDirectorySet

源目录层级结构

惰性集合 API 参考

可变 值使用这些类型

ListProperty<T>

一个值是 List<T> 的属性

SetProperty<T>

一个值是 Set<T> 的属性

惰性对象 API 参考

只读 值使用这些类型

Provider<T>

一个值是 T 实例的属性

工厂方法

可变 值使用这些类型

Property<T>

一个值是 T 实例的属性