Kotlinではバージョン1.3からKotlin Coroutinesが導入され、外部APIとの通信やAndroidクライアントのサーバー通信など、主にI/Oの部分の処理を非同期で実行するために使用されています。
また、SpringのリアクティブWebスタックであるSpring WebFluxと組み合わせて使うことで、APIレベルでの非同期処理も実現することができます。
本記事では、サーバーサイドアプリケーションのI/O処理として最もよく使われるものの一つであるデータベースへのアクセスを、Spring WebFluxとKotlin Coroutines、O/RマッパーとしてMyBatisを使い非同期処理として実装する例を紹介します。
Spring WebFluxとは?
Spring WebFluxは、Springに5系から追加されたリアクティブWebスタックです。
アプリケーションサーバーとしてもNettyを使用しており、非同期でノンブロッキングな処理を実現することができます。
ControllerとRouter Functions
Spring WebFluxでは、ルーティングの定義方法が2種類用意されています。
まず、通常のSpring Framework(Spring MVCをベースとしたもの)と同様にControllerとして定義する方法です。
@RestController
class ExampleController {
@GetMapping("/hello")
fun execute(): String {
return "Hello WebFlux."
}
}
もう一つはRouter Functionsといい、DSLの形でルーティングを定義する方法です。
@Component
class ExampleRouter {
@Bean
fun routesExample() = router {
GET("/hello") {
ServerResponse.ok().bodyValue("Hello WebFlux.")
}
}
}
router
はKotlinでのルーティング定義のために用意されている関数で、その中でHTTPメソッドとパスを指定して処理を実装していきます。
ServerResponse
にレスポンスの値を設定して返却します。
Ktorのルーティングにも似ていますね。
Kotlin Coroutinesを使う場合のルーティング定義
Kotlin Coroutinesを使う場合のルーティング定義も、Spring WebFluxを使うと実装しやすくなります。
Kotlin Coroutinesの処理やsuspend関数をControllerから呼び出す時、通常のSpring Frameworkでは下記のようにrunBlockingをしてブロックする必要があります。
@RestController
class ExampleController {
@GetMapping("/hello/controller/async")
fun executeAsync(): String = runBlocking {
val text = async {
// テキストの取得処理
// ・・・
}
text.await()
}
}
これはブロックをしないとCoroutinesの処理の終了を待たずにスレッドが終了してしまうためです。
しかし、非同期処理を前提としているSpring WebFluxでは、Controllerをsuspend関数として定義することができます。
@RestController
class ExampleController {
@GetMapping("/hello/async")
suspend fun executeAsync(): String = coroutineScope {
val text = async {
// テキストの取得処理
// ・・・
}
text.await()
}
}
これによりスレッドをブロックせず、完全に非同期で使用することができます。
また、RouterFunctionsを使う場合も、coRouterという関数を使うことで同様にブロックせず呼び出すことができます。
@Component
class ExampleRouter {
@Bean
fun routesAsync() = coRouter {
GET("/hello/async") {
coroutineScope {
val text = async {
// テキストの取得処理
// ・・・
}
ServerResponse.ok().bodyValueAndAwait(text.await())
}
}
}
}
Spring Frameworkは本来Javaのフレームワークですが、5.0からの正式にKotlinサポートをしていることもあり、Spring WebFluxでもKotlinでの実装に最適化するための関数などが用意されています。
サンプルの構造の概要
大まかに以下の役割のクラス、インターフェースが存在します。
- Mapper - MyBatisでのデータベースアクセス処理(Generatorでの自動生成)
- Service - ビジネスロジック層(本記事のサンプルではMapperを非同期処理で呼び出す処理を書くのみ)
- Router - ルーティングとServiceの実行、パラメータの受け渡し
サンプルコードのため、最低限の階層だけのシンプルな構成になっています。
ルーティングには前述のRouter Functionsを使用します。
データベースの構築
まずはデータベースを用意します。
DockerでMySQLを起動
※この手順は既にローカルにMySQLが起動されている場合は、必要ありません
Dockerを使用して、MySQLのイメージから作成します。
もしローカル環境にDockerが入っていない場合は、公式ページからダウンロードしてインストールしてください。
Dockerを起動したら、ターミナルから次のコマンドを実行しMySQLのコンテナを起動します。
docker container run --rm -d -e MYSQL_ROOT_PASSWORD=mysql -p 3306:3306 --name mysql mysql
以下のコマンドでログインできれば成功です。
mysql -h 127.0.0.1 --port 13306 -uroot -pmysql
データベース、テーブルの作成
次に、データベースとテーブルを作成します。
データベース名は任意の名前で問題ありませんが、本記事のサンプルでは以下のように作成します。
create database webflux_example;
CREATE TABLE food (
id int NOT NULL,
name varchar(16) NOT NULL,
price int NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE drink (
id int NOT NULL,
name varchar(16) NOT NULL,
price int NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
food、drinkというそれぞれIDと名前、価格の情報を持ったテーブルです。
そしてテストデータを下記のクエリで登録します。
INSERT INTO food values(1, "ハンバーガー", 100), (2, "チーズバーガー", 120), (3, "ポテト", 110);
INSERT INTO drink values(1, "コーラ", 100), (2, "サイダー", 110), (3, "オレンジジュース", 120);
これで環境の準備は完了です。
アプリケーションの実装
それではここからプロジェクトを作成し、実装を進めていきます。
Spring InitializrでWebFluxのKotlinプロジェクトを作成
プロジェクトはSpring Initializrを使用して作成します。
GroupやArtifactなどの名前は任意ですが、サンプルは下記の設定で作成しています。
Dependenciesに以下の2つを追加しています。
- Spring Reactive Web
- MyBatis Framework
Spring ReactiveがSpring WebFluxを使うための依存関係になります。
build.gradle.ktsでは、下記の箇所が追加した2つの依存関係に当たる部分です。
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3")
build.gradle.ktsに必要な依存関係、タスクを追加
プロジェクト生成時に追加したもの以外にも、主にMyBatisに関するところでいくつかの依存関係やタスクの追加が必要です。
全体像としては下記になります。
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.3.3.RELEASE"
id("io.spring.dependency-management") version "1.0.10.RELEASE"
id("com.arenagod.gradle.MybatisGenerator") version "1.4"
kotlin("jvm") version "1.3.72"
kotlin("plugin.spring") version "1.3.72"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
apply(plugin = "com.arenagod.gradle.MybatisGenerator")
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.3")
implementation("org.mybatis.dynamic-sql:mybatis-dynamic-sql:1.1.4")
implementation("mysql:mysql-connector-java:8.0.20")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation("io.projectreactor:reactor-test")
mybatisGenerator("org.mybatis.generator:mybatis-generator-core:1.4.0")
}
mybatisGenerator {
verbose = true
configFile = "${projectDir}/src/main/resources/generatorConfig.xml"
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
MySQLコネクタ
今回はデータベースへの接続でMySQLを使用するため、MySQLのコネクタを追加します。
implementation("mysql:mysql-connector-java:8.0.20")
MyBatis Dynamic SQL
MyBatis Dynamic SQLというライブラリを追加しています。
implementation("org.mybatis.dynamic-sql:mybatis-dynamic-sql:1.1.4")
これはクエリをコード上で動的にして実行することのできるMyBatisのライブラリで、Kotlinもサポートされています。
例としては、下記のようにKotlinのDSLで書くことができるようになります(公式ページから引用)。
val rows = mapper.select {
where(id, isLessThan(100))
or (employed, isTrue()) {
and (occupation, isEqualTo("Developer"))
}
orderBy(id)
}
MyBatis Generator
MyBatis Generatorは、クエリを実行するMapperファイルや、データのやり取りをするデータオブジェクトのファイルを、テーブル構造から生成することのできるツールです。
まず、依存関係にmybatis-generator-coreを追加します。
mybatisGenerator("org.mybatis.generator:mybatis-generator-core:1.4.0")
そして、コード生成のタスクとして以下を追加します。
mybatisGenerator {
configFile = "${projectDir}/src/main/resources/generatorConfig.xml"
}
configFileで設定しているxmlファイルに、コード生成時に使用するデータベースなどの情報を記述し、それを読み込んで実行されます。
設定の記述については後述します。
MyBatis Generatorでデータベースアクセスのコード生成
次に、Gradleで作成したMyBatis Generatorのタスクを使用して、コードを生成します。
設定ファイルの作成
src/main/resources
配下に以下の内容でgeneratorConfig.xml(名前は任意)を作成してください。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD
MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration>
<!-- mysql-connector-javaのパスは個人の環境に合わせる -->
<classPathEntry
location="/Users/xxxx/.gradle/caches/modules-2/files-2.1/mysql/mysql-connector-java/8.0.20/d8d388c71c723570662a45si2344f84141921280/mysql-connector-java-8.0.20.jar"/>
<context id="MySQLTables" targetRuntime="MyBatis3Kotlin">
<plugin type="org.mybatis.generator.plugins.MapperAnnotationPlugin"/>
<commentGenerator>
<property name="suppressDate" value="true"/>
</commentGenerator>
<jdbcConnection
driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/webflux_example"
userId="root"
password="mysql">
<property name="nullCatalogMeansCurrent" value="true"/>
</jdbcConnection>
<javaModelGenerator
targetPackage="com.example.demo.database.record"
targetProject="src/main/kotlin">
</javaModelGenerator>
<javaClientGenerator
targetPackage="com.example.demo.database.mapper"
targetProject="src/main/kotlin">
</javaClientGenerator>
<table tableName="%" />
</context>
</generatorConfiguration>
classPathEntry
のlocation属性でmysql-connector-javaのjarファイルへのパスを指定していますが、こちらは個人の環境に合わせて設定してください。
前述のGradleの依存関係に追加したため、.gradleディレクトリ配下にファイルがダウンロードされています。
また、.gradleディレクトリは通常ユーザーのホーム配下に作られます。
そしてjdbcConnection
で使用するJDBCドライバのクラス、接続先、IDとパスワードを前述のDockerで立てたMySQLに向けて指定しています。
ここで設定した接続先のテーブル情報を元に、各ファイルを作成します。
対象となるテーブルを指定することもできますが、ここでは<table tableName="%" />
で全テーブルを対象としています。
javaModelGenerator
、javaClientGenerator
で指定しているパッケージは、それぞれ生成されるMapper、データオブジェクトの出力先になるので、任意の場所を指定してください。
Gradleタスクの実行
コード生成のタスクを実行します。
ターミナルでプロジェクト直下に行き下記のコマンドを実行するか、IntelliJ IDEAのGradleビューから Tasks -> other -> mbGenerator を実行してください。
./gradlew mbGenerator
generatorConfig.xmlで指定したパッケージに、以下のようなファイルがテーブルごとに生成されていれば成功です(xxxはテーブル名のパスカルケースです)。
- xxxDynamicSqlSupport - クエリ実行時にカラム指定のパラメータとして使用するフィールドが定義されているオブジェクト
- xxxMapper - テーブルのデータを操作する基本的な関数を持ったインターフェース
- xxxMapperExtensions - xxxMapperに加えてより高度な操作をする拡張関数を持ったインターフェース
- xxxRecord - データのやり取りをするテーブル構造と同じプロパティを持ったデータクラス
各ファイルの詳細は、話の本筋から逸れるため割愛します。
SpringからMyBatisで接続するデータベース関連の設定
SpringからMyBatisを使用する際に、接続するデータベースの情報などの設定です。
src/main/resources配下にapplication.ymlというファイルを作成し、下記の内容を記述してください。
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/webflux_example?characterEncoding=utf8
username: root
password: mysql
driverClassName: com.mysql.jdbc.Driver
spring -> database の下に、接続先のデータベース情報、ユーザー名、パスワード、ドライバのクラスを指定しています。
Serviceの実装
生成したデータベースアクセスの処理を呼び出す、Serviceクラスを実装します。
レスポンスとして使用するデータオブジェクトの作成
APIのレスポンスとして使用するため、下記のデータクラスを作成します。
data class Menu(val foodList: List<FoodRecord>, val drinkList: List<DrinkRecord>)
foodとdrinkの情報のリストを持ったMenuクラスです。
FoodRecord、DrinkRecordはMyBatis Generatorで生成した、テーブルのデータをやり取りするためのデータクラスです。
※本来データベースとのやり取りのために生成されたクラスをレスポンスにそのまま使うのは設計上好ましくありませんが、今回はサンプルの簡略化のためこの形を取っています
ServiceからMapperを非同期、並列実行
Serviceではfood、drinkそれぞれのテーブルデータを全件取得し、Menuクラスに設定して返却します。
データの全件取得は、以下のように書けます。
val foodList = foodMapper.select { allRows() }
val drinkList = drinkMapper.select { allRows() }
そして今回は非同期での並列実行になるため、Kotlin Coroutinesのasync
を使用して次のように実装します。
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@Service
class MenuService(private val foodMapper: FoodMapper, private val drinkMapper: DrinkMapper) {
suspend fun findAllMenu(): Menu = coroutineScope {
val foodList = async { foodMapper.select { allRows() } }
val drinkList = async { drinkMapper.select { allRows() } }
Menu(foodList.await(), drinkList.await())
}
}
foodMapper、drinkMapperをそれぞれasync
で実行し、await
で待ち合わせた結果をMenuクラスに設定して返却しています。
複数のテーブルへアクセスする場合は、このような形で並列実行することが可能です。
RouterFunctionsの実装
Serviceを呼び出すRouterFunctionsを実装します。
@Component
class MenuRouter(private val menuService: MenuService) {
@Bean
fun routes() = coRouter {
GET("/menu/list") {
ServerResponse.ok().bodyValueAndAwait(menuService.findAllMenu())
}
}
}
coRouter
を使用しています。
処理としてはServiceクラスの関数を実行して、結果のオブジェクトをServceResponseに設定して返却しているだけになります。
動作確認
下記のcurlコマンドを実行してみてください。
curl http://localhost:8080/menu/list
実行結果として、foodとdrink両方の全レコードのデータが入った下記のレスポンスが返ってくれば成功です。
{"foodList":[{"id":1,"name":"ハンバーガー","price":100},{"id":2,"name":"チーズバーガー","price":120},{"id":3,"name":"ポテト","price":110}],"drinkList":[{"id":1,"name":"コーラ","price":100},{"id":2,"name":"サイダー","price":110},{"id":3,"name":"オレンジジュース","price":120}]}
このデータだけではすぐに処理が返ってくるため体感しづらいかもしれませんが、これでデータベースへの非同期I/Oが実装できました。
サーバーサイドでももっとKotlin Coroutinesを
Androidアプリではメインスレッドを止めないために、API通信の実装などもともと非同期でI/Oを実装することも多いため、Kotlin Coroutinesも自然と使われています。
しかし、サーバーサイドではまだまだ使われている事例が少ないように思います。
今回紹介したデータベースアクセスや、マイクロサービス間の通信など外部サーバーへのアクセス時に使用することでより質を上げることができると思っているので、Spring WebFluxと合わせて積極的に活用していきたいと思います。