Springのプロジェクトをマルチモジュール構成で作る

はじめに

Mavenではプロジェクトをマルチモジュール構成に構築することは可能ですが、Spring Frameworkでもその機能を利用することが可能です。
この辺の機能はあまり触ったことがなかったので、触ってみたいと思ったがのがこのブログのモチベーションです。
このブログでは以下のことをゴールとします。

  • Spring Bootでのマルチモジュールプロジェクトの作り方を学ぶ

基本的にはここに書いてあることに従ってプロジェクトの作成を進めますが、気になったところは少し深ぼりしようかと思います。
また、ビルドツールはMavenを利用しようと思います。

プロジェクトを作成する

以下の二つのモジュールから成り立つWebアプリケーションを作成します

  • library
    • プロパティファイルから文字列を読み取り呼び出し側に返すサービスを含むライブラリ。applicationから利用される。
  • application
    • Springアプリケーションの本体。libraryを利用する。

動作環境

動作環境は以下のようになってます

$ uname -srvmpio
Linux 5.3.0-46-generic #38~18.04.1-Ubuntu SMP Tue Mar 31 04:17:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ mvn -v
Apache Maven 3.6.0
Maven home: /usr/share/maven
Java version: 11.0.7, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.3.0-46-generic", arch: "amd64", family: "unix"

ルートプロジェクトを作成する。

まずはルートとなるプロジェクトを作成します。 私はIntelliJから作成しましたが、やり方は何でも良いと思います。 この際にsrcディレクトリは不要そうだったので削除しました。
pom.xmlを書き換えて以下のようにします。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dev.hirooka</groupId>
    <artifactId>multi-module</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>library</module>
        <module>application</module>
    </modules>
</project>

ポイントは<packageing>タグと<modules>タグです。
Mevanでマルチモジュールなプロジェクトを作成する場合、<packageing>ダグはpomに指定する必要があるようです。
<modules>タグでは、利用するモジュールを定義します。

まだ、applicationとlibraryは作成していないのでInteliJなどでは警告が出ていると思いますが、これから作成するので無視してしまって大丈夫です。

各モジュールを作成する

プロジェクトのルート直下に以下の2つのライブラリを作成します。

  • library
  • application

それぞれのプロジェクトを作成していきます。
プロジェクトの作成にはSpring Initializrを利用します。

libraryを作成する

libraryのプロジェクトを準備する

Spring Initializrでlibraryプロジェクトを作成します。
今回は以下のような設定を選択しました。

Project:Maven Project
Langage:Language
Group:dev.hirooka
Artifact:library
Name:library
Package name:dev.hirooka.library
Spring Boot バージョン:2.2.6 Java バージョン:11

ここで、Group(僕の例だとdev.hirooka)とArtifact(library)は後ほど使うことになります。値は任意のもので構いませんが、後ほどこれらの値が出てきた際は各環境での読み替えを願いします。
依存に関してはspring-boot-starterとテスト関連のもののみ必要なので、ここでは追加では何もいれません。

ダウンロードしたzipを解答し、ルートプロジェクトの直下に置いてください。
また、Spring Initializrを用いた場合Mavenラッパーがプロジェクト内に含まれていると思いますが、これらはプロジェクトのルートに移して置いてください。

$ mv library/mvnw* library/.mvn .

ここまででプロジェクトの構造は以下のとおりになっていると思います。

$ tree
.
├── application
├── library
│   ├── HELP.md
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── dev
│       │   │       └── hirooka
│       │   │           └── library
│       │   │               └── LibraryApplication.java
│       │   └── resources
│       │       └── application.properties
│       └── test
│           └── java
│               └── dev
│                   └── hirooka
│                       └── library
│                           └── LibraryApplicationTests.java
├── multi-module.iml
├── mvnw
├── mvnw.cmd
└── pom.xml

libraryのpomを書き換える

Spring Initialzrで作成したアプリはデフォルトで実行可能Jarを作成するようになっていますが、Libraryはメインメソッドも必要なく、実行可能Jarを作る必要はありません。
ビルドシステムにそれを伝えるためにlibraryのpomを修正する必要があります。
具体的にはpomの以下の箇所を削除します。

 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

