Kotlin + Spring boot2 + Webflux + ThymeleafをRouterFunctions使って実装する

# はじめに
家での開発環境でJavaでやってた箇所をKotlinに変えていこうとしています。
その過程でタイトルにあるような環境をとりあえず作れるようにと作ってみたメモです。

# 環境
IntelliJ IDEA
Kotlin 1.2.20
Java 8
Spring Boot 2.0.0.RELEASE

# プロジェクト準備
https://start.spring.io/

今回は楽をしてInitializerを使用します。
IntelliJからSpringプロジェクトの生成は有料版じゃないと作れないらしいので、Web上から作成することにしています。
Dependenciesは後で追加すればいいので、空でもいいですし入れてもいいです。

## build.gradle
`spring-boot-starter`
`spring-boot-starter-webflux`
`spring-boot-starter-thymeleaf`

は最低限必要となります。
コード作成する際はthymeleafがなくてもどうにかなってしまいますが、動かす時にエラーが出る上になぜエラーになったかさっぱり分からないので気を付けた方が良いです(3時間消費)。

Initializerで作成したものに追加するだけですが、一応動いた時のbuild.gradleは全部載せておきます。

``` gradle
buildscript {
	ext {
		kotlinVersion = '1.2.20'
		springBootVersion = '2.0.0.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
		classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
		classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
	}
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.tasogarei'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
compileKotlin {
	kotlinOptions {
		freeCompilerArgs = ["-Xjsr305=strict"]
		jvmTarget = "1.8"
	}
}
compileTestKotlin {
	kotlinOptions {
		freeCompilerArgs = ["-Xjsr305=strict"]
		jvmTarget = "1.8"
	}
}

repositories {
	mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter')
    compile('org.springframework.boot:spring-boot-starter-webflux')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
	compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	compile("org.jetbrains.kotlin:kotlin-reflect")
	testCompile('org.springframework.boot:spring-boot-starter-test')
}
```

# applicationクラス
それでは実装に入ります。
まずはBootを動かすメインクラスです。

今回の例ではなんとなくREACTIVEを指定してみたかったので`SpringApplicationBuilder`で作成していますが、`SpringApplication.run(Application::class.java, *args)`でも動きます。


``` kotlin
package com.tasogarei.application

import org.springframework.boot.WebApplicationType
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.builder.SpringApplicationBuilder

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    SpringApplicationBuilder()
            .sources(Application::class.java)
            .web(WebApplicationType.REACTIVE)
            .run(*args)
}
```

# Handlerクラス
リクエストをさばくHandlerを作成します。
今回はHTMLを返したいのでContentTypeにHTMLとrenderを使ってViewを返します。
Viewの位置はWebFlux使わない時と同じルールで記載します。

attributeはMap作ってrenderの引数に入れてあげれば良いですが、今回はお試しなので省略します。

``` kotlin
package com.tasogarei.application.handler

import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class Handler {
    fun getIndex(req: ServerRequest): Mono<ServerResponse> {
        return ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("index")
    }
}
```

# RouterFunctionの作成
最後にRouterFunctionを作成します。
RouterFunctionを作成することによりMappingが集まるので、個人的にはControllerでMappingしていくより好みです。

なんとなく`accept`とか`nest`とか使ってURLの構造が複雑になった時の対応をさせていますが、`GET("/", handler::getIndex)`これだけ書けば動きます。
まぁ、普通のWebアプリケーションであればすぐに増えるので最初から追加が容易なよういに書いておいた方が良いとは思ってます。

あと、使用するHandlerを渡していますが、`routeIndex(handler: Handler)`のようにFunction側にも渡せます。
Handlerが増えた時は下記の例のようにコンストラクタに書くのではなくFunctionを増やすべきなのかなと思いましたが、どちらが良いのかは分かってません。

``` kotlin
package com.tasogarei.application.route

import com.tasogarei.application.handler.Handler
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.router

@Configuration
class Route(private val handler: Handler) {
    @Bean
    fun routeIndex() = router {
        accept(MediaType.TEXT_HTML).nest {
            GET("/", handler::getIndex)
        }
    }
}
```

# 動作確認
適当なHTMLを置いて起動してブラウザアクセスすれば見れるようになっているかと思います。

# 最後に
Dependency間違っててはまってしまいましたが、簡単にWebfluxでHTMLを扱えるようになりました。
Webflux使う使わないでメリットデメリットあるとは思いますが、好み的には使った方が好みなのでしばらく使っていこうかと思います。