LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


サーバーサイドKotlinの知見/KotlinでClova Skill Award挑戦

はじめに

LINE Engineering Blogをご覧の皆さまこんにちは!今日の記事は2名の連名によってお送りします。LINEでゲームプラットフォームを開発しているKagayaと、ライブ配信サービスLINE LIVEのAndroidアプリを開発しているakiraです。こちらはLINE Engineering Blog「夏休みの自由研究 -Summer Homework-」の9日目の記事です。

皆さん、Kotlin使ってますか?Kotlinは、JVM上で動作する静的型付きプログラミング言語で、IntelliJ IDEAやAndroid StudioなどのIDEでおなじみのJetBrainsによる主導で開発が行われています。昨年のGoogle I/OにてAndroid向けの公式開発言語となり話題を呼んだのが記憶に新しいですね。弊社のEngineering Blogでも以前「LINE Creators Studio開発に使われるKotlinのご紹介」という記事でAndroidアプリの開発に利用しているというお話をご紹介しました。LINEでは新規アプリはもちろん、既存のアプリでも積極的にKotlinへの変換・リファクタリングが行われている最中です。メッセージングサービスのLINEアプリをはじめ、LINE LIVEやLINE Creators Studio、AI アシスタントのClova用アプリなど多くのAndroidアプリがKotlinを導入しています。

一方、サーバサイド開発においても各チームで少しずつKotlinの導入がスタートしています。本記事では

  1. Kagayaが担当しているLINE GAMEプラットフォームにおけるKotlinの利用
  2. この記事の筆者二人が同じチームで参加し見事(?)最優秀賞を獲得した社内向けClova Skill AwardでKotlinを使ってみた話

という2つの事例を通してサーバサイドKotlinの知見・ユースケースについて書きたいと思います。もちろん、いくつかの内容はサーバサイド・Android開発に共通する内容となります。Androidアプリを開発している皆さんもこの機会にKotlinでのサーバサイド開発をはじめてみてはいかがでしょうか。非常にすんなりと書き始められることかと思います。

ご応募絶賛募集中の優勝賞金1000万円の弊社による開発コンテストLINE BOOT AWARDS 2018も、ぜひKotlinでチャレンジしてみてください!

1. LINE GAMEプラットフォーム サーバサイド開発におけるKotlin

LINE GAMEでは、多くのゲームが外部の開発会社やグループ会社のゲームスタジオによって制作されています。そういったゲーム開発において、より本質的なゲームロジックの開発に専念できるよう、基礎的な機能(認証、ソーシャルグラフなど)やプローモーション・コミュニケーションに関連する機能(ユーザーターゲティング、お知らせ表示機能、ゲーム内掲示板・チャットなど)などを提供しているのがLINE GAMEプラットフォームです。昨年のLINE DEVELOPER DAYでも一部その内容をご紹介しましたので、ご来場いただいた方はご存知かもしれません。プラットフォームという性質上たくさんの独立したコンポーネントがありますが、新規開発の案件をスクラッチから始めることも多く、その一部でKotlinを利用しています。

どんなAPIで使っているか

簡単にどんなコンポーネントで利用しているかを紹介します。LINE GAMEプラットフォームでは、LINE連携をしたユーザ向けにまだそのゲームをプレイしてないLINE友だちを招待する機能を以前から提供しています。今年、さらにそれに加えてプラットフォーム独自のアルゴリズムで、より「ゲームをやってくれそうな」ユーザを推薦する機能をリリースしました。簡略化したアーキテクチャは以下のイメージです。

まず推薦を行うための設定を読み取り(1)、ユーザの友だち情報を取得し(2)、友だちそれぞれの情報を取得して(3)、それを Recommendation APIサーバ内でランキングして、ユーザに返却します。図中(1)〜(3)がネットワーク越しのアクセスとなります。こちらのRecommendation APIサーバを、Kotlin + Spring Boot 1.5で実装しています。

Kotlinの特長

Javaとの互換性

さて、私たちのチームでは、ある程度の単位でモノリシックなGradle Multi Project構成を取っており、その中の一部subprojectのみKotlinで書かれています。そうすると、Javaで書かれたライブラリをKotlinで書かれたアプリケーションから呼ぶようなケースもあります。Kotlinの公式HPを見ると、

