読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

kaptで発生するエラーを回避するworkaround (JSR 269ライブラリ作者向け)

KotlinでJSR 269のライブラリを使う場合以下の様にkaptを使って設定するわけですが不安定な動きをする場合があります。

kapt {
    generateStubs = true
}
dependencies {
  compile 'com.github.sys1yagi.fragment-creator:library:0.6.0'
  kapt 'com.github.sys1yagi.fragment-creator:processor:0.6.0'
}

次のようなエラーが出たり、ファイルが生成されたりされなかったり。

:app:compileDebugKotlin UP-TO-DATE
:app:compileDebugJavaWithJavac FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:compileDebugJavaWithJavac'.
> java.lang.NullPointerException

ちょうどJSR 269を使ったライブラリを2つ作っていたので原因を探りつつ回避する方法がないか調べていたらworkaroundを見つけたのでメモします。

annotation processor内で起こっている事

NullPointerExceptionが起こっているStackTraceに従って該当箇所を見てみると以下の様な処理をしていました。

//ライブラリが提供するアノテーションを取り出す
Simplify simplify = element.getAnnotation(Simplify.class); 
// ここでNullPointerException
String actionName = simplify.value(); 

ここではコードに付与したSimplifyアノテーションのパラメータ取り出してコード生成に利用しようとしています。elementはRoundEnvironment.getElementsAnnotatedWith(Simplify.class)で取り出しているので必ずSimplifyアノテーションを持っているはずなのですがnullが返ってきてエラーとなっているわけです。

なぜnullに?

Better Annotation Processing: Supporting Stubs in kapt | Kotlin BlogSource-retained annotationsの部分を読むとRetentionPolicy.SOURCEアノテーションはgenerateStubsでは直接.classを生成するので消失するよと書いています。このケースは生成したコードにRetentionPolicy.SOURCEアノテーションを付与していた場合消失するという事だと思うのでannotation processorで処理する時には関係ないように思うのですがどうもannotation processorに渡ってくるものもkt->classを経由してRetentionPolicy.SOURCEアノテーションが消失しているぽいです(にもかかわらずRoundEnvironment.getElementsAnnotatedWith()で取り出せるのが謎です)。

しかも初回ビルド時はアノテーションが保持されている状態で渡ってきて、二回目以降は消失するという挙動をするので「不安定だ」という印象を持ってしまうのだと思います(実際どういう風になっているかはkotlin-pluginの実装などを調べてみないとわかりませんが)。

//一回目 ちゃんと処理できる
@Simplify("polling")
public final class PollingAlarmProcessor implements AlarmProcessor {

//二回目以降 書き換わっている
@KotlinClass(version={1, 0, 1}, abiVersion=32, data={"\035\025\tA\"A\003\002\031\005)\021\001B\001\006\003!\tQ\001A\003\002\031\005)\001!B\001\r\003\021\035A\002A\r\0021\003\t+!U\002\002\021\005)C\002B\006\t\0045\t\001DA\r\004\021\013i\021\001G\002\032\007!\035Q\"\001\r\005"}, strings={"Lcom/sys1yagi/longeststreakandroid/alarm/PollingAlarmProcessor;", "Lcom/sys1yagi/android/alarmmanagersimplify/AlarmProcessor;", "()V", "process", "", "context", "Landroid/content/Context;", "intent", "Landroid/content/Intent;"})
public final class PollingAlarmProcessor implements AlarmProcessor {

ライブラリ側の対処方法

realm-javaがこの問題に既に対応していました。 Support KotlinLang · Issue #509 · realm/realm-java · GitHub

以下のようにライブラリ側でアノテーションに定義している@Retention(RetentionPolicy.SOURCE)@Retention(RetentionPolicy.CLASS)にすれば問題がでなくなります。

// workaround for kapt
// @Retention(RetentionPolicy.SOURCE)
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface FragmentCreator {
}

やったね。

利用側の対処方法

別の対処法としてはアノテーションを書くファイルを.javaにする事です。これでつねに.javaがannotation processorで処理されるので安全です。とは言えこれだとKotlinにした意味がなくなるのでできるだけ避けたい所です。利用しているJSR 269のライブラリにPull Requestを出すといいんじゃないでしょうか。

enjoy

@Retention(RetentionPolicy.SOURCE)@Retention(RetentionPolicy.CLASS)の違いは.classにアノテーションが残るかどうかなのでまぁ問題ないと思います。Runtimeには乗らないので精々数百byteくらいclassファイルが大きくなる程度じゃないかなと思います。。