基于提供程序值有条件地更改渐变属性



我正在编写一个Gradle约定插件,该插件使用Gradle的Lazy Configuration API来配置任务。在一种情况下,插件需要有条件地更改Property的值,而该条件基于Provider的有效值。也就是说,如果Provider具有某个值,则更新Property的值;否则,保持Property不变。

如果没有Provider语义,这将是一个简单的逻辑语句,如:

if (someValue > 10) {
property.set(someValue)
}

但是,由于Provider的值尚不清楚,这就更加复杂了。

我天真地尝试了以下操作,但它导致了堆栈溢出错误,因为该属性的转换器包括对同一属性的检索。

// stack overflow error
property.set(provider.map { if (it > 10) it else property.get() })

一个更完整的例子:

val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")
bar.set(foo.map { if (it != "foo") "baz" else bar.get()})

tasks.register("print") {
// goal is to print "baz", but it is a StackOverflowError
logger.log(LogLevel.LIFECYCLE, bar.get())
}

是否缺少一个API,允许我根据Provider的值有条件地更新Property的值?

在您的简化示例中,您实际上并不需要提供者。

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")
tasks.register("print") {
val evaluatedFoo = if (foo.get() != "foo") "baz" else bar.get()
logger.lifecycle("evaluatedFoo $evaluatedFoo")
}
// output:
// evaluatedFoo bar

这是因为记录器正在配置阶段工作(请参阅下面的"渐变阶段摘要")。通常(但并不总是)属性和提供者在配置阶段避免计算。

旁注:propertyprovider

这种差异类似于Kotlin的varvalproperty应用于让用户设置自定义值,provider用于只读值,如环境变量providers.environmentVariable("HOME")

为什么要使用提供程序

因为您使用的是"register",所以在需要之前不会配置该任务。所以我会调整你的例子,让事情变得更糟,看看它有多难看。

// build.gradle.kts
val foo: Provider<String> = providers.provider {
// pretend we're doing some heavy work, like an API call
Thread.sleep(TimeUnit.SECONDS.toMillis(10))
"foo"
}
val bar = objects.property(String::class).convention("bar")
// change from 'register' to 'create'
tasks.create("print") {
val evaluatedFoo = if (foo.get() != "foo") "baz" else bar.get()
logger.lifecycle("evaluatedFoo: $evaluatedFoo")
}

现在Gradle每次加载build.gradle.kts都需要10秒!即使我们不执行任务!或者是一项无关的任务!那不行。这是使用提供者的一个很好的理由。

解决方案

根据你实际想要实现的目标,这里有几种不同的路径。

仅在执行阶段登录

我们可以将log语句移动到doFirst {}doLast {}块。这些块的内容在执行阶段运行。这就是提供者的重点,将工作推迟到这个阶段。所以我们可以调用.get()来评估它们。

// build.gradle.kts
tasks.create("print") {
doFirst {
// now we're in the execution phase, it's okay to crack open the providers
val evaluatedFoo = if (foo.get() != "foo") "baz" else bar.get()
logger.lifecycle("evaluatedFoo: $evaluatedFoo")
}
}

现在,即使迫切地创建了任务,foobar也要等到执行时才能进行评估,此时可以进行工作。

合并两个提供者

我认为这个选项更接近你最初的要求。创建一个新的提供程序,而不是递归地将baz设置回自身。

Gradle将仅在调用fooBarZipped.get()时计算foobar

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")
val fooBarZipped: Provider<String> = foo.zip(bar) { fooActual, barActual ->
if (fooActual != "foo") {
"baz"
} else {
barActual
}
}
tasks.register("print") {
logger.lifecycle("fooBarZipped: ${fooBarZipped.get()}")
}

请注意,这个fooBarZipped.get()也会导致bar被评估,即使它可能不会被使用!在这种情况下,我们可以只使用map()(这与Kotlin的Collection<T>.map()扩展函数不同!)

映射提供程序

这个有点懒。

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")
val fooBarMapped: Provider<String> = foo.map { fooActual ->
if (fooActual != "foo") {
"baz"
} else {
bar.get() // bar will only be evaluated if required
}
}
tasks.register("print") {
logger.lifecycle("evaluatedFoo: ${fooBarMapped.get()}")
}

自定义任务

有时在build.gradle.kts中定义任务更容易,但通常更清楚的是专门创建MyPrintTask类。这部分是可选的——做任何对你的情况最有利的事情。

学习奇怪的Gradle风格的创建任务需要很多,所以我不会全部深入研究。但我想说的是@get:Input真的很重要。

// buildSrc/main/kotlin/MyPrintTask.kt
package my.project
abstract class MyPrintTask : DefaultTask() {
@get:Input
abstract val taskFoo: Property<String>
@get:Input
abstract val taskBar: Property<String>
@get:Internal
val toBePrinted: Provider<String> = project.provider {
if (taskFoo.get() != "foo") {
"baz"
} else {
taskBar.get()
}
}
@TaskAction
fun print() {
logger.quiet("[PrintTask] ${toBePrinted.get()}")
}
}

此外:您也可以使用Kotlin DSL定义输入,但它不像流体那样灵活

//build.gradle.kts
tasks.register("print") {
val taskFoo = foo // setting the property here helps Gradle configuration cache
inputs.property("taskFoo", foo)
val taskBar = foo
inputs.property("taskBar", bar)

doFirst {
val evaluated = if (taskFoo.get() != "foo") {
"baz"
} else {
taskBar.get()
}
logger.lifecycle(evaluated)
}
}

现在您可以在构建脚本中定义任务。

// build.gradle.kts
val foo = objects.property(String::class).convention("foo")
val bar = objects.property(String::class).convention("bar")
tasks.register<MyPrintTask>("print") {
taskFoo.set(foo)
taskBar.set(bar)
}

这里似乎没有太多好处,但如果foobar本身被映射或压缩,或者取决于任务的输出,那么@get:Input可能非常重要。现在Gradle将把任务链接在一起,所以即使你只运行gradle :print,它也知道如何触发一系列必要的前驱任务!


渐变阶段回顾

Gradle有3个阶段

  1. 初始化-在settings.gradle.kts中加载项目。这对我们现在来说并不有趣
  2. 配置-这是加载build.gradle.kts或定义任务时发生的情况
  3. 执行-任务被触发!运行任务,计算提供程序和属性
// build.gradle.kts
println("This is executed during the configuration phase.")
tasks.register("configured") {
println("This is also executed during the configuration phase, because :configured is used in the build.")
}
tasks.register("test") {
doLast {
println("This is executed during the execution phase.")
}
}
tasks.register("testBoth") {
doFirst {
println("This is executed first during the execution phase.")
}
doLast {
println("This is executed last during the execution phase.")
}
println("This is executed during the configuration phase as well, because :testBoth is used in the build.")
}

任务配置回避

https://docs.gradle.org/current/userguide/task_configuration_avoidance.html

为什么这与供应商和物业有关?因为他们确保两件事

  1. 在配置阶段没有完成工作

    Gradle注册任务时,我们不希望它实际运行任务!我们想把它推迟到需要的时候。

  2. Gradle可以创建"有向无环图">

    基本上,Gradle不是一条单一的生产线,有一个单一的起点。这是一个由有投入和产出的小工人组成的蜂巢式思维,Gradle根据工人从哪里获得投入将他们联系在一起。

最新更新