Help us understand the problem. What is going on with this article?

KotlinとSpring 5(Boot 2.0 + WebFlux + Reactive MongoDB)で作るWebAPI

More than 1 year has passed since last update.

はじめに

Spring 5がリリースされてからしばらく経ちますが、目玉機能を使ってちょっとしたWebAPI(?)を作ってみましたのでご紹介します。
なお、今回採用したテクノロジーは下記の通りです。

  • プログラミング言語
    • Kotlin 1.1
  • フレームワーク
    • Spring Boot 2.0 M7
    • Spring 5.0
      • WebFlux
      • Spring Data Reactive MongoDB
  • データベース
    • MongoDB 3.6
  • ビルド・ツール
    • Java Development Kit 9.0
    • Gradle 4.4
  • 統合開発環境
    • IntelliJ IDEA Ultimate 2017.3

本記事はかなりの長文になっていますので、コードのイメージだけつかみたい方は、GitHubにあるサンプル・コードを参照してください。

Spring Initializrを利用してプロジェクトを作成する

Spring Initializrは、Spring Bootアプリケーションのひな型を生成してくれるWebサービスです。
Webブラウザーでサイトに直接アクセスして、ひな型のファイルをダウンロードしてもよいのですが、今回はIntelliJ IDEAを利用して作成してみたいと思います。

Step 1. [Create New Project]を選択します。

01_Welcome to IntelliJ IDEA.png

Step 2. プロジェクトの種類らか[Spring Initializr]を選択して、[Next]ボタンをクリックします。ちなみに、IntelliJ IDEA Community版ではこの項目をサポートしていないので、Ultimate版を利用しましょう。

02_New Project.png

Step 3. プロジェクトの基本情報の設定画面です。[Type]に「Gradle Project」を、[Language]に「Kotlin」を選択しましょう。必要であれば[Group]や[Artifact]などの項目を変更しても構いません。(ただし、以降の記述はデフォルトを指定したものとして説明します)

03_Project Metadata.png

Step 4. フレームワークの選択画面です。Spring 5の新機能を利用する場合は、Spring Boot 2.0が必要なので、まずは画面上部の[Spring Boot]から「2.0.0 M7」を選択しましょう。ちなみにSpring Boot 2.0の正式版は、本記事執筆時点で2018年2月頃にリリースされる予定になっています。
下記の2つにチェックをつけてから[Next]ボタンをクリックします。

  • [Web]-[Reactive Web]
  • [NoSQL]-[Reactive MongoDB]

04_Dependencies.png

Step 5. 保存先を指定します。今回はデフォルトのまま[Finish]ボタンをクリックします。

05_New Project.png

Step 6. 最後にGradleの設定画面が表示されます。[Use auto-import]にチェックを付けて[OK]ボタンをクリックします。

06_Import Module from Gradle.png

Gradleのバージョンを変更する

今回はJava 9を利用していますが、Spring Bootが生成するプロジェクトではJava 9に対応する前のGradleが指定されています。このままではビルド・エラーになったり、IntellJ IDEAがプロジェクトを正しく認識しなかったりするので、設定ファイルを書き替えてGradleのバージョン指定を変更します。
詳しくは、別の記事にまとめておりますので、そちらを参照してください。

/demo/gradle/wrapper/gradle-wrapper.properties
#Fri Jul 28 13:37:07 BST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip

※最後の行の"4.2"の部分を4.4に書き替えています。

なお、IntelliJ IDEAを使っている方は、上記の修正を行った後、[CTRL]+[ALT]+[Y](macOSの方は[command]+[option]+[Y])で同期をかけてください。

値の入れ物を作る

プロジェクトのセットアップが終わったので、いよいよプログラムを作っていきましょう。
まずは、値の入れ物となるクラスを作成します。今回は、TODO管理システムを題材に作成したいと思いますので、TodoItemクラスを新規に作成しましょう。

Step 1. パッケージを作成します。/demo/src/main/kotlinの下にある、アプリケーションのベースとなるパッケージ(デフォルトのまま作成した方はcom.example.demo)を選択し、メニューから[File]-[New]-[Package]を選びます。
パッケージ名を入力するダイアログが開くので、entityと入力して[OK]ボタンをクリックしましょう。

Step 2. クラスを作成します。前の手順で作成したentityパッケージを選択し、メニューから[File]-[New]-[Kotlin File/Class]を選びます。
[name]にTodoItemを入力、[Kind]で「Class」を選択して[OK]ボタンをクリックします。