100% interoperable with Java™

との記述が目に入ります。この言葉通り、Javaとの互換性が非常に高く、上記のような相互呼び出しも特に問題ありません(完全にないわけではないですが、そちらは後述します。)。また、まだ歴史の浅いプログラミング言語であるからか、純粋なKotlinで書かれた外部ライブラリなどはあまり選択肢が多くありません(例えばO/Rマッパー、HTTPクライアントなど)。しかし、当然Javaで書かれたライブラリが利用できるので、RetrofitでもMyBatisでもKotlinから簡単に利用できます。また、言語機能としてExtensionを備えているので、すでに存在するライブラリをKotlin対応させるのも難しくありません。例えば、JSONライブラリであるJacksonのKotlin向け追加モジュールjackson-module-kotlinがわかりやすい例でしょう。

コルーチン

Kotlinは言語機能としてcoroutine(コルーチン)をサポートしています。coroutineとは、協調的マルチタスクを実現する軽量なタスク、およびその実装を指します。一般的なスレッドと違ってネイティブスレッドにマッピングされるわけではなく、coroutine同士を移動する際コンテキストスイッチを引き起こさないために高速な計算が可能です。当然Webアプリケーションの実装においては非同期に処理したいケースが多く存在するので、そういった際に有用ですね。

本APIサーバでは、リクエストしたユーザの友だちのユーザ情報を他のサービスから取得する部分のHTTP呼び出し(前述の図中(3))を並列化するためにcoroutineを利用しています。実際のコードを簡略化すると以下のようなイメージです。

val ctx = newFixedThreadPoolContext(NUMBER_OF_TRHEADS, "user")

fun getFriends(userIds: List<String>): Map<String, UserProfile> = runBlocking {
    userIds.chunked(PROFILE_MAX_PER_REQUEST).map { subset ->
        async(ctx) {
            mapper.readValue<List<UserProfile>>(khttp.get(URL, params = mapOf("ids" to subset.joinToString(","))).text)
        }
    }.map {
        it.await()
    }.flatMap {
        it
    }.associate {
        it.userId to it
    }
}

キャッシュにヒットする確率も高いので、シンプルなブロッキングHTTPクライアントであるkhttpを利用して、CoroutineContextに独自のスレッドプールに基づくディスパッチャを利用することで並列にかつ同時実行リクエストを制限しつつ実行しています。プロフィールを返すAPI側の複数プロフィールに対応するエンドポイントを利用するのですが、こちらの同時に取得できるユーザIDの最大値(PROFILE_MAX_PER_REQUEST)に基づきリクエストを分割しています。

さて、上記のサンプルはシンプルに非同期処理を実装できています...が、実際にはcoroutineの用途としては必ずしも良い例とは言えません。coroutineはsuspend function(中断関数)と呼ばれる特殊な関数内部で処理を一旦「中断」することでCPUを有効活用できるのが特徴なので、ノンブロッキングI/Oとの相性が良くなっています。また、coroutineの目標の一つは従来の非同期処理の手法であるコールバックやFuture/Promiseベースの実装を改善することが目標で、あたかも同期処理かのように非同期処理を書けることが特徴です。そのため、もっと複雑にリモートサービスを呼び出しながら処理するケースのほうが利点をより享受できるでしょう。例えば、上記の例においてNettyベースのHTTPクライアントであるAsyncHttpClientを利用した場合、次のように実装できます。

val client = asyncHttpClient()
    
fun getFriends2(userIds: List<String>): Map<String, UserProfile> {
    return userIds.chunked(PROFILE_MAX_PER_REQUEST).map { subset ->
        asyncApiCall(subset.joinToString(","))
    }.map {
        runBlocking {
            it.await()
        }
    }.flatMap {
        it
    }.associate {
        it.userId to it
    }
}

fun asyncApiCall(idsStr: String) = async {
    val response = client.prepareGet(URL).addQueryParam("ids", idsStr).execute().await()
    mapper.readValue<List<UserProfile>>(response.responseBody)
}

suspend fun <T> ListenableFuture<T>.await(): T = suspendCoroutine { continuation ->
    toCompletableFuture().whenComplete { result, exception ->
        if (exception == null) {
            continuation.resume(result)
        } else {
            continuation.resumeWithException(exception)
        }
    }
}

