やりたいこと
- プロジェクトに、helloというモジュールを作る
- helloモジュール内でControllerを作り、
/hello
にGETでアクセスしたら、hello world
と返すエントリポイントを用意する
参考
Getting Started · Creating a Multi Module Project
環境
-
Spring Initializer
- gradle Project
- Kotlin
- 2.1.0
- Dependencies に 「Web」だけ追加
- IDE
- IntelliJ CE 2018.3
手順
helloモジュールの用意
- プロジェクトの直下に、
hello
というディレクトリを用意する - プロジェクト直下の
setting.gradle
に、include 'hello'
の記述を追記する
rootProject.name = 'demo'
include 'hello'
helloモジュールの用のbuild.gradleを用意
helloディレクトリの直下にbuild.gradle
を作成し、以下のような記述にする
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')
の記述を追記
...
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クラスを用意
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
をつけたクラスを用意する必要がある
@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)について
参考にリンクした公式のチュートリアルにこんな記述がある
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 putapplication.properties
in a library because there might be a clash at runtime in the application that uses it (only oneapplication.properties
is ever loaded from the classpath). You could putapplication.properties
in the test classpath, but not include it in the jar, for instance by placing it insrc/test/resources
.
application.properties(yml) は各モジュールの下に作るのは衝突するから推奨しないとのこと。
試しに、今回の例でプロジェクトルート内とhelloモジュール内にapplication.properties(yml)を用意して、どんな動きになるか見てみた
準備
Controllerにapplication.properties(yml)の値を表示するエントリポイントを用意
@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)を用意
hoge: hogehoge
fuga: fugafuga
この状態でアプリケーションを実行し、http://localhost:8080/test にアクセスしたら hogehoge fugafuga
が表示された
モジュール内にしかapplication.properties(yml)がなくても、問題なく動く
検証2 両方にapplication.properties(yml)を用意
hoge: hoge
fuga: fuga
hoge: hogehoge
fuga: fugafuga
この状態でこの状態でアプリケーションを実行し、http://localhost:8080/test にアクセスしたら hoge fuga
が表示された
プロジェクトルートの設定のほうが優先された
検証3 両方にapplication.properties(yml)を用意、だたしプロジェクトルート側は1部削除
hoge: hoge
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)は1つだけ。
他のモジュールの設定値を取得してくるようなこともしなさそう。
であれば、プロジェクトルートであったりメインのモジュールで管理したほうが良さそう。
サンプルコード