FreeMarkerの全template共通で使用する変数をShared variablesで設定する

Shared variables

FreeMarkerの全てのテンプレート共通で使用する変数を設定するにはShared variablesを使えばいい。

マニュアルの通り以下のように設定できる。

Configuration cfg = new Configuration(Configuration.VERSION_2_3_27);
...
cfg.setSharedVariable("warp", new WarpDirective());
cfg.setSharedVariable("company", "Foo Inc.");

Spring BootからShared variablesを設定する

Spring Bootの2系ではShared variablesをスマートに設定する方法が用意されておらず、BeanPostProcessorを使ってBeanが作成される前後に処理を挟むという力技を使わなければいけない。

またSpring Bootのauto-configurationの仕組みを壊さないように、FreeMarkerのfreemarker.template.Configurationクラスを直接触らないで、SpringのFreeMarkerConfigurerクラスを通して設定変更する必要がある。

例えば環境毎に設定したapplication.propertiesの設定値template.xを変数xに@Valueで読み込んで、FreeMarkerで${x}で使えるようにするには次のようにする。

@Component
public class FreeMarkerConfigurerPostProcessor implements BeanPostProcessor {

    @Value("${template.x}")
    String x;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof FreeMarkerConfigurer) {
            FreeMarkerConfigurer configurer = (FreeMarkerConfigurer) bean;
            Map<String, Object> sharedVariables = new HashMap<>();
            sharedVariables.put("x", x);
            configurer.setFreemarkerVariables(sharedVariables);
        }
        return bean;
    }
}

Shared variablesを使わずModel#addAttributeする

Shared variablesを使わないなら、Spring Bootの全ControllerでModel#addAttributeをしなくてはならない。

共通メソッドを作って毎回呼び出すようにすると、漏れが発生するリスクがある。漏れは例えば以下のような場合に発生する。

  1. Accept: text/htmlをHTTPヘッダーにつけた上で、CSRFトークンをセットせずにPOSTする
  2. Spring SecurityがCSRFトークンの検証をNG判定する
  3. Spring Securityが/errorにフォワードする ※Acceptヘッダーがtext/htmlでなければ403レスポンスをJSONで返すが、text/htmlの場合は/errorにフォワードしてHTMLファイルを返そうとする
  4. /errorに対応するFreeMarkerのテンプレートerror.ftlhがあれば、それを元にHTMLファイルにレンダリングしようとする
  5. error.ftlhで変数が使用されていれば、ErrorControllerimplementsしたコントローラークラスに@RequestMapping("/error")のメソッドを定義し、そこでModel#addAttributeしていない限りNullPointerExceptionが発生する

Spring Securityによるエラーページへのフォワードなど、正常系で想定している範囲外にも漏れなく対応するには、@ControllerAdvice@ModelAttributeを使って全共通処理が実行されるようにする必要がある。

@ControllerAdvice
public class ModelAttributeAdvice {

    @ModelAttribute
    public void addAttributes(Model model, @Value("${template.x}" String x)  {
        model.addAttribute("x", x);
    }
}

環境

  • Java 11
  • Spring Boot 2.2.3
  • FreeMarker 2.3.29

参考

https://github.com/spring-projects/spring-boot/issues/8965#issuecomment-369845407

https://freemarker.apache.org/docs/pgui_config_sharedvariables.html