しおメモ

雑多な技術系ブログです。

Kotlinで簡単にサーバーサイドレンダリング

みなさんKotlin使ってますか?
サーバーサイドでも便利なので、自分は最近よく使っています。
API側はよくみるので、今回はサーバーサイドレンダリングの方を書いてみます💪


Kotlinのいいところ

  • Java互換なのでJavaと混ぜて使える
  • ボイラープレートの削減、Lombokをほぼ内蔵
  • null安全
  • モダンな構文
  • etc...

特に、一番上のメリットは大きく、Better Javaとして混在させることもできます。

準備

使うもの

各種ソフトウェア バージョン
IntelliJ (できればUltimate) 新しいやつ
JDK 1.8.x
Kotlin 1.3.x
Spring Boot 2.1.x

謎の圧力によってSpring Bootを使っていきます。Spring Bootの中で使用する機能は、

  • spring-boot-starter-web
  • spring-boot-starter-thymeleaf
  • spring-boot-starter-test

の3つになります。thymeleafはテンプレートエンジンです。

プロジェクト作成

IntelliJ Ultimateの場合、新しくプロジェクトを作る際にSpringの選択肢があると思いますが、Communityの場合はSpring Intializrから作る必要があります。

f:id:scior:20190203161512p:plain

MavenかGradleか好きな方をを選択して、WebThymeleafを指定します。"Generate Project"を押すと、いい感じの雛形が生成されます。
(ホットリロードしてくれる、Devtoolsも便利ですが、IntelliJでは若干設定が必要です)

雛形を見てみる

最初に一つだけ、ソースファイルが生成されるはずなので、そちらを見ていきます。

package com.example.demo

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

@SpringBootApplication
open class DemoApplication

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

一箇所だけ修正点があるのですが、open class DemoApplicationopenのところだけ付け足しています。
KotlinのクラスはJavafinal class相当なので、@SpringBootApplicationが子クラスを生成出来るようにopenを明示的に指定します。

indexページを作る

中身はなんでも良いので、resources/templatesindex.htmlを作っておきます。

f:id:scior:20190203161528p:plain

(error.htmlも作っておくと、5xxエラー時にいかにもなページが出ずに済みます。)

実装

Controllerを書く

シンプルにindexページだけ返すControllerを作ってみます。

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping

@Controller
class RootController {
    private enum class PathTemplate(val path: String) {
        INDEX("index")
    }

    @GetMapping("")
    fun index(): String {
        return PathTemplate.INDEX.path
    }
}

enumで値を持てるので、Javaだと@AllArgsConstructor@Getterでゴリゴリ書いていた定数の部分が楽になります。

この段階で、ビルド(gradleの場合bootRun)すれば、localhost:8080で動くはずです。

WebConfigを書く

Spring Applicationが起動した時に読み込む、WebMvcConfigurerを設定していきます。
今回の場合、cssjsを配置するため、static以下のファイルを全てリソースとして追加します。

import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
open class WebConfig : WebMvcConfigurer {
    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry
            .addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
    }
}

先ほどと同じ理由で、openがついています。

オブジェクトをレンダリングする

実際に、オブジェクトをレンダリングしていきます。
controllerからModelAndViewを使ってthymeleafに渡していきます。

Modelクラスは色々な実装があり得ると思いますが、Kotlinにはdataクラスという、単純なプロパティの集まりを表現するクラスがあるのでそれを例にしていきます。 Javaでいうと、Lombok@Dataにおまけがついたような感じです。

data class Item (
    val user: String,
    val message: String,
)

Kotlinで記述した際に特徴的なのは、()の部分はコンストラクタなのですが、逆にコンストラクタから、コンパイラが中のプロパティを推論してくれるので、明示的なプロパティの記述は必要なくなります。

@GetMapping("")
fun index(): ModelAndView {
    val modelAndView = ModelAndView(PathTemplate.INDEX.path)
    modelAndView.addObject("item", Item("Taro", "Hello!!"))

    return modelAndView
}

ModelAndViewに追加したので、thymeleafからそのオブジェクトを参照することができます。
index.htmlを編集してみます。

<div>
    <p>
        <span th:text="|${item.user}が${item.message}と言っています|">aaa</span>
    </p>
</div>

th:textの部分が、thymeleafが置き換えてくれる部分です。
${}で文字列展開を行い、またそれを|で囲うと、文字列展開する部分以外はそのまま出力になります。

DIコンテナを使ってみる

Spring BootにはDIコンテナが内蔵されています。
コンストラクタインジェクションで、実際にDIをしてみます。

Serviceクラスを用意します。Spring Bootでは、@Serviceをつけることで、DIの対象となります。

interface IItemFetcher {
    fun fetch() : List<Item>?
}

@Service
class ItemFetcher : IItemFetcher {
    override func fetch() : List<Item>? {
        /** 実装 */
    }
}

interfaceを用意しましたが、Spring Bootではinterfaceに対しても、実装が一つならば、DIをすることができます。
2つ以上の実装がある場合は、明示的に指定したり、@Profileなどのアノテーションでprofile(application-xxx.yml)の切り替えを使って、DIできます。

実際に、このServiceクラスをDIします。

@Controller
class RootController (
    private val itemFetcher: IItemFetcher
) {
    @GetMapping("")
    fun index(): ModelAndView {
        val modelAndView = ModelAndView(PathTemplate.INDEX.path)
        modelAndView.addObject("items", itemFetcher.fetch())

        return modelAndView
    }
}

このように、コンストラクタを追加するだけで、itemFetcherにDIが自動で行われます。

中身は、Java@RequiredArgsConstructorをつけて、各プロパティにコンストラクタインジェクションをしていることと同じです。

// Javaの例
@Controller
@RequiredArgsConstructor
public final class RootController {
    // finalなので、RequiredArgsConstructorでこのプロパティをセットするコンストラクタができる
    @Nonnull
    private final IItemFetcher itemFetcher;

    /** ... */
}

リストをレンダリングする

thymeleafでは、リストもレンダリングすることができます。th:eachというプロパティを使います。

<div>
    <p th:each="item : ${items}">
        <span th:text="|${item.user}が${item.message}と言っています|">aaa</span>
    </p>
</div>

クライアントからはこのようなHTMLが見えます。

<div>
    <p>
        <span>TaroがHello!!!と言っています</span>
    </p>
    <p>
        <span>JiroがBye!!!と言っています</span>
    </p>
</div>

Slf4jを使う

残念ながら、Kotlinにはstaticがないので、Lombok@Slf4jは使えません。
代わりに、companion objectを使います。

import org.slf4j.loggerFactory

@Controller
class RootController (
    private val itemFetcher: IItemFetcher
) {
    companion object {
        private val logger = LoggerFactory.getLogger(RootController::class.java)
    }

    @GetMapping("")
    fun index(): ModelAndView {
        logger.info("GET ./")
        /** ... */
    }
}

companion objectはオブジェクトであり、staticとは若干違いますが、@JvmStaticJavaからstaticフィールドとして扱うことができます。

おわりに

thymeleafなど詳しく書けなかった部分は、また今度書きます。
自分は最近Nuxt.jsにも興味があります👀