SpringBootでモジュールを追加する

## やりたいこと
* プロジェクトに、helloというモジュールを作る
* helloモジュール内でControllerを作り、`/hello`にGETでアクセスしたら、`hello world`と返すエントリポイントを用意する

## 参考

[Getting Started · Creating a Multi Module Project](https://spring.io/guides/gs/multi-module/)

## 環境
* [Spring Initializer](https://start.spring.io/)
  * gradle Project
  * Kotlin
  * 2.1.0
  * Dependencies に 「Web」だけ追加
* IDE
  * IntelliJ CE 2018.3

## 手順

### helloモジュールの用意
* プロジェクトの直下に、`hello`というディレクトリを用意する
* プロジェクト直下の`setting.gradle`に、`include 'hello'` の記述を追記する

```gralde:setting.gradle
rootProject.name = 'demo'
include 'hello'
```

### helloモジュールの用のbuild.gradleを用意

helloディレクトリの直下に`build.gradle`を作成し、以下のような記述にする

```gradle:build.gralde
buildscript {
    ext {
        kotlinVersion = '1.3.10'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
    }
}

ext {
    springBootVersion = '2.1.0.RELEASE'
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'io.spring.dependency-management'

group = 'com.example'
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 {
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('com.fasterxml.jackson.module:jackson-module-kotlin')
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")
    }
}
```

サブモジュールには`org.springframework.boot`のgradleプラグインを使う必要は無いが、`spring-boot-dependencies`は必要なので、`dependency-management`と`mavenBom`を使う

もしかしたらKotlinのプラグインも`dependency-management`と`mavenBom`を使うようにしたほうが良いのかも

### helloモジュールの依存をプロジェクト直下に追加

プロジェクトの直下のbuild.gradleのdependenciesに`implementation project(':hello')`の記述を追記

```gradle

...

dependencies {
	implementation('org.springframework.boot:spring-boot-starter-web')
	implementation('com.fasterxml.jackson.module:jackson-module-kotlin')
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	
	implementation project(':hello')
	
	testImplementation('org.springframework.boot:spring-boot-starter-test')
}
```

### helloモジュールにControllerを用意する
* hello ディレクトリ以下に、`src/main/kotlin`ディレクトリを作成
* `src/main/kotlin`ディレクトリ以下に、`com.example.demo.hello`というパッケージを用意
* `com.example.demo.hello`パッケージ以下に、Controllerクラスを用意

```kotlin
package com.example.demo.hello

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
class Controller {

    @GetMapping("/hello")
    fun hello(
        @RequestParam(required = false, defaultValue = "world") name: String
    ): String {
        return "hello $name"
    }
}
```

### 動作確認
* アプリケーションを実行する
* `http://localhost:8080/hello` にブラウザでアクセスして、`hello world`が表示されることを確認する

## モジュール以下でユニットテストを書くときの注意点

mockMvcを使うようなSpringの機能を使ったテストをモジュールの中で書きたい場合、`@SpringBootApplication`をつけたクラスを用意する必要がある

```kotlin:Springの機能を使ったユニットテストのサンプル
@ExtendWith(SpringExtension::class)
@WebMvcTest
class ControllerTest(@Autowired val mockMvc: MockMvc) {
    @Test
    fun test() {
        // /hello に リクエストパラメータ name=test を付与してリクエストすると、
        // ステータスが200でレスポンスが"hello test" になることを確認する
        val request = MockMvcRequestBuilders.get("/hello")
            .param("name", "test")
        mockMvc.perform(request)
            .andExpect(MockMvcResultMatchers.status().isOk)
            .andExpect(MockMvcResultMatchers.content().string("hello test"))
    }

    @SpringBootApplication
    internal class TestConfiguration
}
```

`@SpringBootApplication`をつけたクラスを用意しないと、こんなエラーメッセージが出ます

```
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
```

`@SpringBootApplication`をつけたクラスは、別ファイルとして用意しても良さそう

## application.properties(yml)について

参考にリンクした[公式のチュートリアル](https://spring.io/guides/gs/multi-module/)にこんな記述がある

> In the sample above we have configured the `service.message` for the test using the default attribute of the `@SpringBootTest` annotation. It is not advisable to put `application.properties` in a library because there might be a clash at runtime in the application that uses it (only one `application.properties` is ever loaded from the classpath). You could put `application.properties` in the test classpath, but not include it in the jar, for instance by placing it in `src/test/resources`.

application.properties(yml) は各モジュールの下に作るのは衝突するから推奨しないとのこと。

試しに、今回の例でプロジェクトルート内とhelloモジュール内にapplication.properties(yml)を用意して、どんな動きになるか見てみた

### 準備

Controllerにapplication.properties(yml)の値を表示するエントリポイントを用意

```kotlin
@RestController
class Controller {

    @Value("\${hoge}")
    private val hoge: String? = null

    @Value("\${fuga}")
    private val fuga: String? = null

...

    @GetMapping("/test")
    fun test(): String {
        return "$hoge $fuga"
    }
}
```

### 検証1 helloモジュール内にだけapplication.properties(yml)を用意

```yaml
hoge: hogehoge
fuga: fugafuga
```

この状態でアプリケーションを実行し、http://localhost:8080/test にアクセスしたら `hogehoge fugafuga` が表示された

モジュール内にしかapplication.properties(yml)がなくても、問題なく動く

### 検証2 両方にapplication.properties(yml)を用意

```yaml:プロジェクトルート内
hoge: hoge
fuga: fuga
```

```yaml:helloモジュール内
hoge: hogehoge
fuga: fugafuga
```

この状態でこの状態でアプリケーションを実行し、http://localhost:8080/test にアクセスしたら `hoge fuga` が表示された

プロジェクトルートの設定のほうが優先された

### 検証3 両方にapplication.properties(yml)を用意、だたしプロジェクトルート側は1部削除

```yaml:プロジェクトルート内
hoge: hoge
```

```yaml:helloモジュール内
hoge: hogehoge
fuga: fugafuga
```

この状態でこの状態でアプリケーションを実行しようとしても、クラッシュして実行エラーになる
```
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'controller': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'fuga' in value "${fuga}"
...
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'fuga' in value "${fuga}"
...
```

足りない項目を別のモジュールにあるapplication.properties(yml)から取得してくるようなことはしなさそう

### まとめ

モジュール毎にapplication.properties(yml)を作り、それぞれ独自の設定値を用意しても、優先されるapplication.properties(yml)は１つだけ。
他のモジュールの設定値を取得してくるようなこともしなさそう。
であれば、プロジェクトルートであったりメインのモジュールで管理したほうが良さそう。

## サンプルコード

https://github.com/eno314/SpringModuleSample