Spring MVC(もしくはSpring Boot)のmessages多言語対応でYAMLが利用できた件

ようやく期限キーパーver2.5がリリースできた。
なんかここ最近のiOSバージョンアップで、アプリ名にCFBundleDisplayNameが効かなくなってる!?
初回インストールの時だけなのかな?自分のMacとiPhoneではいけてたから油断してた。
おかげで2回もリジェクトされた・・・。

まあそれはさておき。
Javaのお話。お仕事で多言語対応のお話が来まして。
まあ、てっとり早いのはmessages.propertiesで対応するんですけど。
Spring Bootのこのご時世、さすがにPropertiesはもう使いたくない。
Sakuraエディタで開いた瞬間、日本語がすべてユニコードに置き換わるあんなファイルはもうコリゴリだ。
ただWebで調べてみてもapplication.propertiesのYAML化はたくさん出てくるけど、多言語対応messagesファイルのYAML化はきちんとした情報が見つからなかった。
独自のメッセージソースクラス作成とかリソースバンドルクラスとかよく分かんない状態から、いろんな記事の情報をかき集めてなんとかできたのでここに記す。


そろそろPropertiesファイルを卒業したいアナタにっ!


まずは、目的のYAMLファイルを用意。
message_ja.yml

error:
  AE00101: "ユーザーID/パスワードを入力してください。"

info:
  AI00101: "ログインに成功しました"

message.yml

error:
  AE00101: "Please enter your user ID / password."

info:
  AI00101: "Login succeeded"

以下の実装では、これらのファイルをクラスパスの通っているsrc/main/resourcesの下にi18nフォルダを作り、その下に配置した状態です。
YAMLファイルをメッセージファイルとして読み込むため、各クラスを準備していきます。

ResourceBundleMessageSourceを継承した独自のメッセージソースクラスを定義する。
このご時世にKotlinじゃねーのかよ、っていうツッコミは無しこちゃん。

package setting;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.springframework.context.support.ResourceBundleMessageSource;

public class YamlMessageSource extends ResourceBundleMessageSource {

	// 独自のリソースバンドルコントローラを準備
	private static final ResourceBundle.Control ctrl = new YamlResourceBundleControl();
	
    @Override
    protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
		ResourceBundle res = ResourceBundle.getBundle(basename, locale, ctrl);
		return res;
    }
}

リソースバンドルコントローラ(独自)

package setting;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;

public class YamlResourceBundleControl extends ResourceBundle.Control {

	private static final String ext = "yml";

	public List<String> getFormats(String baseName) {
		return Arrays.asList(ext);
	}

	/**
	 * YAMLリソースをバンドルします
	 */
	public ResourceBundle newBundle(String baseName, Locale locale,
			String format, ClassLoader loader, boolean reload)
			throws IllegalAccessException, InstantiationException, IOException {
		if (baseName == null || locale == null || format == null
				|| loader == null) {
			throw new NullPointerException();
		}

		ResourceBundle bundle = null;
		if (format.equals(ext)) {
			// ここでYAMLを読み込むのだ!
			bundle = new YamlResourceBundle(baseName, locale);
		}
		return bundle;
	}
}

YAMLメッセージファイルからバンドルを生成するクラス

package setting;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Properties;
import java.util.ResourceBundle;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;

public class YamlResourceBundle extends ResourceBundle {

	private Properties props = new Properties();

	private static String ext = ".yml";
	
	/**
	 * リクエストされたロケールに対して対応するYAMLファイルが存在する場合、それを読み込みます。 [basename]_[locale].yml, ex. i18n/messages_ja.yml
	 * ファイルが存在しない場合、デフォルトを読み込みます。 ex. i18n/messages.yml
	 * @param basename
	 * @param locale
	 * @throws IOException
	 */
	YamlResourceBundle(String basename, Locale locale) throws IOException {
		String messageFile = basename;
		ClassPathResource rsc = null;
		if (StringUtils.isNotBlank(locale.getLanguage())) {
			messageFile += "_" + locale.getLanguage() + ext;
	        rsc = new ClassPathResource(messageFile);
	        if (rsc.exists()) {
	        	messageFile = basename + ext;
	        }
	        else {
	        	rsc = null;
	        }
		} else {
        	messageFile = basename + ext;
	        rsc = new ClassPathResource(messageFile);
		}
		if (rsc != null) {
	        YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
	        bean.setResources(rsc);
	        props = bean.getObject();
		}
	}

	@Override
	protected Object handleGetObject(String key) {
		return props.getProperty(key);
	}

	@Override
	@SuppressWarnings("unchecked")
	public Enumeration<String> getKeys() {
		@SuppressWarnings("rawtypes")
		Enumeration enm = props.keys();
		return enm;
	}
}

Spring MVCとかだとYAML対応してなかったりするから依存性管理にsnakeyamlパッケージを追加する。
例)pom.xml


    org.yaml
    snakeyaml
    1.24

MessageSourceのBeanとして独自のBeanをロードする。
Spring MVCだと、application-config.xmlかな?
以下のようにBeanを追加。



	
	

Spring Bootの場合は上記のXMLの代わりに@Configure/@Beanを使うよね。

package setting;
import java.io.IOException;
import org.springframework.context.MessageSource;

@Configure
public class MessageConfig {
    @Bean
    public MessageSource messageSource() throws IOException {
    	YamlMessageSource messageSource = new YamlMessageSource();
    	// クラスパスの通っているsource/main/resourcesの下にi18nフォルダ、その下にmessages.ymlを配置
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
}

準備完了。WEBサーバを起動。

まずはコントローラから。Localeをメソッドの引数に取得してメッセージソースに渡す。

@Controller
class xxController {
  @Autowired
  MessageSource msgSrc;

  @RequestMapping(value = {"/"}, method = RequestMethod.GET)
  public String get(Locale locale, Model model)
  {
    String msg = msgSrc.getMessage("error.AE00101", null, locale);
    System.out.println(msg);  // ブラウザの言語設定でテキストが切り替わることを確認。
    return "index";
  }
}

thymeleafは#{xxx}で参照できる。ロケールは内部で判定してるみたい。
index.html



  

Springのリソースバンドルの仕組みというかクラスを継承してるから、無駄なリソースの再読み込みはしないようになってる。作った人えらい。
またYamlResourceBundleのとこで、他の言語ファイルmessages_xxを用意すればブラウザの言語設定次第で勝手に対応してくれる仕様とした。

今更な感じかもしれないけどみなさんのお仕事に役立ててやってください。

あー、疲れた。

コメントを残す

メールアドレスが公開されることはありません。