Step 3. 下記の通り記述してクラスを完成させます。

/demo/src/main/kotlin/com/example/demo/entity/TodoItem.kt
package com.example.demo.entity

import java.time.LocalDate

/**
 * TODO項目の値を格納するためのクラス
 */
data class TodoItem(

        /**
         * ID
         */
        val id: String? = null,

        /**
         * 完了状態
         */
        val done: Boolean = false,

        /**
         * 表題
         */
        val subject: String = "無題",

        /**
         * 期日
         */
        val deadline: LocalDate? = null

)

Javaをご存知の方は驚くかもしれませんが、Kotlinではたったこれだけの記述で値を格納するクラスを定義できます。
詳細な説明は他の方の記事や書籍などに譲るとして、ここではポイントのみ簡単に説明いたします。

ポイント1:ボディの省略

Javaでのもっとも簡単なクラスの記述は次のようになります。

TodoItem.java
package com.example.demo.entity;

class TodoItem {
}

クラス名の後ろに{ }を記述し、その中にメンバーを定義していくのですが、Kotlinでは定義するメンバーがない場合には{ }を省略できます。

ポイント2:プライマリー・コンストラクター

クラス名の後ろの( )は、プライマリー・コンストラクターを意味します。すなわち、今回の例ではTodoItemのインスタンス・オブジェクトを生成する際に、iddonesubjectdeadlineの4つの値を渡してオブジェクトを初期化できます。

ポイント3:プライマリー・コンストラクターでのプロパティの宣言

プライマリー・コンストラクターでvalまたはvarキーワードを指定すると、プロパティーを宣言することができます。
Kotlinでのプロパティーは、Javaでいうところのフィールド+getter/setterメソッドの組み合わせとほぼ同義であり、valは読み取り専用プロパティ(フィールド+getter)、varは読み書き両用プロパティ(フィールド+getter+setter)を意味しています。

ポイント4:型の指定

Javaでは識別子の前に型を指定しますが、Kotlinでは識別子の後ろに:区切りで型を指定します。

ポイント5:Null許容型(Nullable型)

Javaでは参照型のすべての変数にnullを代入することが可能ですが、Kotlinでは安全性確保のため、nullを代入できる変数は宣言時に型の後ろに?を明示的に指定しなくてはなりません。
これにより、Nullを許容しない型(NotNull型)の変数にnullを代入しようとしたり、変数がnullのままメンバーにアクセスしようとしたりすると、コンパイル時にエラーではじいてくれるようになります。

ポイント6:引数のデフォルト値

仮引数の宣言時に=でデフォルト値を指定することができます。適切なデフォルト値を指定しておけば、コンストラクターやメソッドの呼び出し時に最小限の値を与えるだけで利用できるようになります。

ポイント7:データ・クラス

Kotlinには、データ・クラスと呼ばれる便利な機能があります。
classキーワードの前にdataと指定すると、値を格納するクラスでよく利用される下記の機能が自動生成されます。

  • equalsおよびhashCodeメソッドのオーバーライド
  • toStringメソッドのオーバーライド
  • 分割代入のサポート
  • copy関数(一部の値を変更して新たなインスタンス・オブジェクトを生成する機能)

データベース・アクセス用のコンポーネントを作る

Kotlinのクラスの説明が思った以上に長くなってしまいましたが、気を取り直して次の説明に移りましょう。今回は、Reactive MongoDBというフレームワークを用いてデータベース・アクセスを行います。
Reactive MongoDBには、Reactive MongoDB repositoriesという機能が提供されています。簡単に言ってしまうと、インターフェイスを定義するだけで基本的なデータ操作(Create、Read、Update、Delete)を実装したオブジェクトが自動生成されます。
それでは、早速作ってみましょう。

Step 1. パッケージを作成します。/demo/src/main/kotlinの下にある、アプリケーションのベースとなるパッケージ(デフォルトのまま作成した方はcom.example.demo)を選択し、メニューから[File]-[New]-[Package]を選びます。
パッケージ名を入力するダイアログが開くので、repositoriesと入力して[OK]ボタンをクリックしましょう。

Step 2. インターフェイスを作成します。前の手順で作成したrepositoriesパッケージを選択し、メニューから[File]-[New]-[Kotlin File/Class]を選びます。
[name]にTodoItemRepositoryを入力、[Kind]で「Interface」を選択して[OK]ボタンをクリックします。

