こんな感じのコードを書いてました
Gradleプラグインの開発中、以下のようなコードを書いていました。
allメソッドはクロージャ(ラムダ式?)を渡された時点でコレクションが持っているオブジェクトを流し込んでくれる上、
後から追加したオブジェクトも随時クロージャに流し込んでくれるオブザーバっぽい動きもします。
この動きを利用して、buildTypesとflavorsに更新があった際にvariantsへオブジェクトを追加するというもの。
open class Yakiniku(val project: Project) {
val buildTypes: NamedDomainObjectContainer<ProjectBuildOptions> =
project.container(ProjectBuildOptions::class.java)
val flavors: NamedDomainObjectContainer<ProjectFlavorOptions> =
project.container(ProjectFlavorOptions::class.java)
val variants: DomainObjectSet<BuildVariant> =
DefaultDomainObjectSet<BuildVariant>(BuildVariant::class.java, CollectionCallbackActionDecorator.NOOP)
init {
buildTypes.all { buildOption ->
variants.add(BuildVariant(buildOption, null))
}
flavors.all { flavorOption ->
buildTypes.all { buildOption ->
variants.add(BuildVariant(buildOption, flavorOption))
}
}
}
}
class BuildVariant(val build: ProjectBuildOptions?, val flavor: ProjectFlavorOptions?) : ObjBase() {
fun getName() = listOfNotNull(build, flavor).joinToString("") { it.name.capitalize() }
override fun toString(): String {
return getName()
}
}
で、build.gradle側からこんな感じで呼び出したかったんです。
apply plugin: 'com.osm.sample.gralde.yakiniku'
yakiniku {
buildTypes {
debug {
target "hogehoge"
}
release {
target "fugafuga"
}
}
flavors {
free {
target "piyopiyo"
}
paid {
target "こんにちは!"
}
}
// ★★★ここ★★★
variants.all {
println(it)
}
"debugFree"とか"releasePaid"とかを期待していたわけなんですが…動かしてみると、null。
呼び出される回数は想定通り。しかし全部null。
あるぇ…
ソースを追跡して原因を特定するため、スタックトレースを吐き出しつつ紐解いていったところ、
以下のような箇所を見つけました。
public static <T> T configure(@Nullable Closure configureClosure, T target) {
if (configureClosure == null) {
return target;
} else {
if (target instanceof Configurable) {
((Configurable)target).configure(configureClosure);
} else {
configureTarget(configureClosure, target, new ConfigureDelegate(configureClosure, target));
}
return target;
}
}
/** 中略 */
private static <T> void configureTarget(Closure configureClosure, T target, ConfigureDelegate closureDelegate) {
if (!(configureClosure instanceof GeneratedClosure)) {
(new ClosureBackedAction(configureClosure, 1, false)).execute(target);
} else {
Closure withNewOwner = configureClosure.rehydrate(target, closureDelegate, configureClosure.getThisObject());
(new ClosureBackedAction(withNewOwner, 2, false)).execute(target);
}
}
このtargetという変数には、巡り巡ってaddされたオブジェクトが入ってくるようです。
これだけ見てると、targetがConfigurableじゃなければうまくいきそう。
しかし、スタックトレースを見ているとtargetがConfigurableとして判定されて、
configure()が呼ばれた形跡があったので「んん?」となったわけです。
おかしいなあと思ってクラスの定義を追ってみました。ObjBaseになんかあるのかなあ。
class BuildVariant(val build: ProjectBuildOptions?, val flavor: ProjectFlavorOptions?) : ObjBase() {
fun getName() = listOfNotNull(build, flavor).joinToString("") { it.name.capitalize() }
override fun toString(): String {
return getName()
}
}
どれどれ…
open class ObjBase: Configurable<Any> {
var resolveStrategy = Closure.DELEGATE_FIRST
override fun configure(closure: Closure<*>?): Any {
closure?.also {
it.delegate = this
it.resolveStrategy = resolveStrategy
it.run()
}
return this
}
}
ちくしょう!
ObjBaseの継承をやめたところ、想定通りallメソッドに渡したクロージャの中でaddされたオブジェクトが取れました。
まとめ
- addするオブジェクトがConfigurableの実装であるかどうかによって、DomainObjectCollection#all()の挙動が変わる。
- Configurableの実装である場合、addされたオブジェクトが持ってるconfigureが呼び出される。
- Configurableの実装でない場合、DomainObjectCollection#all()に渡したクロージャに、addされたオブジェクトが渡される