ちなみに、この例では同時リクエスト数は制限されていないので、AsyncHttpClientのconfigで調整するか、coroutineでpub-subのような機能を提供するChannelを利用するのが良いでしょう。

上記のように、すでに存在する非同期通信ライブラリとの連携の実装は非常に簡単です。また、いくつかのライブラリ(Rx、Reactor、nio、GuavaのListenableFutureなど)はすでに公式の連携実装があります。

Coroutineの基礎については、公式のガイドおよびJetBrainsのRoman Elizarov氏によるKotlinConf 2017の発表が最も参考になります。また、上記のような既存のfuture形式、コールバック形式のライブラリとの連携を含め、coroutineがいかに実装されているかは同じくElizarov氏による発表「Deep Dive into Coroutines on JVM」において非常に丁寧に解説されています。特に後者の発表はcoroutineの実装を理解し正しく利用する上で是非一度目を通すべきだと思います。

Kotlinにおけるcoroutineまだ実験的機能としての位置づけですが、JetBrainsのAndrey Breslav氏や前述のElizarov氏によれば、coroutineはすでにproduction readyであり、experimentalであるのは今後ユーザのフィードバックに基づいて設計を調整する可能性があるためだと説明されています。なお、Kotlin 1.3でcoroutine機能はstableになる予定です。また、kotlinx.coroutines.experimentalパッケージは最終版リリース後も残されるようなので、Kotlinをアップデートしたとしてもしばらくはそのまま利用できそうです。

その他

前述の弊社ブログの既存記事:「LINE Creators Studio開発に使われるKotlinのご紹介」を始めとして、インターネット上で見られるたくさんの記事に挙げられている一般的なKotlinの利点は、サーバサイド・クライアント開発ほぼ関係なく享受することができます。

  • 簡潔にPOJOを記述できるdata class
    • 私達のチームではLombokを長年利用していましたが、Kotlinでは黒魔術の利用は不要です。APIサーバの開発ではDTOを多く記述する場合がありますが、その場合でも非常に有用です。
  • null-safety
  • valによる再代入不可能性の簡潔な定義
  • enumの拡張であるsealed class
  • operator overloading
  • etc...

などなど、Javaから移行してコーディングを始めるとおっ!と思う機能がたくさんあります。個人的にはtry-catch文やif文が式になっているのが使いやすくて好きです。より詳しく知りたい方は、是非公式リファレンスをざっと眺めるとわかりやすいと思います。公式リファレンスの読みやすさも、さすがIDEを開発しているJetBrainsだなあと思います。

Kotlinサーバ開発でハマりやすい点

我々のチームでは、現状メインでSpring Boot 1.5.x 系を利用しているので、Spring MVC 4系を間接的に扱っています。Spring 5・Spring Boot 2系から公式にKotlinサポートが謳われているのですが、いくつかの問題はあるもののSpring 4でもKotlinを使うことは可能になっています。Springとの連携も含めていくつかハマりやすいポイントを紹介します。

リクエストパラメータのバインディングに(そのままでは)data classを活用できない

Springでは@RequestMappingアノテーション(およびその拡張の@GetMappingなど)を付与したメソッドが各エンドポイントに対応し、その引数にPOJOを指定することでパラメータをPOJOにバインディングすることが可能です。しかし、Springのこちらのバインディング機能は、バインディング担当するModelAttributeMethodProcesserクラスの実装からもわかるように、

  1. POJOがデフォルトコンストラクタをもっていること
  2. 各フィールドが適切なセッターもっていること

が必要です。そのため、valプロパティだけをもっているdata classなどを指定すると、java.lang.NoSuchMethodExceptionが発生してしまいます。1.の問題だけであれば公式に提供されているNo-arg compiler pluginでも解決できますが、せっかく存在するnullable型(String?など)を利用できない上、セッターが必要なのでvalも利用できません。我々は現状、DTOにdata classではなく普通のclassを利用し、optionalなデータにはvar、requiredなものにはlateinit varを付加することで対応し、コンストラクタではなくクラス内部でプロパティ定義することで対応しています。ちなみに、data classでもlateinit varは利用できますが、lateinit varはコンストラクタでは使えないのでクラス内部で定義する必要があり、クラス内部に定義してしまうとtoStringhashCodeなど自動生成されるメソッドの制御対象外になってしまう問題があります。

