随着构建复杂性的增加,要知道特定值何时以及在何处配置变得难以跟踪。Gradle 提供了几种使用延迟配置来管理这种情况的方法。

writing tasks 4

理解延迟属性

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

延迟属性提供三个主要好处

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

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

  3. 改进构建性能: 避免配置期间资源密集型的工作,从而积极影响构建性能。例如,当配置值来自解析文件,但仅在运行功能测试时使用时,使用 Property 实例捕获此值意味着仅在运行功能测试时才解析该文件(例如,而不是在运行 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 属性的 task

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 显示问候语的 task
2 可配置的问候语
3 从问候语计算的只读属性
4 配置问候语
5 调用 Property.set() 的替代表示法
$ gradle greeting

> Task :greeting
Hi from Gradle

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Greeting task 具有 Property<String> 类型的属性来表示可配置的问候语,以及 Provider<String> 类型的属性来表示计算出的只读消息。消息 Provider 是使用 map() 方法从问候语 Property 创建的;当问候语属性的值更改时,其值会保持最新。

创建 Property 或 Provider 实例

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

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

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

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

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

当使用 Groovy 编写插件或构建脚本时,您可以将 map(Transformer) 方法与闭包一起使用,Groovy 会将闭包转换为 Transformer

同样,当使用 Kotlin 编写插件或构建脚本时,Kotlin 编译器会将 Kotlin 函数转换为 Transformer

连接属性

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

以下示例说明了如何将 task 的属性连接到项目扩展的属性

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

所有这些类型也被认为是延迟类型。

还有更强类型的模型用于表示文件系统的元素: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,您可以创建 FileCollection 实例,其中包含一组目录中包含的文件,使用 DirectoryProperty.files(Object...)Directory.files(Object...)

处理 task 输入和输出

许多构建都有多个 task 连接在一起,其中一个 task 消耗另一个 task 的输出作为输入。

为了使这项工作正常进行,我们需要配置每个 task 以知道在哪里查找其输入以及在哪里放置其输出。确保生产 task 和消费 task 配置为相同的位置,并在 task 之间附加 task 依赖关系。如果这些值中的任何一个可由用户配置或由多个插件配置,则这可能会很麻烦且脆弱,因为 task 属性需要在正确的顺序和位置配置,并且 task 依赖关系需要随着值的更改而保持同步。

Property API 通过跟踪属性的值和生成该值的 task,使此过程变得更加容易。

例如,考虑以下插件,其中生产者和消费者 task 连接在一起

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

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

此示例中需要注意的另一个重要事项是没有任何显式的 task 依赖关系。使用 Provider 表示的 task 输出会跟踪哪个 task 生成了它们的值,并且将它们用作 task 输入将隐式添加正确的 task 依赖关系。

隐式 task 依赖关系也适用于非文件的输入属性

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() 方法中配置延迟属性的约定,同时执行定义属性的 task 或扩展的初步配置。这对于常规插件(旨在分发并在野外使用)和内部 约定插件(通常以统一的方式为整个构建配置第三方插件定义的属性)都非常有效。

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

从构建脚本

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

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(…​)),而不是约定,因为约定仅用于定义默认值。

从 task 初始化

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

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

在属性声明旁边

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

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

使属性不可修改

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

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

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

libVersioning.version.finalizeValue()

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

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

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

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

延迟集合 API 参考

可变值使用这些类型

ListProperty<T>

一个属性,其值是 List<T>

SetProperty<T>

一个属性,其值是 Set<T>

延迟对象 API 参考

只读值使用这些类型

Provider<T>

一个属性,其值是 T 的实例

工厂

可变值使用这些类型

Property<T>

一个属性,其值是 T 的实例