隨著建置複雜度的增加,要知道特定值在何時何地配置變得難以追蹤。 Gradle 提供了幾種方法來使用延遲配置來管理此問題。

writing tasks 4

理解延遲屬性

Gradle 提供了延遲屬性,它會延遲計算屬性的值,直到實際需要時才計算。

延遲屬性提供三個主要優點

  1. 延遲值解析: 允許連接 Gradle 模型,而無需知道何時會知道屬性的值。 例如,您可能想要根據擴充功能的來源目錄屬性來設定 task 的輸入原始檔,但在建置腳本或某些其他外掛配置它們之前,擴充功能屬性值是未知的。

  2. 自動 Task 相依性管理: 將一個 task 的輸出連接到另一個 task 的輸入,自動判斷 task 相依性。 屬性實例攜帶有關哪個 task(如果有的話)產生其值的資訊。 建置作者無需擔心使 task 相依性與配置變更保持同步。

  3. 改善建置效能: 避免在配置期間進行資源密集型工作,從而正面影響建置效能。 例如,當配置值來自解析檔案,但僅在執行功能測試時使用時,使用屬性實例來捕獲此值意味著僅在執行功能測試時(而不是在執行 clean 時,例如)才解析檔案。

Gradle 使用兩個介面來表示延遲屬性

Provider

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

  • 具有這些類型的屬性是唯讀的。

  • 方法 Provider.get() 傳回屬性的目前值。

  • 可以使用 Provider.map(Transformer) 從另一個 Provider 建立 Provider

  • 許多其他類型擴充了 Provider,並且可以在需要 Provider 的任何地方使用。

Property

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

  • 具有這些類型的屬性是可配置的。

  • Property 擴充了 Provider 介面。

  • 方法 Property.set(T) 指定屬性的值,覆寫可能存在的任何值。

  • 方法 Property.set(Provider) 為屬性的值指定 Provider,覆寫可能存在的任何值。 這可讓您在配置值之前將 ProviderProperty 實例連接在一起。

  • Property 可以由工廠方法 ObjectFactory.property(Class) 建立。

延遲屬性旨在傳遞,並且僅在需要時查詢。 這通常發生在執行階段期間。

以下示範了一個具有可配置 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 輸出和輸入是在定義任何位置之前連接的。 setter 可以在執行 task 之前的任何時間呼叫,並且變更將自動影響所有相關的輸入和輸出屬性。

此範例中另一個需要注意的重要事項是缺少任何明確的 task 相依性。 使用 Providers 表示的 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 開始執行,我們就希望防止進一步的屬性變更。 這樣,我們可以避免因不同的消費者(例如 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

    • 如果是孵化中的屬性,請將其變更為使用 PropertyProvider 與單個 getter。

    • 如果是穩定的屬性,請新增新的 PropertyProvider 並棄用舊的屬性。 您應該根據需要將舊的 getter/setter 連接到新的屬性。

延遲集合 API 參考

將這些類型用於可變

ListProperty<T>

其值為 List<T> 的屬性

SetProperty<T>

其值為 Set<T> 的屬性

延遲物件 API 參考

將這些類型用於唯讀

Provider<T>

其值為 T 實例的屬性

工廠

將這些類型用於可變

Property<T>

其值為 T 實例的屬性