はじめに (対象読者・この記事でわかること)

この記事は、Spring Bootでのアプリケーション開発経験があり、特にマルチモジュール構成を扱っている方、またはこれから導入を検討している方を対象としています。また、多言語対応(国際化、i18n)に取り組んでいる開発者にとっても有用な情報を提供するでしょう。

この記事を読むことで、Spring Bootのマルチモジュールプロジェクトで、プロパティファイル(特にmessages.propertiesなどの国際化リソース)からのメッセージ取得がうまくいかない原因と、その具体的な解決策を理解し、実装できるようになります。マルチモジュール化はプロジェクトの保守性や再利用性を高める上で非常に有効な手段ですが、リソースファイルの配置や読み込みパスの設定で思わぬ落とし穴にはまることがあります。特に国際化リソースは、モジュールを跨いで参照されることが多いため、この問題に直面するケースが少なくありません。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * Javaの基本的な文法とSpring Bootの基本(DI、MVCなど) * MavenまたはGradleによるマルチモジュールプロジェクトの構築経験 * プロパティファイル、特に国際化(i18n)におけるmessages.propertiesの概念

Spring Bootマルチモジュールとプロパティ読み込みの課題

Spring Bootアプリケーションは、デフォルトでsrc/main/resourcesディレクトリ配下のプロパティファイルを自動的に読み込みます。開発者がapplication.propertiesmessages.propertiesといったファイルをこのディレクトリに配置するだけで、Springが適切に認識し、利用可能にしてくれます。しかし、プロジェクトを複数の独立したモジュールに分割する「マルチモジュール構成」を採用した場合、このリソース読み込みの仕組みが途端に複雑になることがあります。

特に、各モジュールが独自のmessages.propertiesファイルを持ち、それをアプリケーションのどこからでも参照したい場合や、共通モジュールで定義したメッセージをビジネスロジックモジュールから利用したい場合に問題が発生しがちです。Spring BootのMessageSourceは、デフォルトではメインアプリケーションモジュール(つまり、@SpringBootApplicationアノテーションが付いたクラスを含むモジュール)のクラスパス直下にあるリソースのみを探索しようとします。そのため、他のモジュールに配置されたmessages.propertiesファイルは、適切に設定しないと認識されず、「Key not found」エラーが発生したり、設定したデフォルトメッセージが表示されたりする事態に陥ります。この問題は、国際化対応(i18n)を行う際に、メッセージを一元管理したい、あるいはモジュールごとに分離したいといった要件と衝突しやすいため、その解決策を知ることが重要になります。

マルチモジュール環境でのプロパティメッセージ取得問題の解決

ここでは、Spring Bootマルチモジュールプロジェクトにおいて、プロパティファイルからのメッセージ取得ができない問題の具体的な解決手順と実装方法を解説します。

まず、問題が発生する典型的なマルチモジュール構成を想定しましょう。

my-multi-module-app/
├── pom.xml (親プロジェクト、parent)
├── application-module/              # メインのSpring Bootアプリケーション
│   ├── pom.xml
│   └── src/main/resources/
│       └── messages.properties      # アプリケーション固有のメッセージ
│   └── src/main/java/com/example/app/AppController.java
├── common-module/                   # 共通のビジネスロジックやDTO、共通リソース
│   ├── pom.xml
│   └── src/main/resources/
│       └── messages.properties      # 共通メッセージ
│   └── src/main/java/com/example/common/CommonService.java
└── domain-module/                   # ドメイン固有のビジネスロジックやエンティティ
    ├── pom.xml
    └── src/main/resources/
        └── messages.properties      # ドメイン固有のメッセージ
    └── src/main/java/com/example/domain/DomainService.java

この構成において、application-moduleMessageSourcecommon-moduledomain-moduleに配置されたmessages.propertiesを読み込めない問題が発生します。

ステップ1: 問題の再現と確認

上記のプロジェクト構成で、各モジュールに以下のようにmessages.propertiesを作成し、application-moduleからメッセージを取得してみます。

application-module/src/main/resources/messages.properties

Properties
app.hello=Hello from App!

common-module/src/main/resources/messages.properties

Properties
common.greeting=Greetings from Common!

domain-module/src/main/resources/messages.properties

Properties
domain.error=Domain Error!

次に、application-moduleに以下のコントローラを作成し、各モジュールのメッセージ取得を試みます。

application-module/src/main/java/com/example/app/AppController.java

