SpringBootでメッセージファイルを扱う

android-chrome-512x512 Spring Framework
android-chrome-512x512

SpringBootのメッセージファイルの扱いについての解説です。
ユーザーに表示するメッセージなどを外部ファイルとしてまとめておくためのファイルで
国際化(i18n)・多言語対応を行う際によく使われます。

ブラウザの言語設定に応じてメッセージの言語を切り替える

まずはブラウザの言語設定に応じてメッセージの言語を切り替える方法です。日本語、英語を切り替えてみます。

事前準備として日本語、英語のメッセージファイルを用意します。「messages_ロケール.properties」のようにファイル名にロケールを付与することでSpringの自動切替処理を利用できます。”messages”の部分は任意の名前に変更可能です。今回は以下のファイルを用意します。

src/main/resources
├─ messages_ja.properties ・・・日本語用メッセージ
├─ messages_en.properties・・・英語用メッセージ

greeting=こんにちは!
welcome.user={0}さん、ようこそ!
greeting=Hello!
welcome.user=Welcome, {0}!

プロパティファイル名を認識させるためにapplication.ymlで「basename」を指定します。複数指定することも可能です。
「use-code-as-default-message」を trueにしておくとキーが見つからない場合も例外にならないので安全です。

spring:
  application:
    name: demo  # アプリケーション名(全環境共通で利用)
    messages:
        # メッセージプロパティファイルのベース名(クラスパス直下の場合は"messages"のみ、サブディレクトリの場合"i18n/messages"など)
        basename: messages
        # ファイルのエンコーディング
        encoding: UTF-8
        # キーが見つからない場合に例外を出さず、キー自体を返す
        use-code-as-default-message: true

Javaで設定する場合は以下のようにします。

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

@Configuration
public class MessageConfig {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}

Accept-Languageに応じたロケールの設定

メッセージプロパティは、MessageSource を用いた取得や <spring:message> タグによる参照が可能です。これらの処理で「どの言語ファイル(メッセージプロパティ)を利用するか?」を決定する仕組みが「LocaleResolver」です。ここではブラウザから送信される「Accept-Language」ヘッダーをもとに、適切な言語ファイルが選択されるよう構成します。

package com.example.demo.framework.config;

import java.util.Locale;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

/**
 * アプリケーション全体のロケール(言語・地域)判定の設定クラスです。
 * <p>
 * このクラスでは、どのようにしてユーザーの言語設定(ロケール)を判定するかを指定します。
 * ここでは {@link AcceptHeaderLocaleResolver} を利用し、HTTPリクエストのAccept-Languageヘッダーから
 * 自動的に最適なロケールを選びます。
 * <br>
 * デフォルトロケールは日本語に設定。
 */
@Configuration
public class LocaleConfig {

    /**
     * ロケール判定に利用するLocaleResolverをBean定義します。
     * <p>
     * {@link AcceptHeaderLocaleResolver}は、
     * HTTPリクエストの"Accept-Language"ヘッダー値から自動的にロケール(言語)を判定します。
     * </p>
     * @return 言語判定にAcceptHeaderLocaleResolverを使い、デフォルトを日本語にしたLocaleResolver Bean
     */
    @Bean
    public LocaleResolver localeResolver() {
        // Accept-Languageヘッダーを見てロケールを判定するResolverを生成
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();

        // ユーザーのリクエストにAccept-Languageが無い場合は日本語(Locale.JAPANESE)をデフォルトで利用
        localeResolver.setDefaultLocale(Locale.JAPANESE);

        // BeanとしてSpringに登録
        return localeResolver;
    }
}

LocaleResolverには他に以下のようなものもあります。

Resolver名特徴保存先用途・主なシーン
AcceptHeaderLocaleResolverAccept-Languageヘッダーからロケール自動判定なしリクエスト毎に言語を切り替えたい場合
SessionLocaleResolverセッション単位でロケールを保持セッションログイン後や一定期間同じ言語を使いたいウェブアプリ
CookieLocaleResolverクッキー単位でロケールを保持クッキーセッションをまたいで(ブラウザ再起動後も)言語維持したい場合
FixedLocaleResolver常に固定値なし多言語不要のシステム(日本語だけ等)

