はじめに
少し前に見つけた「国内の公開されているサーバーサイド Kotlin 採用事例まとめ」の記事を見てから、
Kotlin でやるサーバーサイド開発にだんだん興味が出てきているので、実際に何か作ってみようと思います。
今回は、軽量な Spark Framework と組み合わせて、簡単な REST API を作ってみました。
https://github.com/amtkxa/kotlin-spark-rest-api
作るにあたり、色んな方の記事を参考にさせていただきました。ありがとうございます。
前準備
主に使ったもの
- Kotlin version 1.3.11-release-272 (JRE 1.8.0_181-b13)
- spark-core 2.8.0
- jackson-databind 2.9.8
- jackson-module-kotlin 2.9.8
- sql2o 1.6.0
- MySQL 8.0.13
データベース
MySQL でテスト用のデータベースを準備して、下記テーブルを作成しておきます。
CREATE TABLE `user` (
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
見ての通り、めちゃくちゃ簡単なテーブルにしています。
いざ、作ってみる
プロジェクト構成
「TERASOLUNA Global Framework Development Guideline - アプリケーションのレイヤ化」を参考に、
以下の3レイヤーに分割して、それぞれのコンポーネントを作っていきます。
- アプリケーション層
- ドメイン層
- インフラストラクチャ層
エントリポイント
fun main(args: Array<String>) {
Server(args)
}
class Server : SparkApplication {
val logger = LoggerFactory.getLogger(Server::class.java)
override fun init() = Unit
constructor(args: Array<String>) {
initServer()
initControllers()
}
private fun initServer() {
port(8080)
}
private fun initControllers() {
val reflections = Reflections(
this.javaClass.`package`.name, MethodAnnotationsScanner(), TypeAnnotationsScanner(), SubTypesScanner()
)
val controllers = reflections.getTypesAnnotatedWith(SparkController::class.java)
controllers.forEach {
logger.info("Instantiating controller: " + it.name)
it.newInstance()
}
}
}
以下の理由から、org.reflections.Reflections.getTypesAnnotatedWith
を使って、個別に用意した@SparkController
が付与された Controller クラスを初期化する実装にしています。
- Controller にルーティングに関する記述を記載したい。
- Controller を増やすたびに、エントリポイントの編集をせずに済むようにしたい。
- Controller の初期化に関する実装をエントリポイントに直接書くことに心理的な抵抗があった。
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class SparkController
ここに関しては、どうするべきか、未だに悩んでいる。。。
アプリケーション層
Controller
@SparkController
class UserController {
private val jsonTransformer = JsonTransformer(ObjectMapper().registerKotlinModule())
private val mapper = ObjectMapper()
private val userService: UserService = UserServiceImpl()
init {
path("/users") {
get("", index(), jsonTransformer)
get("/:id", show(), jsonTransformer)
post("", create(), jsonTransformer)
patch("", update(), jsonTransformer)
delete("/:id", destroy(), jsonTransformer)
}
}
private fun index(): Route = Route { req, res ->
userService.findAll()
}
private fun show(): Route = Route { req, res ->
userService.findById(
id = req.params("id").toLong()
)
}
private fun create(): Route = Route { req, res ->
val request = mapper.readValue(req.body(), User::class.java)
val id = userService.create(
id = request.id,
name = request.name
)
res.status(201)
id
}
private fun update(): Route = Route { req, res ->
val request = mapper.readValue(req.body(), User::class.java)
val id = userService.update(
id = request.id,
name = request.name
)
res.status(200)
id
}
private fun destroy(): Route = Route { req, res ->
userService.delete(
id = req.params("id").toLong()
)
res.status(204)
}
}
Helper
Jackson を活用した Json 形式の文字列への変換クラスです。
class JsonTransformer(private val objectMapper: ObjectMapper) : ResponseTransformer {
override fun render(model: Any?): String = objectMapper.writeValueAsString(model)
}
ドメイン層
Model
data class User(
@JsonProperty("id", required = true)
val id: Long,
@JsonProperty("name", required = true)
val name: String
)
Service
interface UserService {
val userRepository: UserRepository
fun findAll(): List<User>
fun findById(id: Long): List<User>
fun create(id: Long, name: String): Long
fun update(id: Long, name: String): Long
fun delete(id: Long): Long
}
class UserServiceImpl : UserService {
override val userRepository: UserRepository = UserRepositoryImpl()
override fun findAll(): List<User> = userRepository.findAll()
override fun findById(id: Long): List<User> = userRepository.findById(id = id)
override fun create(id: Long, name: String) = userRepository.create(id = id, name = name)
override fun update(id: Long, name: String) = userRepository.update(id = id, name = name)
override fun delete(id: Long) = userRepository.delete(id = id)
}
Repository
interface UserRepository {
fun findAll(): List<User>
fun findById(id: Long): List<User>
fun create(id: Long, name: String): Long
fun update(id: Long, name: String): Long
fun delete(id: Long): Long
}
インフラストラクチャ層
Repository
データベースに対する各操作には、 Sql2o を使っています。
class UserRepositoryImpl : UserRepository {
override fun findAll(): List<User> =
DBConnectionManager.getSql2o().open().use { conn ->
conn.createQuery("SELECT id, name FROM user")
.executeAndFetch(User::class.java)
}
override fun findById(id: Long): List<User> =
DBConnectionManager.getSql2o().open().use { conn ->
conn.createQuery("SELECT id, name FROM user WHERE id = :id")
.addParameter("id", id)
.executeAndFetch(User::class.java)
}
override fun create(id: Long, name: String) =
DBConnectionManager.getSql2o().open().use { conn ->
conn.createQuery("INSERT INTO user (id, name) VALUES (:id, :name)")
.addParameter("id", id)
.addParameter("name", name)
.executeUpdate()
id
}
override fun update(id: Long, name: String) =
DBConnectionManager.getSql2o().open().use { conn ->
conn.createQuery("UPDATE user SET name = :name WHERE id = :id")
.addParameter("id", id)
.addParameter("name", name)
.executeUpdate()
id
}
override fun delete(id: Long) =
DBConnectionManager.getSql2o().open().use { conn ->
conn.createQuery("DELETE FROM user WHERE id = :id")
.addParameter("id", id)
.executeUpdate()
id
}
}
class DBConnectionManager {
companion object {
private val sql2o = Sql2o(
"jdbc:mysql://localhost:3306/sampledb",
"testuser",
"testuser"
)
fun getSql2o(): Sql2o = sql2o
}
}
最後に
Kotlin の良い書き方などはもっと知りたいので、これから少しずつ勉強していこうと思っています。
Spark Framework は、マイクロフレームワークに分類されるため、学習が容易であり、作り始めるまでにそこまで時間はかからなくていいなーと思いました。ただ、やはりその代償は少なからずあると思うので、使いどころは慎重に検討した方が良さそうだと感じました。
あと、なんとなくなんですが、Spark Framework について調べたつもりが Apache Spark の方が引っかかる。
というのは、きっとみんな通る道なんだろうな...と思いました。