Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

やりたいこと

  • プロジェクトに、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' の記述を追記する
setting.gradle
rootProject.name = 'demo'
include 'hello'

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

helloディレクトリの直下にbuild.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-managementmavenBomを使う

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

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をつけたクラスを用意する必要がある

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)について

参考にリンクした公式のチュートリアルにこんな記述がある

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)の値を表示するエントリポイントを用意

@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
helloモジュール内
hoge: hogehoge
fuga: fugafuga

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

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

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

プロジェクトルート内
hoge: hoge
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)は1つだけ。
他のモジュールの設定値を取得してくるようなこともしなさそう。
であれば、プロジェクトルートであったりメインのモジュールで管理したほうが良さそう。

サンプルコード

https://github.com/eno314/SpringModuleSample

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away