MessageSource のメッセージから、プレースホルダーで別のプロパティを参照する

Spring の MessageSource は、メッセージ内に「{0}は必須項目です」のように { } 記号でプレースホルダーを置いておき、メッセージ取得時に動的に値を埋め込むことができます。こういった機能は Struts 以降の Web フレームワークには標準的に搭載されていることと思います。

さて。今回はそれとは似て非なる話です。具体的なコードを見たほうが分かりやすいでしょう。



別のプロパティ値を参照するようなメッセージを定義したい、というニーズです。残念ながら Spring が標準で提供している MessageSource には、こういった機能はないようなのです。あまりニーズがないのでしょうか。
一方で、Spring には Bean 定義ファイルに ${ } といった形式でプレースホルダーを置いて、定義ファイルのロード時にプロパティファイルの対応する値を適用する手法が用意されています。Spring を使ったことがある人ならご存知かと思いますが、 PropertyPlaceholderConfigurer のことです。Bean 定義ファイルでこのような動的置換の手法が使えているのですから、MessageSource にもこれを応用すれば、目標の実現はそれほど難しくなさそうに思えます。
というわけで、MessageSource の実装のひとつである ReloadableResourceBundleMessageSource にパッチをあててみました*1。修正後のソースを全部載せると700行近くあるため、差分のみです*2



本質的な修正点は以下の1箇所だけです。それ以外の部分は、以下を実現するための付随的なものです。

@@ -629,7 +644,8 @@
  if (this.properties == null) {
  return null;
  }
- return this.properties.getProperty(code);
+ String value = this.properties.getProperty(code);
+ return placeholderHelper.replacePlaceholders(value, placeholderProperties);
  }
 
  public MessageFormat getMessageFormat(String code, Locale locale) {


本当は ReloadableResourceBundleMessageSource を継承するだけで済ませたかったのですが、鍵となる部分のオーバーライドが簡単にはできないようになっており*3、直接修正するしかありませんでした。
ともかく、ここまで来ればあとは Bean 定義を書くだけです。



実行結果は以下の通り。

2011/07/02 23:27:14 org.springframework.test.context.TestContextManager retrieveTestExecutionListeners
情報: @TestExecutionListeners is not present for class [class sandbox.message.MessageTest]: using defaults.
2011/07/02 23:27:14 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
情報: Loading XML bean definitions from class path resource [sandbox/message/MessageTest-context.xml]
2011/07/02 23:27:15 org.springframework.context.support.AbstractApplicationContext prepareRefresh
情報: Refreshing org.springframework.context.support.GenericApplicationContext@1d9dc39: startup date [Sat Jul 02 23:27:15 JST 2011]; root of context hierarchy
2011/07/02 23:27:15 org.springframework.core.io.support.PropertiesLoaderSupport loadProperties
情報: Loading properties file from class path resource [sandbox/message/env.properties]
2011/07/02 23:27:15 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
情報: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@15ac3c9: defining beans [message,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,placeholderProperties,messageSource]; root of factory hierarchy
こんにちは。Xデーは 2011/07/02 です
2011/07/02 23:27:15 org.springframework.context.support.AbstractApplicationContext doClose
情報: Closing org.springframework.context.support.GenericApplicationContext@1d9dc39: startup date [Sat Jul 02 23:27:15 JST 2011]; root of context hierarchy


めでたしめでたし。

*1:該当のソースファイルをローカルのパッケージにコピーしてきて手を入れているだけですので、正確にはパッチ当てではないですが。

*2:修正元のソースは http://www.koders.com/java/fid490D60B2D8B47215C00DD894AD364C3F8347333A.aspx

*3:やりたいことは ReloadableResourceBundleMessageSource のインナークラスとして定義されている PropetiesHolder の getProperty メソッドを修正するだけなのですが、その PropetiesHolder を new している箇所をオーバーライドする必要があり、そのメソッドは private なフィールドを参照しており……というように芋づる式に修正箇所が増えてしまうため、直接手を加えた方が早そうでした。