Step 3. 下記の通り記述してインターフェイスを完成させます。

/demo/src/main/kotlin/com/example/demo/repositories/TodoItemRepository.kt
package com.example.demo.repositories

import com.example.demo.entity.TodoItem
import org.springframework.data.repository.reactive.ReactiveCrudRepository

/**
 * TODO項目に対するCRUD操作をサポートするリポジトリー・インターフェイス
 */
interface TodoItemRepository: ReactiveCrudRepository<TodoItem, String>

これだけです。

簡単に解説すると、Spring Dataが提供しているインターフェイスReactiveCrudRepositoryを継承して独自インターフェイスを定義します。その際、ジェネリックスを用いて値を格納するクラス(エンティティ・クラス)の型(今回はTodoItemクラス)と、主キーの型(今回はStringクラス)を指定します。
Kotlinでは、クラスやインターフェイスの継承や実装の際、extendsimplementsキーワードを利用せずに、単に:でスーパー・クラスやインターフェイスを指定します。

データベース接続のための設定を行う

次に、データベース接続のための設定を行います。
プロジェクトのひな型を作成した時に、このアプリケーションのメイン・クラスとエントリー・ポイントが生成されています。

/demo/src/main/kotlin/com/example/demo/DemoApplication.kt
package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

ここに定義されているDemoApplicationクラスを下記のように書き替えます。

/demo/src/main/kotlin/com/example/demo/DemoApplication.kt
package com.example.demo

import com.mongodb.reactivestreams.client.MongoClient
import com.mongodb.reactivestreams.client.MongoClients
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration

@SpringBootApplication
class DemoApplication: AbstractReactiveMongoConfiguration() {
    override fun reactiveMongoClient(): MongoClient = MongoClients.create()
    override fun getDatabaseName(): String = "todo"
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

DemoApplicationクラスのスーパー・クラスに、Reactive MongoDBが提供している抽象クラスAbstractReactiveMongoConfigurationを指定します。このクラスはReactive MongoDBの構成を行うためのクラスで、データベース接続オブジェクトを生成するreactiveMongoClientメソッドと、接続先データベースの名前を返すgetDatabaseNameメソッドをオーバーライドする必要があります。
今回は、デフォルトの接続先(localhostで実行されている認証なしのMongoDB)のtodoデータベースを利用するように構成しています。

ちなみにKotlinでは、(インターフェイスでなく)クラスを継承する際に、必ずスーパー・クラスのコンストラクターを呼び出さなくてはなりません。今回の例では、引数無しのコンストラクターを呼び出してスーパー・クラスの初期化を行っています。

また、Javaにおいてメソッド本体は{ }の中に記述しますが、Kotlinでは戻り値の生成が1行の式で記述できる場合、{ }の代わりに=を利用して代入式のように記述できます。今回の例ではどちらも1行の式として表現できるほど簡単なので、どちらも式の形式で記述しています。

ハンドラー・クラスの作成

いよいよWebFluxを利用してWebAPIを作っていきます。まずは、個別のリクエストを処理するハンドラー・クラスから作成しましょう。

Step 1. パッケージを作成します。/demo/src/main/kotlinの下にある、アプリケーションのベースとなるパッケージ(デフォルトのまま作成した方はcom.example.demo)を選択し、メニューから[File]-[New]-[Package]を選びます。
パッケージ名を入力するダイアログが開くので、webと入力して[OK]ボタンをクリックしましょう。

Step 2. クラスを作成します。前の手順で作成したwebパッケージを選択し、メニューから[File]-[New]-[Kotlin File/Class]を選びます。
[name]にTodoHandlerを入力、[Kind]で「Class」を選択して[OK]ボタンをクリックします。

Step 3. 下記の通り記述してクラスを完成させます。

/demo/src/main/kotlin/com/example/demo/web/TodoHandler.kt
package com.example.demo.web

import com.example.demo.repositories.TodoItemRepository
import org.springframework.http.MediaType.APPLICATION_JSON_UTF8
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.ServerResponse.ok
import org.springframework.web.reactive.function.server.body
import reactor.core.publisher.Mono

/**
 * TODO項目のハンドラー
 */
class TodoHandler(private val todoItemRepository: TodoItemRepository) {