なお、この問題はSpring 5系では解決されています。詳しい実装についてはModelAttributeMethodProcesserクラスの4系・5系の実装を比較すると理解の助けになると思います。もし何らかの事情でSpring Boot 1.5系以前を利用している場合は、この部分だけ独自にArgumentResolverを実装するというのも選択肢としてはありかもしれません。

@Componentなどのアノテーションが(そのままでは)利用できない

Spring は @Component@Service などのアノテーションが付加されたクラスを自動でBeanとして登録してくれますが、この時内部ではdynamic proxyを用いてクラスを拡張しています。しかし、Kotlinのクラスはデフォルトですべてfinalなので、明示的にopen識別子を付加しなければSpringのコンポーネントスキャンに失敗します。そのために、kotlin-springというGradleプラグインが用意されており、コンパイル時に自動でSpringが拡張する必要がある(可能性のある)クラスをopenにしてくれます。Spring Initalizrを利用する場合は最初からbuild.gradleに記載がありますが、フルスクラッチでアプリケーション開発を始める場合は注意が必要です。

Kotlinでは検査例外がないのでJavaから呼ぶとtry-catchできない

Kotlinでは検査例外が存在しません。そのため、Kotlinから(たとえJavaの検査例外をKotlinからthrowする場合でも)例外を投げたとしても、Kotlinで使う分にはtry-catch文を強制されません。一方で、JavaとKotlinを相互運用していて、JavaからKotlinのコードを呼ぶような場合は、そのままではtry-catch文がコンパイルエラーになります。これを防ぐためには@Throwsアノテーションを追加します。

@Throws(IOException::class)
fun throwSomeException(): String

これは、Spring AOPなどでCGLIBを利用してクラスをプロキシする場合にも問題になります。CGLIBでは、throws節に含まれない検査例外が投げられると、UndeclaredThrowableExceptionという非検査例外が投げられて、Spring側でうまくハンドリングできません。AOPを利用している場合は注意が必要です。

KotlinからJavaのメソッドを呼び出す場合の引数・返り値

上の節とは逆に、KotlinからJavaのメソッドを呼ぶ場合のハマりどころです。Kotlinにはあらゆる型にnullable typeが存在しますが、Javaには当然そういったものはないので、Kotlinから見るJavaのメソッド引数はすべてplatform type(T!)と呼ばれる特別な型になり、Kotlinの最大の特長のひとつであるNull Safetyが厳格に適用できなくなってしまいます。これを回避するには、Java側で引数に@NotNull@Nullableなどのヒントを与えてあげる必要があります。もっとも、T!TあるいはT?にキャストでき、呼び出し側でハンドリングすることも可能ではありますが、これは厳密なNull Safetyとは言えないでしょう。

返り値の方で問題になるのは、Mockitoを利用しメソッドをモッキングする際に使うMockito.any()などのヘルパー関数が良い例です。any()はnullを返す関数なので、Kotlinでnon-nullに設定されている引数に使おうとするとランタイムエラーになります。そのため私達のチームでは、小さなヘルパークラスを用意しています。

@Suppress("UNCHECKED_CAST")
class KotlinMockitoHelper {
    companion object {
        fun <T> any(): T {
            return Mockito.any() ?: null as T
        }
    }
}

Kotlinでは議論はあるものの、ジェネリックな返り値を持つ関数に対してはnull checkが入らないため、以下のようにnon-nullな型に対してもう一度null checkが必要になってしまいます。以下のコードは、コンパイル時にコンパイラはこのnull checkいらないよ!と言ってきますが、null checkを外してb()を実行するとNullPointerExceptionが発生します。

fun <T> a(): T {
    return null as T
}

fun b() {
    val c: String = a()
    if (c != null) { // Your compiler will display some warnings here!
        print(c.length)
    }
}

しかしたまたまこれがモッキングのようなケースでworkaroundとして作用しているようです。

ちなみに、このany()のworkaroundを含めて、様々な点でMockitoをKotlinに向けに改良することを目的としたライブラリmockito-kotlinもあります。現状我々のテストでは問題になっていないので、今のところ自前のヘルパー関数を使っていますが、自分で管理するのが辛くなったらこちらを利用するのがいいでしょう。