メッセージ取得ユーティリティ(ロケールに応じて自動切替)

アプリケーション向けのメッセージ取得ユーティリティです。現在のロケールに応じてメッセージを返却します。

package com.example.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

/**
 * メッセージ取得のためのユーティリティクラスです。
 * <p>
 * このクラスはSpringのMessageSourceと現在のロケール情報(LocaleContextHolder)を利用し、
 * 指定したメッセージキーおよび埋め込みパラメータから
 * 現在のリクエストのロケールに適した国際化メッセージを取得します。
 * <br>
 * コントローラやサービス層で多言語メッセージが必要な場合に、
 * 毎回Locale取得処理を書くことなく簡潔な呼び出しが可能となります。
 * </p>
 * <pre>
 * 例:
 *   messageUtil.getMessage("greeting");
 *   messageUtil.getMessage("welcome.user", "Taro");
 * </pre>
 *
 * メッセージプロパティに定義した
 * プレースホルダー付きメッセージも、可変長引数 args を指定すれば動的に展開されます。
 *
 */
@Component
public class MessageUtil {

    /**
     * Springのメッセージソース。DIにより自動注入。
     */
    @Autowired
    private MessageSource messageSource;

    /**
     * 指定したメッセージキーに対するメッセージを、現在のロケール(Accept-Language ヘッダーにより決定)に基づき取得します。
     *
     * @param key 取得したいメッセージのキー名 (messages_XX.properties のキー)
     * @param args メッセージ内のプレースホルダー埋め込み用パラメータ(可変長引数)
     *             例: "hello.user" = "こんにちは、{0} さん" の場合、args[0]に"太郎"など入れる
     * @return ロケールに合ったメッセージ文字列。キーが未定義の場合は例外。
     */
    public String getMessage(String key, Object... args) {
        // 現在の実行スレッド・リクエストに紐付くロケールを自動取得
        // Accept-Language ヘッダーなどから Resolver により決定されている
        return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());
    }
}

アプリケーションサンプル

MessageUtilを利用したアプリケーションサンプルです。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.framework.util.MessageUtil;

@RestController
public class MessageSampleController {

    @Autowired
    private MessageUtil messageUtil;

    @GetMapping("/msg1")
    public String msg1() {
        return messageUtil.getMessage("greeting");
    }

    @GetMapping("/msg2")
    public String msg2() {
        return messageUtil.getMessage("welcome.user", "太郎");
    }
}

メッセージ取得ユーティリティ(ロケールは明示的に指定)

ロケールを自動で判断せず、基本はデフォルトのロケールを使用。アプリケーションから明示的にロケールが指定された場合は、指定のロケールを返却するサンプルです。可変長引数を受け取り、プレースホルダーを置換するI/Fも用意しています。

import java.util.Locale;

import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;

/**
 * アプリケーション共通のメッセージ取得ユーティリティクラス * <ul>
 *   <li>application.ymlで設定したMessageSourceを@Autowriedで注入</li>
 *   <li>デフォルトロケール:日本語</li>
 *   <li>キーのみ指定時はデフォルトロケールでメッセージ返却</li>
 *   <li>キー+Locale指定時は指定ロケールのメッセージ返却</li>
 *   <li>可変長パラメータで{0},{1}等のプレースホルダーに値を埋め込み返却</li>
 *   <li>
 *     application.yml の <code>use-code-as-default-message: true</code> 設定で、プロパティにキーが無い場合はNoSuchMessageExceptionとならず、キー(例:"foo.bar")自身を返却。
 *   </li>
 * </ul>
 * <p>
 * 使用例:
 * <pre>
 *   @Autowired
 *   private AppMessageUtil messageUtil;
 *   String msg1 = messageUtil.getMessage("login.success"); // デフォルト(日本語)
 *   String msg2 = messageUtil.getMessage("login.success", Locale.ENGLISH); // 英語
 *   String msg3 = messageUtil.getMessage("user.greet", "田中さん", 3); // プレースホルダ例
 *   // user.greet=こんにちは、{0}。{1}件のご案内があります。
 * </pre>
 * </p>
 */
@Component
public class AppMessageUtil {