    /**
     * TODO項目一覧の取得
     */
    fun findAll(request: ServerRequest): Mono<ServerResponse> =
            ok().contentType(APPLICATION_JSON_UTF8)
                    .body(todoItemRepository.findAll())

}

このクラスのプライマリー・コンストラクターでは、先ほど作成したTodoItemRepositoryインターフェイス型のオブジェクトを受け取るように記述しています。このオブジェクトは、Springによって自動的に生成されコンストラクターの引数として渡される(コンストラクター・インジェクションと呼ぶ)ので、私たち開発者はインスタンス・オブジェクトの生成については一切気にしなくて構いません。
なお、プライマリー・コンストラクターの宣言で指定されているprivateキーワードですが、引数として渡されたTodoItemRepositoryのインスタンス・オブジェクトを内部的に格納しているだけで、外部には公開しないことを意味しています。Javaでいうところのプライベート・フィールドを宣言しているのと似たようなものと考えてよいかもしれません。

このプログラムでもう一か所(と、いうよりこちらが主役ですが)注目して欲しい箇所があります。それはfindAllメソッドです。
リクエストを受け取り、レスポンスを返すメソッドですが、引数および戻り値にServerRequestServerResponse型がそれぞれ指定されています。名前が示すとおり、それぞれリクエストとレスポンスに対応しており、これらを利用してリクエスト処理を行います。
ただ、戻り値の型がMono<ServerResponse>となっているのが気になりますよね。これがまさにリアクティブ・プログラミングと関係する部分で、レスポンスが非同期に返されることを意味しています。
Spring 5で導入されたReactive Stack(WebFluxやReactive MongoDBなどのリアクティブ・プログラミングに基づくフレームワーク一式)は、Reactorというライブラリーを利用しています。このライブラリーで定義されているイベント・ストリームを表すオブジェクトにはMonoFluxがあります。

  • Mono:0または1個のイベントを生成するオブジェクト
  • Flux:0個以上、複数のイベントを生成するオブジェクト

なお、今回のプログラムでは直接出てきていませんが、TodoItemRepositoryfindAllメソッドは、戻り値としてFluxを返します。(すなわち、検索結果の1件1件が非同期のイベントとして扱われます)

このクラスの説明の締めくくりとしてfindAllメソッドの中身について解説します。
このメソッドは、データベースから検索した複数件のデータをステータス・コード200(OK)とともにJSON形式で返す処理を行っています。コードの見た目もほとんどそのままですね。

ルーターの作成

ルーターとは、リクエストに含まれるパスやHTTPメソッド(GET、POST、etc.)などの情報をもとに、適切な処理に振り分ける役目のコンポーネントです。
さっそく作ってみたいと思います。

Step 1. ルーターを定義するためにファイルを作成します。前の手順で作成したwebパッケージを選択し、メニューから[File]-[New]-[Kotlin File/Class]を選びます。
[name]にRouterを入力、[Kind]で「File」を選択して[OK]ボタンをクリックします。

Step 2. 下記の通り記述してクラスを完成させます。

/demo/src/main/kotlin/com/example/demo/web/Router.kt
package com.example.demo.web

import org.springframework.web.reactive.function.server.router

/**
 * ルーター
 */
fun router(todoHandler: TodoHandler) = router {

    /* TODO項目に対するパスの割り当て */
    "/todos".nest {

        /* TODO項目一覧の取得 */
        GET("/", todoHandler::findAll)

    }

}

上記のrouter関数もTodoHandlerと同様に、Springのインジェクションの機能を利用して、引数にTodoHandlerクラスのインスタンス・オブジェクトを受け取るように記述しています。
関数本体の部分では、WebFluxのKotlin DSLを利用して、「/todosのパスに対してGETリクエストが行われた際に、TodoHandlerクラスのfindAllメソッドを実行する」ように指定しています。
一見すると、独自フォーマットで記述されているように見えますが、これでもKotlinの文法に従って記述されています。今回はあまり深くまで説明しませんが、気になる方はKotlinの文法について下記の項目を調べてみてください。

  • 関数呼び出しにおける( )の省略
  • ラムダ式
  • 拡張関数
  • メソッド参照

コンポーネントの登録

いよいよ仕上げに取り掛かります。
TodoItemRepositoryインターフェイスはSpringが自動的に認識して実装クラス(とそのインスタンス・オブジェクト)を動的に生成してくれるので特別な考慮は必要ないのですが、TodoHandlerクラスとrouter関数については、Springに対してコンポーネントとして登録しておかなければなりません。
そこで、アプリケーションの起動ポイントとなるmain関数の中身を下記のとおり修正します。

/demo/src/main/kotlin/com/example/demo/DemoApplication.kt
package com.example.demo

import com.example.demo.web.TodoHandler
import com.example.demo.web.router
import com.mongodb.reactivestreams.client.MongoClient
import com.mongodb.reactivestreams.client.MongoClients
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.support.beans
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration

/**
 * Spring Bootのアプリケーション定義
 */
@SpringBootApplication
class DemoApplication: AbstractReactiveMongoConfiguration() {