また、メインメソッドを持つLibraryApplication.javaも削除しておきましょう。

Serviceを作成する

libraryのプロジェクトを作成したところで、そのライブラリに属するサービスを作成します。

@Service
@EnableConfigurationProperties(ServiceProperties.class)
public class MyService {

    private final ServiceProperties serviceProperties;

    public MyService(ServiceProperties serviceProperties) {
        this.serviceProperties = serviceProperties;
    }

    public String message() {
        return this.serviceProperties.getMessage();
    }
}

このサービスでは、プロパティファイルから値を読み込んで、そのメッセージを返します。
値を読み込むための、ServiceProperties.classも作成します。

@ConfigurationProperties("service")
public class ServiceProperties {

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

サービスのテストを記述する。

@SpringBootTestアノテーションを使えばモジュールのテストを書くことも可能です。
以下に作成したサービスのテストの例を示します。

package com.example.multimodule.service;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest("service.message=Hello")
public class MyServiceTest {

  @Autowired
  private MyService myService;

  @Test
  public void contextLoads() {
    assertThat(myService.message()).isNotNull();
  }

  @SpringBootApplication
  static class TestConfiguration {
  }

}

@SpringBootTestの引数として"service.message"を渡しています。
ここで、モジュールを作成する際の注意点として、モジュール単位でapplication.propertiesを作らないほうが良いです。これは、SpringBootではクラスパス配下の読み込む、application.propertiesは1つだけであるため、このモジュールを利用する側に影響を与える可能性があるからです。 ただしtest/resource配下のapplication.propertiesはビルド後のjarの中に含まれないため、テストのために記述しておくことは可能です。

ここまでで、libraryの作成は完了です。

applicationを作成する

applicationのプロジェクトを作成する

libraryと同様にSpring Initializrを使ってaplicationのプロジェクトを作成します。

Project:Maven Project
Langage:Language
Group:dev.hirooka
Artifact:application
Name:application
Package name:dev.hirooka.application
Spring Boot バージョン:2.2.6 Java バージョン:11

依存にはwebとactuatorを追加しておきます。
Mavenラッパーはプロジェクトルートのものを使うため、削除しておきます。

$ rm -rf application/mvnw* application/.mvn

ここで、application側では、libraryを利用するためpomに依存を追加します。

<dependency>
    <groupId>dev.hirooka</groupId>
    <artifactId>library</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

applicationを作成する。

以下のようにアプリケーションを実装します。

@SpringBootApplication(scanBasePackages = "dev.hirooka")
@RestController
public class Application {


    private MyService myService;

    public Application(MyService myService) {
        this.myService = myService;
    }

    @GetMapping("/")
    public String hoge(){
        return myService.message();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

ここでは、プロパティファイルから読み込んだ文字列を よくあるコントローラーの実装なのですが、ひとつだけポイントがあります。
それは、@SpringBootApplication(scanBasePackages = "dev.hirooka")コンポーネントスキャンを対象とするパッケージを指定していることろです。
これは、libraryで実装されているserviceが存在しているパッケージはdev.hirooka.serviceであり、@SpringBootApplicationが存在しているパッケージはdev.hirooka.applicationなので、そのままではコンポーネントスキャンの対象とならないからです。

コンポーネントスキャンの対象にするためには以下の3つの方法があります。

  • @Import(MyService.class)を使う
  • @SpringBootApplication(scanBasePackageClasses={…​})を活用する。
  • @SpringBootApplication(scanBasePackages = "dev.hirooka")のようにパッケージ名を指定する

今回は三個目の方法を利用しました。

最後にapplication/src/main/resources/application.propertiesservice.messageを追加します。

service.message = hello, multi module

これで、applicationも完成です。
起動して動作を確認してみましょう。

$ ./mvnw install && ./mvnw spring-boot:run -pl application
$ curl localhost:8080
hello, multi module

きちんとlibrayも使えて、application.propatiesが読み込めていることがわかります。

感想

Springのアプリケーションのmodule化を行なうためには、modulithsといったようなライブラリもあります。こんどはこっちを試してみたいです。