    /** デフォルトロケール(日本語) */
    private final Locale defaultLocale = Locale.JAPANESE;

    /** Spring管理のMessageSource */
    private final MessageSource messageSource;

    /**
     * コンストラクタ。MessageSourceはSpringによってインジェクションされる
     * 
     * @param messageSource application.ymlで構成済みのMessageSource
     */
    public AppMessageUtil(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    /**
     * 指定キーのメッセージをデフォルトロケールで取得(置換なし)
     * 
     * @param key メッセージプロパティのキー
     * @return メッセージ文字列
     */
    public String getMessage(String key) {
        return getMessage(key, defaultLocale);
    }

    /**
     * 指定キー+Localeのメッセージを取得(置換なし)
     * 
     * @param key メッセージプロパティのキー
     * @param locale 利用ロケール
     * @return メッセージ文字列
     */
    public String getMessage(String key, Locale locale) {
        return messageSource.getMessage(key, null, locale);
    }

    /**
     * 指定キーのメッセージで、可変長置換パラメータあり<br>
     * ※messages_*.properties の "{0}" "{1}" ... に args を差し込む
     * デフォルトロケールで取得
     * 
     * @param key メッセージプロパティのキー
     * @param args プレースホルダー値(可変長)
     * @return 置換済メッセージ
     */
    public String getMessage(String key, Object... args) {
        return getMessage(key, defaultLocale, args);
    }

    /**
     * 指定キー+Localeのメッセージで可変長置換パラメータあり
     *
     * @param key メッセージプロパティのキー
     * @param locale 利用ロケール
     * @param args プレースホルダー値(可変長)
     * @return 置換済メッセージ
     */
    public String getMessage(String key, Locale locale, Object... args) {
        return messageSource.getMessage(key, args, locale);
    }
}
greeting=こんにちは!
welcome.user={0}さん、ようこそ!
user.greet=こんにちは、{0}。{1}件のご案内があります。
greeting=Hello!
welcome.user=Welcome, {0}!
user.greet=Hello, {0}. You have {1} notifications.

アプリケーション利用サンプル

AppMessageUtilをアプリケーションから利用するサンプルです。

import java.util.Locale;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.framework.util.AppMessageUtil;

@RestController
public class MessageSampleController {

	@Autowired
	private AppMessageUtil messageUtil;

	@GetMapping("/msg1")
	public String msg1() {
		return messageUtil.getMessage("greeting");
	}

	@GetMapping("/msg2")
	public String msg2() {
		return messageUtil.getMessage("welcome.user", "太郎");
	}

	@GetMapping("/msg3")
	public String msg3() {
		return messageUtil.getMessage("user.greet", "太郎さん", 3);
	}

