はじめまして。システム部の岡崎です。
現在はCTOオフィスでビックデータのデータ集めに関する基盤整備や、アプリケーションの開発を担当しています。
タイトルにある通り、今日はServer side KotlinでAPIを開発した話について、そして自分が実際にKotlinを使っていて便利だと感じたことや、発見したことについて説明していこうと思います。
Kotlinを使うことになった経緯
社内利用のAPIを作ることになり、 spring-boot + myBats + Kotlin を提案したところ見事採用されました。
なぜKotlinを使ってみようと思ったかというと。
そもそもWeb APIを作成することになった際、下記3つの選択肢がありました。
- Cake PHP
- Java Spring-boot
- Kotlin
以前 Spring-boot+myBatis+Java でバッチの開発をしたときに、Springが便利!なことに気づき今回もそれに乗っかって行こうと考えたのですが…。
「一度Kotlinを使うとJavaには戻れない・・・」といった声を前々から(ネット上で)目にしていたこともあり、どうせなら、モダンと呼ばれるKotlinに挑戦してみたい!NullPointerException撲滅したい!と思ってKotlinの使用を提案してみた、というのがキッカケです。
もちろん、Kotlinという、我々GMOリサーチシステム部にとって新しい言語を採用するにあたって乗り越えるべきハードルも多々ありました。
- 使用する言語が増えると、その分メンテナンスコストが増える
- 社内エンジニアに対する教育コストがかかる
- Kotlinという言語の将来性の検討(サポート終了の可能性等)
- Kotlinでうまくいかなかった場合初めからJavaで書き直す必要がある・・・等
これらの課題に対してどのように向き合うべきか社内で検討した結果、
『メンテ、教育コストがかかるのは仕方がないので割り切る。導入すると決めた自分たちがある程度の苦労を要するのは覚悟の上!頑張ることと引き換えにチャレンジできる権利を手に入れるんだ!!!』
という若干スポ根漫画のような結論に至りました(だって、それでも挑戦したかったんだ…)。
とはいえ現実問題として、Kotlinという言語が今後廃れてしまっては困ります。
この問題に関しても、Androidに選定されている&サーバーサイドでも使われる機会が増えてきていたり、JetBrainsによる開発であったりということから、ある程度信頼性の高い(これから更に流行っていくであろう)言語であると判断しました。
また、実際にKotlinで書いてみてうまく行かなかったらどうするかという問題について。
もちろん最初からJavaで書き直すことになりますが、ダメな場合は割と早い段階でわかるだろうと考えていた、かつ、KotlinからJavaはIntelliJでソースコードを一括置換することができる!ということが後押しとなり、ここのハードルも乗り越えることができました。
ということで、長くなってしまいましたがここまでが今回Kotlinを使ってAPI開発を行った経緯になります。
自分は今回初 Kotlin であったため、実際の開発の中で「これ便利だな」と思ったところや、ハマった末に解決した事象などを下記に書いていきます。
Kotlinという言語について
簡単にではありますが、Kotlinという言語について軽く説明を入れておきます。
* Kotlin とは
Android Native Application の標準開発言語。コンパイル後に出力されるコードはJavaバイトコードも出力できるので、サーバサイドでJavaVM上で動作させることができる。
* Spring boot(Sprong framework) とは
Java界隈では非常に有名な アプリケーションフレームワーク。WEB関連やら、DIやら、ORマッピングやら様々な機能、モジュールが存在する
* myBatisとは
* これもJava界隈では非常に有名な O/R マッパー
Kotlin入門(感想と便利Tips)
それでは、まずはKotlinを実際に使ってみて便利だと思ったことや、分かったことご紹介していきます。
1. val と var
- 明示的に変数の書き換え「可 (var)」「不可 (val)」を宣言したい
- できる限り val(書き換え不可)で宣言し、どうしても必要なときだけ var にする
- ちなみにval で宣言した変数に誤って再代入しようとした場合、コンパイラでエラーにしてくれるのでたのもしい
1 2 |
val value_a = "TEST" // String の宣言不要 val value_b = arrayOf(100, 200) // 自動的に Array<Int> になる |
2. 型推論
- 冗長に型宣言をする必要がない(これがないだけでもソース書きの快適さが増す!)
1 2 |
val value_a = "TEST" // String の宣言不要 val value_b = arrayOf(100, 200) // 自動的に Array<Int> になる |
3. nullable
- 基本的にNullの代入は許容されない
- NullPointerException 撲滅!(したい。。)
1 2 |
val value_a: Sgtring = null // コンパイルエラー val value_b: String? = null // '?'をつけて明示的にNullを許容すると OK |
4. もし Null だったら初期値代入の書き方が簡潔にできる
- myBatis を使っていると Nullを許容していないのにNullになってしまう場合がある
- 下記のような書き方で 簡潔に記述できる
1 2 |
val columnName = TestMapper.getCol(999) ?: "default column name" // エルビス演算子を使う 左式がNullだったら、右式が評価される |
5. constructor の宣言と property の宣言を同時に書ける(data class)
- 例えば myBatis の Domain class の宣言が簡潔にできる
1 2 3 4 5 6 |
@Data data class TestDomain( val id: Int, val name: String, val age: Int ) |
- equals メソッドを自動生成してくれるところも地味に便利
6. 定数を定義する際記述がちょっと違う
- 定数はこんな感じで定義
1 2 3 4 5 |
class TestClass { companion object { const val DEFAULT_ITEM = "default item" } } |
- これで static final String などで定義していた定数を表現可能
Spring + Kotlin編
続いてSpring + Kotlin 編
1. Autowired するとき
- Autowired でインスタンスを生成したい場合、初期値を設定できないため 普通 var で定義できない。なので、以下のような定義をする必要あり
1 2 |
@Autowired lateinit var testMapper: TestMapper |
2. log4j で Logを出力する
- java のときは @sl4j annotation で済んでいたのだが、今回は使えない。ので、以下の方法でlogを出力を実装した
- logger.kt を定義(importできるどこかにおいておく)
1 2 3 4 5 6 7 8 |
package jp.gmoresearch.testproject import org.slf4j.Logger import org.slf4j.LoggerFactory inline fun <reified T> T.logger(): Logger { return LoggerFactory.getLogger(T::class.java) } |
- logを使用したいclassで以下で利用する
1 2 3 4 5 6 7 8 9 10 |
import jp.gmoresearch.testproject.logger @Component class TestComponent { val logger = logger() fun testMethod() { logger.error("Error Message") } } |
myBatis + Kotlin 編
さらにmyBatis + Kotlin 編
Java でmyBatis を使ったときとここが違うなーと思ったところです。
1. myBatis + Kotlin で Nullable が無視される問題
- myBatis で落とし穴
- Select結果が1件のなにかである場合のMapperの戻り値で、検索結果に結果が0件であった場合、たとえNullable宣言していたとしても、Nullが返る
例えば
Mapper
1 2 3 4 5 6 7 |
// Test Mapper @Mapper interface TestMapper { @Select("Select test_col from test_table where id = ${' }{id}") fun getCol(@Param("id") id: Int): String } |
Service
1 2 3 4 5 6 7 8 9 10 11 |
@Component class TestService { @Autowired lateinit var testMapper: TestMapper fun getCol() { val col = testMapper.getCol(999) // ここで検索結果が0件だった場合 Null が返る!! print(col) } } |
なので、場合によっては下記のように宣言して Nullが帰ってくるかもしれないことを考える必要あり。
1 2 3 4 5 6 7 |
// Test Mapper @Mapper interface TestMapper { @Select("Select test_col from test_table where id = ${' }{id}") fun getCol(@Param("id") id: Int): String? // Null許容 } |
2. プレイススホルダの書き方が変
- Kotlinではすでに文字列のプレイスホルダが存在するため、Mapper などで @Select Annotation のプレイスホルダを普通に書けない。なので特殊な書き方をする必要あり。
だめパターン
1 |
@Select("Select * from test_table where id = ${id}") // 変数idを展開しようとする |
OKパターン
Select Annotation に ‘${id}’ という文字列を渡す必要があるため下記のように記述する
1 2 |
@Select("Select * from test_table where id = ${' }{id}") |
3. Domain class を data class で宣言したい。
- 単純に data class として宣言しただけだと Mapperのメソッドを実行した時点でエラーになる。
- myBatis は Domain class のインスタンスを作るときに 引数無コンストラクタを呼ぶためそこでエラーとなる。
- ただし、data class そのままでは引数なしのコンストラクタを許容してくれない
- 解決方法は以下(gradle を使っていることを前提として説明する)
gradle.build.kts の plugins へ以下を追加する
1 2 3 4 5 |
plugins { // id("org.jetbrains.kotlin.plugin.noarg") version (kotlin のバージョン) // } |
domain class へ @Data annotation を追加して data class を宣言する
1 2 |
@Data data class TestDomain(val id: Int, name: String) |
4. マルチスキーマを定義する
WEB検索してもあまり参考例が出てこないので、複数のスキーマを使う際のやり方を記載してみる。
1.ディレクトリ構成
1 2 3 4 5 6 7 8 9 10 11 12 |
src/ └ jp/ └ gmoresearch/ └ testproject/ ├ db/ │ └ config/ <-- スキーマの定義クラス設置するディレクトリ ├ rdb/ <-- スキーマ語問サブディレクトリ │ ├ domain/ │ └ mapper/ └ redshift/ <-- スキーマ毎にサブディレクトリ ├ domain/ └ mapper/ |
2.configration クラスを定義する
スキーマの数だけこのclassを作る
それぞれ内容はほぼ同じで @Promary annotation だけ 1つの configration class 追加する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
package jp.gmoresearch.testproject.db.config // snip... @Configurable @MapperScan(basePackages = [RdbDbConfig.BASE_PACKAGES], sqlSessionTemplateRef = "rdbSqlSessionTemplate") @ConfigurationProperties(prefix = "spring.rdb") class RdbDbConfig { companion object { const val BASE_PACKAGES = "jp.gmoresearch.testproject.db.rdb" const val MAPPER_XML_PATH = "jp/gmoresearch/testproject/db/rdb/mapper/*.xml" } var url: String = "" var username: String = "" var password: String = "" var maximumPoolSize: String = "10" var driverClassName: String = "" @Primary // Promary にしたいスキーマにだけ @Primary annotation を付加する @Bean(name = ["rdbDataSource"]) fun dataSource(): DataSource { val dataSource = HikariDataSource() dataSource.jdbcUrl = url dataSource.username = username dataSource.password = password dataSource.maximumPoolSize = maximumPoolSize.toInt() dataSource.driverClassName = driverClassName return dataSource } @Primary // Promary にしたいスキーマにだけ @Primary annotation を付加する @Bean(name = ["rdbSqlSessionFactory"]) fun sqlSessionFactory(@Qualifier("rdbDataSource") primaryDataSource: DataSource): SqlSessionFactory? { val bean = SqlSessionFactoryBean() bean.setDataSource(primaryDataSource) val resolver = ResourcePatternUtils.getResourcePatternResolver(DefaultResourceLoader()) bean.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml")) bean.setMapperLocations(*PathMatchingResourcePatternResolver().getResources(MAPPER_XML_PATH)) return bean.getObject() } @Primary // Promary にしたいスキーマにだけ @Primary annotation を付加する @Bean(name = ["rdbSqlSessionTemplate"]) fun sqlSessionTemplate(@Qualifier("rdbSqlSessionFactory") sqlSessionFactory: SqlSessionFactory): SqlSessionTemplate{ return SqlSessionTemplate(sqlSessionFactory) } } |
3.Application class で読み込む
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package jp.gmoresearch.testproject // snip... @SpringBootApplication @Import(RdbDbConfig::class, RedshiftDbConfig::class) // スキーマ毎の Configration class を読み込む open class TestProjectApplication { companion object { @Generated @JvmStatic fun main(args: Array<String>) { runApplication<TestProjectApplication>(*args) } } } |
4.これで各スキーマディレクトリ下に作られたMapperは所定のスキーマに接続されて動作するようになる
Kotlinを実際に使ってみた感想
今回自分が初めてKotlinを使ってみて、やはり「とても書きやすい!」と感じました。
冗長な宣言などが必要なく、またIntelliJの補完機能もあり、思考をコードに落とすのがこれまでのどの言語より気持ちよかったです(まさにモダン!)。
また、いざとなったらJavaのライブラリをそのまま使える点も安心して使える要因の一助になりました。
これからもっとKotlin用のライブラリが充実してくれば、myBatisのときのような Null安全が崩れる状態も起こりづらくなるのかなと。
それまでは、ある程度は気配りが必要になるのでないかと思います(にしても気を使う部分はかなり減っているステキ)。
*
GMOリサーチでは、WEBエンジニア(サーバーサイド、インフラ、フロントエンド)を随時募集しております。
興味のある方は、ぜひこちらからご応募ください!
詳しい募集要項など採用情報はこちら