我正在编写一个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
这是因为记录器正在配置阶段工作(请参阅下面的"渐变阶段摘要")。通常(但并不总是)属性和提供者在配置阶段避免计算。
旁注:
property
与provider
这种差异类似于Kotlin的
var
和val
。property
应用于让用户设置自定义值,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")
}
}
现在,即使迫切地创建了任务,foo
和bar
也要等到执行时才能进行评估,此时可以进行工作。
合并两个提供者
我认为这个选项更接近你最初的要求。创建一个新的提供程序,而不是递归地将baz
设置回自身。
Gradle将仅在调用fooBarZipped.get()
时计算foo
和bar
。
// 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)
}
这里似乎没有太多好处,但如果foo
或bar
本身被映射或压缩,或者取决于任务的输出,那么@get:Input
可能非常重要。现在Gradle将把任务链接在一起,所以即使你只运行gradle :print
,它也知道如何触发一系列必要的前驱任务!
渐变阶段回顾
Gradle有3个阶段
- 初始化-在
settings.gradle.kts
中加载项目。这对我们现在来说并不有趣 - 配置-这是加载
build.gradle.kts
或定义任务时发生的情况 - 执行-任务被触发!运行任务,计算提供程序和属性
// 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
为什么这与供应商和物业有关?因为他们确保两件事
在配置阶段没有完成工作
Gradle注册任务时,我们不希望它实际运行任务!我们想把它推迟到需要的时候。
Gradle可以创建"有向无环图">
基本上,Gradle不是一条单一的生产线,有一个单一的起点。这是一个由有投入和产出的小工人组成的蜂巢式思维,Gradle根据工人从哪里获得投入将他们联系在一起。