2. KotlinでClova Skill Awardに挑戦してみた話

簡単にいうと、以下のtech stackでClova スキルを開発した話です。

  • 言語: Kotlin
  • Webフレームワーク: Spring Boot 2.x + WebFlux
  • O/Rマッパー: Exposed
  • DB: MySQL

社内向けClova Skill Award?

LINEで提供しているAIアシスタントClovaでは、この度ついに、ユーザーの皆さまが独自にスキル(Extension)を開発し、公開することができるようになりました。プログラムを書いて、好きな言葉を喋らせ、ユーザーの応答をわかりやすく取得することができます。ユーザーの応答はClova Extensions Kit(CEK)プラットフォーム側で推論されるので、複雑に見えるAIスピーカー向けアプリの開発が非常に簡単にできます。

さて、このスキル開発プラットフォームの一般公開に先立ち、プラットフォームの試験も兼ねて、 「社内でClovaスキルで面白いものを作りましょう、総額50万円です!」 というコンテストが開催されました。この章を書いている私(akira)と前章を書いたKagayaは同期の友人であり、普段の業務は全く異なるながらも、同じく同期のりょかちの鶴の一声でチームを組んでこのコンテストに参加することになりました。

今回開発したのは「クックのお料理サポート」というClovaスキルです。冒頭の動画はその動作の様子です。皆さんもスマホで料理のレシピを見ながらキッチンに立った経験があると思いますが、そんなときに役立つスキルです。LINE botと連携して作りたいレシピを登録すると、Clovaがステップバイステップでレシピを読み上げてくれます。もちろん、料理には必須のタイマー機能や「もう一回読んで」などにも対応しています。

本章では、そんなスキルのKotlinでの作り方についてご説明します。

つい先日、公式のKotlin用Clova CEK SDKがリリースされましたですが、今回はいちからAPIを実装しました。他のAPIやWebアプリケーションの開発にも生かせる内容だと思います。また前述した通り、サーバーサイドを書いたことがなくても、ぜひ試してみてください。筆者も普段はAndroidエンジニアです。

(1)テンプレートの作成

Spring InitalizrでKotlin + Gradle + Spring Boot 2.0.4の設定でReactive WebとMySQLを依存関係にいれてダウンロードします。今回はIntelliJにKotlinプラグインをインストールして作業します。

(2)モデルクラスの定義

まず、Clova Extensions Kit(CEK)のドキュメントを読みながら、CEKのサーバー側から送られるRequestおよびこちらから返すResponseのJSONの各項目のモデルクラスを作っていきます。この際にKotlinのdata class とnull-safetyが活躍します。data classを使うことで簡潔にモデルクラスを実装することができ、ドキュメント中でOptionalと書いてあるフィールドは送られてくるときと送られないときがあるのでval xxx: String?とすることで安全にnullableであることを表現できます。各フィールドの詳細はCEKのドキュメントを参考にしてください。ちなみにKotlinはJava同様アノテーションが使えるのでJacksonのアノテーションを活用しています。以下に実際のコードの一例を示します。

// CEKから送られてくるリクエスト
data class ClovaRequest(
    val version: String,
    val session: Session,
    val context: Context,
    @JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "type",
        visible = true
    )
    @JsonSubTypes(
        JsonSubTypes.Type(value = LaunchRequest::class, name = "LaunchRequest"),
        JsonSubTypes.Type(value = IntentRequest::class, name = "IntentRequest"),
        JsonSubTypes.Type(value = SessionEndedRequest::class, name = "SessionEndedRequest")
    )
    val request: RequestInterface
)

// 起動リクエスト、Intent(後述)リクエスト、セッション終了リクエストのいずれか
interface RequestInterface {
    val type: String
}

// 起動リクエスト
data class LaunchRequest(override val type: String) : RequestInterface

// Intentリクエスト
data class IntentRequest(override val type: String, val intent: Intent) : RequestInterface {
    data class Intent(val name: String, val slots: Map<String, Slot>?)
    data class Slot(val name: String, val value: String)
}

// セッション終了リクエスト
data class SessionEndedRequest(override val type: String) : RequestInterface

data class Session(val new: Boolean,
                   val sessionAttributes: Map<String, String>?,
                   val sessionId: String,
                   val user: User)

