SpringFramework と Velocity の統合

SpringFramework 上のコンポーネントとして Velocity を使いたいという状況は、割とよくあるのではないかなと思っています。プレゼンテーション層に Velocity を採用するパターンもありますが、アプリケーションから定型のメールを送信するケースなどでも、テンプレートエンジンを使いたくなります。パスワードリマインダーやユーザー登録まわりのメール通知、実行に数分から数十分を要するようなオンラインバッチ処理の完了通知など、ユーザーへの情報通知手段としてのメールが使われるケースは少なくないでしょう。
さて、Velocity を生で使っても良いのですが、SpringFramework を中核に据えたシステムでは、やはり Velocity も SpringFramework のアーキテクチャの上で統一的にコンポーネント管理したくなります。SpringFramework 側では、このような昔からあるニーズに応えるべく、VelocityEngineFactoryBean という Bean 生成エンジンを準備してくれています。これを使えば万事解決なのですが、この VelocityEngineFactoryBean についての言及を日本語圏では見かけませんので、せっかくですから記事にしてみました。このクラス自体はかなり昔からあるものなのですが、意外に言及が少ないのは不思議です。解説するほどのものでもないから、かな?
SpringFramework を使い慣れた方なら XxxFactoryBean という名称でピンと来るのではないかと思います。そうです。この FactoryBean は、VelocityEngine を Spring Bean として生成するための bean です。これを bean 定義ファイルに記述するだけで設定は完了です。

    <bean id="velocityEngine"
            class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
        <property name="resourceLoaderPath"
                value="classpath:path/to/your/resource" />
        <property name="velocityPropertiesMap">
            <map>
                <entry key="input.encoding" value="UTF-8" />
                <entry key="output.encoding" value="UTF-8" />
            </map>
        </property>
    </bean>

では、生の Velocity と比べてよい点をあげてみましょう。

  • 上記のように bean 定義の中に直接プロパティを設定することで、いままで velocity.properties に記述していたような設定内容を SpringFramework 側の applicationContext.xml に移植できます。これにより、設定ファイルの数をを減らせます。
  • resourceLoaderPath には、カンマ区切りで複数のパスを指定できます(これは、生 Velocity でも可能だったかもしれません)。
  • scheme は、classpath: と file: をサポートしています。
  • これらの scheme を書かずに、直接パスを記述することもできます。相対パスで書く場合、起点となるパスは ApplicationContext が動作しているパスのようです。

このあたりの説明は、SpringFramework の Javadoc に記載があります。また、実装を知りたい場合は VelocityEngineFactory.java の initVelocityResourceLoader メソッドに具体的な処理が書かれていますので、そちらを読んでみるのも良いでしょう。ともあれ、これで VelocityEngine が Spring のコンポーネントとして登録されますので、インジェクションするなり ApplicationContext#getBean("velocityEngine") するなり好きにできます。
これで VelocityEngine インスタンスをお手軽に取得可能となりましたが、実際に Velocity を使ってやりたいことは、テンプレートにパラメーターを差し込み、新たな文字列を得ることです。このマージ作業というのは、

  1. パラメーターの値を VelocityContext に設定する
  2. VelocityEngine#mergeTemplate または Template#merge メソッドを呼び出す

という手順で実行します。以下は簡易的な実行イメージです。

    Map model = new HashMap();
    StringWriter writer = new StringWriter();
    
    model.put("param1", "value1");
    model.put("param1", "value2");
    model.put("param1", "value3");
    VelocityContext ctx = new VelocityContext(model);
    velocityEngine.mergeTemplate("template_file.vm", ctx, writer);
    String merged = writer.toString();

この過程はメンドウなので、たいていのプロジェクトでは適当なラッパーを書くことが多いのではないでしょうか。
SpringFrameworkでは、このマージ作業に関するユーティリティクラスを提供しています! ありがたいことですね。VelocityEngineUtils というクラスがそれにあたります。このクラスには、mergeTemplate メソッドと mergeTemplateIntoString メソッドがそれぞれ2つずつ用意されています。Utils というクラス接尾辞からも分かるとおり、すべて static メソッドです。
なんとなくコンポーネントとして扱えるようにしておきたいなぁということで、以下のようなラッパーを書いてみます。

package your.service.domain;

import java.io.Writer;
import java.util.Map;
import org.apache.velocity.app.VelocityEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ui.velocity.VelocityEngineUtils;

/**
 * Velocity テンプレートにパラメータをマージするクラスです。
 */
@Component
public class VelocityMerger {
    
    @Autowired
    VelocityEngine engine;
    
    /**
     * Velocity テンプレートにパラメータをマージした結果を得ます。
     * @param templateName テンプレート名
     * @param model パラメータを保持するMap
     * @return マージ結果の文字列
     */
    public String merge(String templateName, Map<String, Object> model) {
        return VelocityEngineUtils.mergeTemplateIntoString(engine, templateName, model);
    }
    
    /**
     * Velocity テンプレートにパラメータをマージし、結果を Writer に書き出します。
     * @param templateName テンプレート名
     * @param model パラメータを保持するMap
     * @param writer マージ結果を書き出す Writer
     */
    public void mergeToWriter(String templateName, Map<String, Object> model, Writer writer) {
        VelocityEngineUtils.mergeTemplate(engine, templateName, model, writer);
    }
}

至極単純なラッパーですね。実際の呼び出しコードは以下のようになります。

    @Autowired
    VelocityMerger merger;

    public String doAction() {
        ...
        Map<String, Object> model = new HashMap<String, Object>();
        ...
        String merged = merger.merge("example.vm", model);
        ...
    }

これで、以下の2つの本質的な作業だけに集中できるようになりました。

  • テンプレートエンジンに渡すパラメータの準備
  • マージ処理の実行

また、VelocityMerger クラスの上位に Merger インターフェースを作成して、そこに merge メソッドや mergeToWriter メソッドを持たせるようにすれば、後から別のテンプレートエンジンを追加することも比較的簡単にできそうです。たとえば、FreeMarker で実装された FreeMarkerMerger を作りたい場合でも、Merger の呼び出し元を変更する必要がなくなります。

まとめ

Velocity を SpringFramework 上のコンポーネントとして取り扱う方法について述べました。

  • SpringFramework と Velocity の統合には、VelocityEngineFactoryBean を使う
  • SpringFramework の設定ファイルに、Velocity の設定情報を統合できる
  • ラッパーを書くことで、さらに抽象度を上げることもできそう