	@GetMapping("/msg3.en")
	public String msgEn() {
		return messageUtil.getMessage("user.greet", Locale.ENGLISH, "太郎さん", 3);
	}

}

2つ以上のメッセージファイルを扱う

MessageSource構成クラス

「messages.properties」に加えて「constants.properties」を追加し別々に扱いたい場合はAppMessageUtilとは別にクラスを用意し、クラスとプロパティファイルのマッピングをSpringに認識させるために、MessageSource構成クラスを作成します。

MessageSource型について、SpringはBean名でどちらを注入するか判別するため、コンストラクタ引数の名前と@Bean名が一致させておく必要があります。(引数:constantsMessageSource← @Bean:constantsMessageSource

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

/**
 * 複数プロパティファイル用のMessageSource構成クラス。
 * ・messages.propertiesはAppMessageUtilで利用
 * ・constants.propertiesはConstantsMessageUtilで利用
 */
@Configuration
public class MessageSourceConfig {

    @Bean
    public MessageSource messageMessageSource() {
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasename("messages");
        ms.setDefaultEncoding("UTF-8");
        ms.setUseCodeAsDefaultMessage(true);
        return ms;
    }

    @Bean
    public MessageSource constantsMessageSource() {
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasename("constants");
        ms.setDefaultEncoding("UTF-8");
        ms.setUseCodeAsDefaultMessage(true);
        return ms;
    }
}

固定値を扱うプロパティファイルのユーティリティ

2つ目のクラスはメッセージを扱うプロパティファイルではなく文字列や数値を扱うものを想定して作成してみます。

import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import java.util.Locale;

/**
 * <h2>固定値(定数)管理専用ユーティリティクラス</h2>
 * <ul>
 *   <li>constants.properties内の様々なキー・値(定数/マスタ値など)を取得する。</li>
 *   <li>ロケールは考慮せず、単純なキー=値の管理(多言語対応しない固定値列挙向け)。</li>
 *   <li>文字列取得・数値取得(int型)メソッドを提供。</li>
 *   <li>パラメータ置換、言語切替等は行わない。</li>
 * </ul>
 *
 * <pre>
 * 利用例:
 *   @Autowired
 *   private ConstantsPropUtil constantsPropUtil;
 *   String adminMail = constantsPropUtil.getString("admin.mail");
 *   int maxUserCount = constantsPropUtil.getInt("max.user.count");
 * </pre>
 *
 * このクラスはconstants.propertiesのみを対象とし、ロケールの区別なく値を返す点に注意。
 *
 * @author Your Name
 */
@Component
public class ConstantsPropUtil {

    /** constants.properties専用のメッセージソース(ロケール固定で利用) */
    private final ResourceBundleMessageSource messageSource;

    /**
     * コンストラクタ。
     * constants.properties用のResourceBundleMessageSourceを受け取り、ロケール考慮せずに利用。
     *
     * @param messageSource constants.properties用のResourceBundleMessageSource
     */
    public ConstantsPropUtil(ResourceBundleMessageSource messageSource) {
        this.messageSource = messageSource;
    }

    /**
     * 指定キーに対応する値を文字列として取得する。
     * ロケール固定・単純取得(多言語切替なし)。
     *
     * @param key propertiesファイル内のキー名
     * @return 該当キーの値。見つからない場合はキー名自体("key")を返す(setUseCodeAsDefaultMessage=true想定)。
     */
    public String getString(String key) {
        // ロケール未指定(固定値なのでROOTで十分。nullでもOK)
        return messageSource.getMessage(key, null, Locale.ROOT);
    }

    /**
     * 指定キーに対応する値を数値型(int)で取得する。
     * <p>
     * 値が数値として解釈できない場合はNumberFormatExceptionをスローします。
     * </p>
     *
     * @param key propertiesファイル内のキー名
     * @return 数値として解釈できる場合はint値
     * @throws NumberFormatException 数値変換できない場合
     */
    public int getInt(String key) {
        String value = getString(key);
        return Integer.parseInt(value); // パース失敗時は例外スロー
    }

    /**
     * 任意Object型として値の取得も可能(現状は常にString返却)。将来的な拡張用。
     *
     * @param key propertiesファイル内のキー名
     * @return 値(現状はString型)
     */
    public Object get(String key) {
        return getString(key);
    }
}

アプリケーションからの利用サンプル

テスト用に「constants.properties」を用意します。

# 管理者メールアドレス
admin.mail=admin@example.com

# 最大ユーザー数
max.user.count=100
import com.example.util.ConstantsPropUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * ConstantsPropUtilの利用例を示すRESTコントローラサンプル。
 */
@RestController
public class ConstantsPropUtilController {

    @Autowired
    private ConstantsPropUtil constantsPropUtil;

    /**
     * 管理者メールアドレスと最大ユーザー数を返却するシンプルなエンドポイント
     */
    @GetMapping("/constants/sample")
    public String getSampleConstants() {
        String adminMail = constantsPropUtil.getString("admin.mail");
        int maxUserCount = constantsPropUtil.getInt("max.user.count");
        return String.format("管理者メール: %s, 最大ユーザー数: %d", adminMail, maxUserCount);
    }

    /**
     * 任意キーで固定値を取得するAPIの例
     * /constants/key?name=キー名 で取得可能
     */
    @GetMapping("/constants/key")
    public String getConstantByKey(String name) {
        String value = constantsPropUtil.getString(name);
        return String.format("キー [%s] の値: %s", name, value);
    }
}

言語切替(i18n)不要のプロパティ参照

日本語や英語の切替が不要のプロパティファイルを参照する方法です。言語切替が不要な分シンプルになります。

サンプルプロパティファイルの準備

サンプルとしてプロパティファイルを準備します。プロパティファイルの中身は適当なものでOKです。

app.name=MyApp
app.version=2.1.0
app.mode=production

プロパティファイル参照用クラス

プロパティファイル(coreConfig.properties)の値を取得するためのクラスです。プロパティファイルパスは「@PropertySource(“classpath:prop/coreConfig.properties”)」で指定します。

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

/**
 * <p>
 * coreConfig.propertiesの値へ動的アクセスするための設定クラスです。
 * </p>
 * <ul>
 *   <li>指定したプロパティファイル(prop/coreConfig.properties)を{@code @PropertySource}でロード</li>
 *   <li>{@code Environment}をDIし、値取得メソッド(文字列・整数変換)が利用可能</li>
 *   <li>プロパティ値取得APIとして、{@link #getValue(String)} と {@link #getIntValue(String)} を提供</li>
 * </ul>
 *
 * <p>
 * 利用例:<br>
 *   <pre>
 *   CoreConfig coreConfig = ... // DI
 *   String url = coreConfig.getValue("core.url");
 *   Integer timeout = coreConfig.getIntValue("core.timeout");
 *   </pre>
 * </p>
 */
@Configuration
@PropertySource("classpath:prop/coreConfig.properties")
public class CoreConfig {
    /**
     * SpringのEnvironment。アプリ実行環境下のプロパティ値を取得可能。
     */
    private final Environment env;

    /**
     * {@link CoreConfig}のコンストラクタ。<br>
     * Spring BootのDIを利用して、{@link Environment}を注入します。
     * @param env Spring実行環境のEnvironment(DIで自動注入されます)
     */
    public CoreConfig(Environment env) {
        this.env = env;
    }

    /**
     * <p>
     * 指定されたキーでcoreConfig.propertiesから文字列値(String)を取得します。
     * </p>
     * <ul>
     *   <li>対象のプロパティキー(例: "core.url")に対応する値を文字列で返却</li>
     *   <li>キーが存在しない、もしくは値がnullの場合は{@code null}を返却</li>
     * </ul>
     *
     * @param key プロパティ値を取得したいキー(例: "core.url")
     * @return 対応する値(String)、値がない場合は{@code null}
     */
    public String getValue(String key) {
        return env.getProperty(key);
    }

    /**
     * <p>
     * 指定されたキーでcoreConfig.propertiesから値を取得し、整数型(int)として返却します。
     * </p>
     * <ul>
     *   <li>対象のプロパティキー(例: "core.timeout")に対応する値を数値に変換して返却</li>
     *   <li>値が存在しない場合や、数値に変換できない場合は{@code null}を返却</li>
     * </ul>
     *
     * @param key プロパティ値を取得したいキー(例: "core.timeout")
     * @return 変換可能な場合はint値、変換できない/値がない場合は{@code null}
     */
    public Integer getIntValue(String key) {
        String val = getValue(key);
        if (val == null) return null;
        try {
            return Integer.parseInt(val);
        } catch (NumberFormatException e) {
            return null;
        }
    }
}

アプリケーションからの利用サンプル

「CoreConfig」を利用しアプリケーションからプロパティ値を参照するサンプルです。

@RestController
public class CoreController {

    private final CoreConfig coreConfig;

    @Autowired
    public CoreController(CoreConfig coreConfig) {
        this.coreConfig = coreConfig;
    }

    /** coreConfig.properties の数値プロパティ取得API例 */
    @GetMapping("/core/int")
    public Integer getIntValue(@RequestParam("key") String key) {
        Integer value = coreConfig.getIntValue(key);
        if (value == null) {
            throw new IllegalArgumentException("整数値で取得できない key: " + key);
        }
        return value;
    }

    /** coreConfig.properties の文字列プロパティ取得API例 */
    @GetMapping("/core/value")
    public String getValue(@RequestParam("key") String key) {
        String value = coreConfig.getValue(key);
        if (value == null) {
            throw new IllegalArgumentException("値が存在しない key: " + key);
        }
        return value;
    }
}
タイトルとURLをコピーしました