Java
package com.example.app; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Locale; @RestController public class AppController { @Autowired private MessageSource messageSource; @GetMapping("/") public String getMessages() { // application-module のメッセージ String appMsg = messageSource.getMessage("app.hello", null, Locale.getDefault()); // common-module のメッセージ (見つからない可能性が高い) String commonMsg = messageSource.getMessage("common.greeting", null, "Default Common Message", Locale.getDefault()); // domain-module のメッセージ (見つからない可能性が高い) String domainMsg = messageSource.getMessage("domain.error", null, "Default Domain Message", Locale.getDefault()); return String.format("App Message: %s<br>Common Message: %s<br>Domain Message: %s", appMsg, commonMsg, domainMsg); } }

このアプリケーションを実行し、http://localhost:8080/にアクセスすると、common.greetingdomain.errorに対応するメッセージが取得できず、指定したデフォルトメッセージ("Default Common Message", "Default Domain Message")が表示される、またはNoSuchMessageExceptionが発生するはずです。これは、MessageSourceが他のモジュールのmessages.propertiesを適切に認識できていないことを示しています。

ステップ2: 複数のリソースバンドルを登録する

Spring BootのMessageSourceのデフォルト設定は、spring.messages.basenameプロパティ(デフォルト値はmessages)で指定されたベース名のリソースバンドルを、アプリケーションのクラスパス直下から探索します。複数のモジュールに分散したリソースを読み込むためには、ResourceBundleMessageSourceを明示的に設定し、すべてのリソースバンドルのベース名を指定する必要があります。

メインアプリケーションモジュール(application-module)の@ConfigurationクラスでMessageSourceをカスタマイズします。

application-module/src/main/java/com/example/app/config/MessageConfig.java

Java
package com.example.app.config; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; @Configuration public class MessageConfig { @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); // ここにすべてのモジュールのmessages.propertiesのベース名を列挙します // 例: src/main/resources/i18n/app_messages.properties の場合は "i18n/app_messages" messageSource.setBasenames( "messages", // application-module の src/main/resources/messages.properties "common.messages", // common-module の src/main/resources/common/messages.properties (後述の対策後) "domain.messages" // domain-module の src/main/resources/domain/messages.properties (後述の対策後) ); messageSource.setDefaultEncoding("UTF-8"); messageSource.setUseCodeAsDefaultMessage(true); // キーが見つからない場合にキー自体をメッセージとして利用 return messageSource; } }

重要ポイント: * setBasenames()に指定する文字列は、クラスパスのルート(通常はsrc/main/resources以下)からの相対パスで、ファイル拡張子(.properties)とロケールサフィックス(_ja.propertiesなど)を含めない「ベース名」です。 * もし各モジュールで単にsrc/main/resources/messages.propertiesという名前で統一されている場合、同じベース名"messages"を複数のモジュールで持つことになり、Springがどのリソースを優先的に読み込むか不確実になることがあります。 * 推奨される解決策: 各モジュールでmessages.propertiesに異なるプレフィックスをつけるか、サブディレクトリに配置して、ベース名をユニークにすることです。 * 例: src/main/resources/i18n/app_messages.propertiessrc/main/resources/i18n/common_messages.propertiessrc/main/resources/i18n/domain_messages.properties のようにパスとファイル名をユニークにします。 * この場合、setBasenames("i18n/app_messages", "i18n/common_messages", "i18n/domain_messages") のように記述します。

共通モジュールからのプロパティ参照

common-moduledomain-module自身でMessageSource@Autowiredして利用したい場合も、上記MessageConfigapplication-moduleに定義していれば、それがメインのMessageSourceとして利用されます。つまり、他のモジュールで@Autowired MessageSourceと宣言するだけで、application-moduleで定義したMessageSourceがインジェクトされ、すべてのリソースバンドルを検索してくれます。

ハマった点やエラー解決

実装中に遭遇しがちな問題とその解決策をまとめます。

  • クラスパスの問題: setBasenames()に指定するパスが間違っていると、リソースが見つかりません。特にマルチモジュールの場合、各モジュールのsrc/main/resourcesが最終的にビルドされたJARファイルのどの位置に配置されるかを正確に理解する必要があります。
    • デフォルトでは、MavenやGradleは各モジュールのsrc/main/resourcesの内容を、そのモジュールのJARファイルのルートに配置します。application-moduleが他のモジュールに依存している場合、それらのモジュールのJARもapplication-moduleのクラスパスに含まれます。つまり、common-module/src/main/resources/messages.propertiesは、application-moduleの実行時にはクラスパス上でmessages.propertiesとして参照可能です。
    • しかし、複数のモジュールが同じベース名(例: messages)を持つ場合、どのファイルが優先されるかはクラスパスのロード順に依存し、予期せぬ挙動を引き起こす可能性があります。そのため、各モジュールでリソースファイルのパスをユニークにすることが最も安全で推奨される解決策です。
  • キャッシュの問題: 開発中にプロパティファイルを変更しても、アプリケーションに反映されない場合があります。これはIDEのキャッシュやSpring Boot DevToolsのキャッシュが原因の可能性があります。一度ビルドし直し、アプリケーションを完全に再起動してみてください。
  • 依存関係の問題: common-moduledomain-moduleapplication-modulepom.xml(またはbuild.gradle)でdependencyとして正しく追加されていないと、これらのモジュールのリソースはapplication-moduleのクラスパスに含まれません。必ず依存関係が正しく設定されていることを確認してください。 xml <!-- application-module/pom.xml の一部 --> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>common-module</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>domain-module</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!-- その他の依存関係 --> </dependencies>
  • ロケールの問題: MessageSourceがメッセージを見つけられない場合、現在のロケールに対応するファイル(例: messages_ja.properties)が見つからない可能性も考えられます。デフォルトロケールやフォールバックロジックが期待通りに機能しているか確認しましょう。messageSource.setUseCodeAsDefaultMessage(true)を設定しておくと、キーが見つからない場合にキー名自体が表示されるため、デバッグ時の問題切り分けがしやすくなります。

解決策

上記のハマりどころを踏まえ、Spring Bootマルチモジュール環境でプロパティメッセージを確実に取得するための最も推奨される解決策は以下の通りです。

  1. 各モジュールでリソースファイルのパスをユニークにする: 各モジュールのsrc/main/resourcesディレクトリ配下に、モジュール名を含んだサブディレクトリを作成し、その中にmessages.propertiesを配置します。これにより、クラスパス上で各リソースバンドルのベース名が一意になります。

    例: * application-module/src/main/resources/i18n/app_messages.properties * common-module/src/main/resources/i18n/common_messages.properties * domain-module/src/main/resources/i18n/domain_messages.properties

  2. メインアプリケーションでResourceBundleMessageSourceをカスタマイズ: application-module@Configurationクラスを作成し、MessageSource Beanを定義します。setBasenamesメソッドには、上記で設定したユニークなパス(ベース名)を全て指定します。

    ```java // application-module/src/main/java/com/example/app/config/MessageConfig.java package com.example.app.config;

    import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource;

    @Configuration public class MessageConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        // 各モジュールのmessages.propertiesのユニークなベース名を列挙
        messageSource.setBasenames(
            "i18n/app_messages",    // application-module のリソース
            "i18n/common_messages", // common-module のリソース
            "i18n/domain_messages"  // domain-module のリソース
        );
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setUseCodeAsDefaultMessage(true); // キーが見つからない場合にキー自体をメッセージとして利用
        return messageSource;
    }
    

    } ```

  3. 各モジュールのpom.xmlで依存関係を確認: application-modulepom.xmlには、common-moduledomain-moduleが正しくdependencyとして追加されていることを再確認します。これにより、これらのモジュールのリソースが最終的なJARファイルのクラスパスに確実に含まれます。

この設定により、SpringのMessageSourceは、アプリケーション全体で利用可能なすべての指定されたリソースバンドルを検索し、キーに対応するメッセージを正しく取得できるようになります。

まとめ

本記事では、Spring Bootのマルチモジュール構成において、プロパティファイルからメッセージが取得できないという問題の背景と具体的な解決策について解説しました。

  • 要点1: Spring BootのデフォルトMessageSourceは、マルチモジュール環境で他のモジュールのリソースを自動的に認識しない場合があるため、明示的な設定が必要であることを理解しました。
  • 要点2: ResourceBundleMessageSource@ConfigurationクラスでBeanとして定義し、setBasenames()メソッドを使って、アプリケーション全体で利用したいすべてのモジュールのリソースバンドルのベース名を指定することで、この問題を解決できます。
  • 要点3: 各モジュールでプロパティファイルのパスをユニークにする(例: src/main/resources/i18n/module_messages.properties)ことが、複数のリソースバンドルを確実に読み込み、競合を避けるための推奨プラクティスです。

この記事を通して、読者はSpring Bootのマルチモジュールプロジェクトにおける国際化リソースの適切な管理方法を理解し、実際に発生するであろうメッセージ取得のトラブルを自信を持って解決できるようになります。

今後は、Spring Cloud Config Serverを利用した中央集権的なプロパティ管理や、データベースからのメッセージ取得といった発展的な内容についても記事にする予定です。

参考資料