data class User(val userId: String, val accessToken: String?)

data class Context(@JsonProperty("System") val system: System)
data class System(val application: Application?, val user: User, val device: Device)
data class Application(val applicationId: String)
data class Device(val deviceId: String, val display: Display?)
data class Display(val size: String?, val orientation: String?, val dpi: Int, val contentLayer: ContentLayer?)
data class ContentLayer(val width: Int, val height: Int)

(3)各種インテントを含んだリクエストへの対応

CEKでは、ユーザーの発話の内容が、登録したどの例文に近いか解析され、Intentという形で私達のサーバーにリクエストとしてとどきます。例えば、今回の場合は、

  • 「5番のレシピを始めて」に対応する、n番のレシピで料理をするかを表すRecipeSelectionIntentや、「m分はかって」に対応するTimerIntentを定義しました。これら対応する必要のあるIntentをKotlinのsealed classを使って定義することで、わかりやすく簡潔なコードを書くことができます。以下に実際のコードを示します。
// Intent定義
sealed class OryoriInternalIntent {
    data class RecipeSelectionIntent(val recipeId: String): OryoriInternalIntent() // 「n番のレシピをはじめて」「5番をつくりたい」
    object PreparedIntent : OryoriInternalIntent() // 「料理の準備できたよ」
    data class TimerIntent(val min: String) : OryoriInternalIntent() // 「5分はかって」「タイマー5分」
    object RepeatIntent : OryoriInternalIntent() // 「もう一回言って」「もっかい読んで」 
    object HowToIntent : OryoriInternalIntent() // 「ヘルプ」「使い方を教えて」
    object UnknownIntent: OryoriInternalIntent() // どのIntentにもマッチしない場合
}

// ClovaのIntentから上記のIntentに変換```
@Component
class OryoriIntentMapper {
    fun fromIntent(intent: IntentRequest.Intent): OryoriInternalIntent {
        return when (intent.name) {
            "RecipeSelectionIntent" ->
                intent.toInternalIntentWithFirstSlot { OryoriInternalIntent.RecipeSelectionIntent(it) }
            "PreparedIntent" -> OryoriInternalIntent.PreparedIntent 
            ...
        }
    }
}

それでは実際にHTTPリクエストを受け、応答を返す部分を書いていきましょう。
まず、ルータを定義します。classに@Configurationアノテーションをつけて宣言し、routerを返すメソッドを@Beanとして宣言することで、SpringのDIフレームワークが自動的に検出(Component Scan)し、このクラス及びメソッドの戻り値を使用してくれます。この場合だと"/"へのJSONのPOSTが来たら、cloveHandlerhandleメソッドを呼び出しています。またclovaHandlerはコンストラクタ引数で渡されていますが、後述するように@Componentアノテーションが付与されているため自動的に注入されます。

@Configuration
class Router(private val clovaHandler: ClovaHandler) {

    @Bean
    fun apiRouter() = router {
        accept(MediaType.APPLICATION_JSON_UTF8).nest {
            POST("/", clovaHandler::handle)
        }
    }
}

次に、上述のRouterクラスに登場した、実際のリクエストを受けるクラスClovaHandlerを定義します。
SpringのWebfluxを利用することにより、Request→Intent解釈→Responseの流れを宣言的に書くことができます。WebfluxはReactive Streamsの一種の実装であるReactorをベースにしています。RxJavaなどを利用したことがあれば、見ただけでなんとなくわかっていただけるかと思います。

@Component
class ClovaHandler (...) {
    fun handle(request: ServerRequest): Mono<ServerResponse> {
            return request.bodyToMono(ClovaRequest::class.java) // ClovaRequestクラスにデシリアライズして受け取る
                .doOnNext {
                    logger.info("{}", it) // debug用のロギング
                }
                .map {
                    // itはClovaRequest
                    // whenとisを組み合わせられる
                    when (it.request) {
                        is LaunchRequest -> launchRequestHandler.handle(it.session.user.userId) // スキル起動時のリクエスト
                        is SessionEndedRequest -> SpeechesSession(SpeechInfo("またお料理するときは呼んでくださいね。"))
                        is IntentRequest -> {
                            val oryoriIntent = oryoriIntentMapper.fromIntent(it.request.intent)
                            assistant.assist(oryoriIntent, it.session.user.userId)
                        }
                        else -> throw RuntimeException("Unknown request type is given")
                    }
                }
                .map {
                    // レスポンスとして、Intentをもとに、Clovaに何を喋らせたいかを返す
                    val outputSpeech = OutputSpeech(SpeechType.SPEECH_LIST, it.speeches)
                    ClovaResponse(shouldEndSession = it.endsSession, outputSpeech = outputSpeech) 
                }
                .flatMap {
                    // ClovaResponseオブジェクトをJSONにシリアライズ
                    ServerResponse.ok() 
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(Mono.just(it), ClovaResponse::class.java)
                }
    }
}