    /**
     * リアクティブに対応したMongoDB Clientオブジェクトのプロバイダー
     */
    override fun reactiveMongoClient(): MongoClient = MongoClients.create()

    /**
     * このアプリケーション用のデータベース名
     */
    override fun getDatabaseName(): String = "todo"

}

/**
 * Spring Bootアプリケーションの起動ポイント
 */
fun main(args: Array<String>) {

    runApplication<DemoApplication>(*args) {

        /**
         * Bean定義
         */
        addInitializers(beans {
            bean<TodoHandler>()
            bean {
                router(ref<TodoHandler>())
            }
        })

    }

}

中でも注目していただきたいのがここの部分です。

    runApplication<DemoApplication>(*args) {

        /**
         * Bean定義
         */
        addInitializers(beans {
            bean<TodoHandler>()
            bean {
                router(ref<TodoHandler>())
            }
        })

    }

Spring Bootアプリケーションを起動するrunApplication関数の呼び出しの記述の後に、{ }が追加されています。実は、この{ }はラムダ式であり、runApplication関数を実行した後でなく、呼び出しの際の第2引数として渡されています。
このラムダ式の中ではaddInitializers関数を呼び出して、コンポーネントの登録を行っています。引数のbeans { }はKotlin Bean定義DSLとして記述されており、TodoHandlerクラスの登録と、(独自に作成した方の)router関数をファクトリーとして登録しています。
ちなみに、router関数の呼び出しにはTodoHandlerクラスのインスタンス・オブジェクトが必要ですが、ref関数を利用してSpringから目的のオブジェクトを取得しています。

実行の前に

これでプログラムの方は一通り準備ができました。
後は実行して動作確認を行いたいのですが、その前にMongoDBのデータベースにダミーのデータを用意する必要があります。

Step 1. MongoDBをインストールしていない方は、まずMongoDBをインストールしてください。本記事ではインストール手順などは説明しませんが、おそらく多くの方々が優秀な記事を書いているはずです。
ただ、本記事では「認証を行わない」ことを前提としてプログラムを記述しているので、認証が有効な状態だと実行に失敗いたします。

Step 2. 下記のファイルを用意してください。ダミーデータを登録するスクリプトです。

insert_data.js
const conn = new Mongo()
const db = conn.getDB("todo")
db.todoItem.insert([
    {"done" : false, "subject" : "粗大ごみを出す" },
    {"done" : false, "subject" : "リビングを掃除する", "deadline" : ISODate("2017-12-31T00:00:00Z") },
    {"done" : true, "subject" : "牛乳を買って帰る", "deadline" : ISODate("2017-12-21T00:00:00Z") }
])

Step 3. コマンド・プロンプト(またはmacOSの方はターミナル)で、スクリプト・ファイルが保存されている場所にカレント・ディレクトリーを合わせ、下記のコマンドを実行します。

mongo todo insert_data.js

アプリケーションの実行

いよいよ実行するときが来ました。

Step 1. main関数を実行しましょう。

Step 2. Webブラウザーを起動して、http://localhost:8080/todosにアクセスします。
下記のような画面が出れば成功です!

[{"id":"5a3cbcad5dbcd23a4fc853b7","done":false,"subject":"粗大ごみを出す","deadline":null},{"id":"5a3cbcad5dbcd23a4fc853b8","done":false,"subject":"リビングを掃除する","deadline":"2017-12-31"},{"id":"5a3cbcad5dbcd23a4fc853b9","done":true,"subject":"牛乳を買って帰る","deadline":"2017-12-21"}]

最後に

今回は、Spring 5の新機能とKotlinでちょっとしたWebAPIを作ってみました。かなり簡単に作れることがお分かりいただけたかと思います。
実際のアプリケーションを開発するには、まだまだ知らなくてはならないことはありますが、本記事が少しでも皆さんのお役に立てたら幸いです。
それではよいKotlin & Springライフを!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away