protobuf, gRPCを使っているサーバーと通信するために公式が用意しているコード生成を使用しています。
https://github.com/protocolbuffers/protobuf/tree/master/java
https://github.com/grpc/grpc-java
https://github.com/grpc/grpc-kotlin (バックエンドはgrpc-javaで、追加でcoroutine対応があるくらいのやつ。今回はあんまり関係ない。)
現状の問題点
java7で java.annotation.Nonnull
が使えないのでNullアノテーションを追加する気は無いらしい。
https://github.com/protocolbuffers/protobuf/issues/4351#issuecomment-390767828
grpc側も特に話は進んでいない。
https://github.com/grpc/grpc-java/issues/4724
non-nullアノテーションが無いとどうなるかというと。
public class JavaClass { String getString() { return ""; } }
String!
と表示されています。javaの情報だけではどっちかわからないので、どっちにもなれる状態を表しています。
class Kotlin { init { JavaClass().value } }
なので以下のようにnullableがありえる事を明示的に指定すれば特に問題は起きません。
val value: String? = JavaClass().value println("value=$value") // value=null
しかし、non-nullを明示的に指定したり、non-nullな関数の引数に入れた時点でNullPointerExceptionが起きます。
val value: String = JavaClass().value println("value=$value")
しかし、protobufのJavaではnullが返されることは無いそうです。
https://github.com/protocolbuffers/protobuf/issues/4351#issuecomment-374803044
non-nullかnullableかわからない状態の場合、特にnullチェックをしなくてもいいのですが、そのような事を覚えておかなければなりませんし、一緒に開発している人にも知らせておかないとnullチェックを行う冗長なコードが上がってくるかもしれません。
しかし、 package-info.java
を使えばパッケージ下のデフォルトのアノテーションを設定できるそうです。
https://github.com/protocolbuffers/protobuf/issues/4351#issuecomment-390841619
package-info.java を差し込む
既に .proto
からJava及びKotlinの拡張のコードを生成できているものとして進めます。
packageごとに以下の package-info.java
を入れれば強制的にnon-nullになります。
詳細はKotlinの仕様を読むと良いです。
以下は自分が読んだ時点のURLなので、最新版を読んでください。
https://github.com/Kotlin/KEEP/blob/5e2d3dd1f81b237c3010461793c34bdf3857be9f/proposals/jsr-305-custom-nullability-qualifiers.md
@NonNullApi package com.exsample; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.annotation.Nonnull; import javax.annotation.meta.TypeQualifierDefault; @Nonnull @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @interface NonNullApi { }
子のpackageには適用されないので、全てのpackageに入れる必要があります。
Gradle で package-info.java
を差し込む。
Protobufを自動で生成しているのだから、 package-info.java
も自動で生成されるようにしましょう。
タスクの順番を決める
protobufプラグインでクラスが生成された後、クラスファイルが生成される前にタスクを差し込みたいわけです。なので以下でタスクの順番を指定します。このあたりはややこしくて触っていると循環参照になったりするのでよくわからないです。
build.gradle.kts
// まずはタスクを定義しましょう val insertJavaInfoTask = tasks.create("insertJavaInfo") // ProtoからJavaコードを生成した後に `insertJavaInfoTask` を実行したいので指定します。 tasks.withType<GenerateProtoTask> { finalizedBy(insertJavaInfoTask) } // `compileKotlin` の前に `insertJavaInfoTask` を実行するように指定します。 tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { dependsOn(insertJavaInfoTask) } insertJavaInfoTask.apply { doLast { // 後はここにコードを差し込む処理を入れるだけ } }
コードを出力する
今回は簡単な決まったものを入れるだけなのでPoetなどのコード生成ツールは使用しません。
doLastの部分に以下のコードを差し込みます。このあたりはとりあえず動くコードという感じです。
val mainSourceSet = project.extensions.getByType(JavaPluginExtension::class.java) .sourceSets .getByName(SourceSet.MAIN_SOURCE_SET_NAME) val loader = URLClassLoader( mainSourceSet.runtimeClasspath.map { it.toURI().toURL() }.toTypedArray(), Thread.currentThread().contextClassLoader ) val classes = ClassPath.from(loader).getTopLevelClassesRecursive("com.exsample.packagename").map { it.load().kotlin }.filterNot { it.jvmName.endsWith("Kt") } val outRootFile = File(project.rootDir, "build/generated/source/proto/main/info") classes.map { it.java.packageName }.toSet().forEach { packageName -> val packagePath = packageName.split(".").joinToString("/") val file = File(File(outRootFile, packagePath), "package-info.java") val source = """ @ApiNonNull package $packageName; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import javax.annotation.Nonnull; import javax.annotation.meta.TypeQualifierDefault; @Nonnull @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @interface ApiNonNull { } """.trimIndent() file.parentFile.mkdirs() file.writeText(source) }
ソースの指定を忘れずに
// 後はソースの指定を忘れなければ大丈夫です。
sourceSets { main { java { srcDirs( "build/generated/source/proto/main/grpckt", "build/generated/source/proto/main/grpc", "build/generated/source/proto/main/info" ) } } }
これで取得できるものは non-null
扱いになりました。
その他
後は上記プロジェクトそのまま使ったり、mavenに上げたり、publishToMavenLocalしたりしてください。
引数は警告が出るものの、コンパイルエラーにはなりません。
以下のオプションを設定するとエラーになってくれます。 (jarとしてライブラリとして配布する場合は、使用する側で設定します)
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
}
}
おわりに
早くちゃんと対応されると良いんですけどね。
Kotlinサポートは力入れていくみたいなので。
https://github.com/protocolbuffers/protobuf/issues/3742#issuecomment-843591924