(4) Exposedの利用

ユーザーやレシピの保存にはMySQLを利用していますが、O/Rマッパーとして、せっかくなので100%Kotlinで書かれたExposedを利用しました。ExposedはJetBrainsが開発した軽量なSQLライブラリです。導入にはGradleの依存関係に以下を追加します。

compile 'org.jetbrains.exposed:exposed:0.10.2' // 開発時のバージョン
compile 'org.jetbrains.exposed:spring-transaction:0.10.2'

Exposedでは、テーブルのスキーマ定義と作成を以下のようにKotlinで書くことができます。

object Users : Table() {
    val id = varchar("id", 255).primaryKey()
    val recipeId = (integer("recipe_id") references Recipes.id).nullable()
    val step = integer("step")
    val createdAt = datetime("created_at")
    val updatedAt = datetime("updated_at")
    val status = varchar("status", 512)
}

create(Users)

また、例えばidをもとにユーザーを探すコードは以下のように書けます。

fun findUser(userId: String): User? = Users.select { Users.id eq userId }
    .limit(1)
    .map { it.toUser() } // SQLの実行結果はResultRowとして返ってくるので、変換する
    .firstOrNull()
}
private fun ResultRow.toUser() = Users.rowToUser(this)

// DAO定義
data class User(val id: String, val recipeId: Int?, val step: Int, val status: String)

また、Exposedを用いてSpringの@Transacationalアノテーションによるトランザクション管理を有効にするために以下のConfigurationを定義します。なにをやっているかというと、Exposedの提供するSpringTransactionManagerを、@Transactionalによるトランザクション管理で用いるPlatformTransactionManagerに指定しています。これによって、Exposedのメソッドを使いながらも、@Transactionalを付与したメソッド単位で、トランザクションの開始・コミット・(例外が発生したら)ロールバックができるようになります。

import org.jetbrains.exposed.spring.SpringTransactionManager
import ...
@Configuration
@EnableTransactionManagement
class TransactionConfiguration(val dataSource: DataSource): TransactionManagementConfigurer {
    @Bean
    override fun annotationDrivenTransactionManager(): PlatformTransactionManager =
        SpringTransactionManager(dataSource)
}

Exposedを用いた感想としては、テーブルの定義・作成からCRUDを始めとして、JOINなどのSQLもKotlinらしく書けて、もちろん型推論もきくのでだいぶ気持ちがいいですが、Exposedで各SQLはどう書くのかを覚えなければならないのが慣れないうちはつらかったです。

弊社ではSQLをそのまま書けるMyBatisを利用しているプロジェクトが多いのですが、MyBatisもKotlinで使うことができます。さらにall-openとno-arg compiler pluginを利用することで、data classもDAOに使うことが出来ます。

data class User(val id: Int? = null, name: String)
val user = User(name = "foo")
insert(user)
user.id != null  // true, DBで採番されたidが入る!

非常に便利ですが、本来のdata classとは異なった挙動になるため注意が必要です。

まとめ

簡単ではありますが、Kotlin+Spring Boot 2によるClovaスキル開発を紹介しました。ぜひ皆さんもつくってみてください。

大まとめ

Kotlin楽しい!!みなさんも自由研究がてら趣味アプリをKotlinで書いたり、LINE BOOT AWARDS 2018にKotlinで参加してみたり、業務アプリでも小さな部分からKotlinに置換してみたり、いろいろ試してみてくださいね!

明日の記事は弊社京都開発室所属のsugyan(@sugyan)による「line-bot-sdk-go での Flex Messageを扱うための内部の話」です!お楽しみに。