thymeleaf3でDialectを実装する

thymeleaf3でDialectを実装したメモです。

thymeleaf3からDialectのインターフェースが新しくなったので(IDialect)実装してみました。

Thymeleaf 3 ten-minute migration guide - Thymeleaf

Maven dependency

現時点のspring-boot-starter-thymeleafでは、最新版でもthymeleafの依存関係は2系なので、
3系を使いたい場合、下記の2つのプロパティをオーバーライドします。

  • thymeleaf.version
  • thymeleaf-layout-dialect.version

pom.xml

    <properties>
        <thymeleaf.version>3.0.3.RELEASE</thymeleaf.version>
        <thymeleaf-layout-dialect.version>2.2.1</thymeleaf-layout-dialect.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

Configuration

thymeleaf3用の設定を行います。
(1) ApplicationContextAwareを実装して、ApplicationContextの参照を保持します。
(2) ViewResolverとTemplateEngineをBean定義します。
(3) 今回実装するDialectをコンストラクタでインジェクションします。
(4) engine.addDialect(myExpressionObjectDialect)で実装するDialectを追加します。

ThymeleafConfig.java

@Configuration
@EnableWebMvc
public class ThymeleafConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware {

    private MyExpressionObjectDialect myExpressionObjectDialect;

    private ApplicationContext applicationContext; // (1)

    // (3)
    public ThymeleafConfig(MyExpressionObjectDialect myExpressionObjectDialect) {
        this.myExpressionObjectDialect = myExpressionObjectDialect;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // (2)
    @Bean
    public ViewResolver viewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }

    // (2)
    @Bean
    public TemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.addDialect(myExpressionObjectDialect); // (4)
        engine.setEnableSpringELCompiler(true);
        engine.setTemplateResolver(templateResolver());
        return engine;
    }

    private ITemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("/WEB-INF/templates/");
        resolver.setTemplateMode(TemplateMode.HTML);
        return resolver;
    }
}

Helper

Helperクラスとして、下記の2つのヘルパーを用意。

日付関連のヘルパー。

DateHelper.java

@Component
public class DateHelper {
    public Date now() {
        return new Date();
    }
}

文字列関連のヘルパー。

StringHelper.java

@Component
public class StringHelper {
    public String addFoo(String string) {
        return "Foo" + string;
    }
}

Dialect

上記のDateHelperとStringHelperを利用するDialectを実装します。
独自のUtility Dialectを実装するので、IExpressionObjectDialectインターフェースを実装します。
[MAJOR FEAT] New Dialect API · Issue #401 · thymeleaf/thymeleaf · GitHub

(1) IExpressionObjectDialectを実装したMyExpressionObjectDialectを作成します。
(2) IExpressionObjectDialectでは、getExpressionObjectFactory()でIExpressionObjectFactoryを返す必要があります。
  IExpressionObjectFactoryを実装したMyExpressionObjectFactoryを返しています。

MyExpressionObjectDialect.java

@Component
public class MyExpressionObjectDialect implements IExpressionObjectDialect { // (1)
    
    private MyExpressionObjectFactory myExpressionObjectFactory;

    public MyExpressionObjectDialect(MyExpressionObjectFactory myExpressionObjectFactory) {
        this.myExpressionObjectFactory = myExpressionObjectFactory;
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() { // (2)
        return myExpressionObjectFactory;
    }

    @Override
    public String getName() {
        return "MyExpressionObjectDialect";
    }
}

IExpressionObjectFactoryインターフェースは下記の3つのメソッドが定義されています。
・Object buildObject(IExpressionContext context, String expressionObjectName)
・Set getAllExpressionObjectNames()
・boolean isCacheable(String expressionObjectName)

IExpressionObjectFactoryインターフェースを実装したMyExpressionObjectFactoryを作成します。
(1) 作成したヘルパークラスをインジェクト
(2) すべてのExpressionオブジェクトのリストを返します
(3) expressionObjectNameごとに対応するExpressionを返します
(4) Expressionオブジェクトをキャッシュしないのでfalseにします

MyExpressionObjectFactory.java

@Component
public class MyExpressionObjectFactory implements IExpressionObjectFactory {
    private static final String dateExpression = "dateHelper";
    private static final String stringExpression = "strHelper";

    // (1)
    private DateHelper dateHelper;
    private StringHelper strHelper;

    public MyExpressionObjectFactory(DateHelper dateHelper, StringHelper strHelper) {
        this.dateHelper = dateHelper;
        this.strHelper = strHelper;
    }

    private static final Set<String> expressionSet = new HashSet<String>() {
        {
            add(dateExpression);
            add(stringExpression);
        }
    };

    @Override
    public Set<String> getAllExpressionObjectNames() { // (2)
        return expressionSet;
    }

    @Override
    public Object buildObject(IExpressionContext context, String expressionObjectName) { // (3)
        switch (expressionObjectName) {
            case stringExpression:
                return strHelper;
            case dateExpression:
                return dateHelper;
            default:
                return null;
        }
    }

    @Override
    public boolean isCacheable(String expressionObjectName) { // (4)
        return false;
    }
}

テンプレート

確認用のテンプレートとして下記を作成しました。

dateHelperとStrHelperを使用しています。

my.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>my test</title>
</head>
<body>
    <h2>Thymeleaf3 Test</h2>
    <h4>date helper</h4>
    <p>[[${#dateHelper.now()}]]</p>
    <h4>string helper</h4>
    <p>[[${#strHelper.addFoo('abc')}]]</p>
</body>
</html>

エンドポイント

テスト用のエンドポイントとして、下記のコントローラを追加。

MyController.java

@Controller
public class MyController {
    @RequestMapping("/my")
    public String my() {
        return "my";
    }
}

確認

Spring Bootアプリケーションを実行して、エンドポイントにアクセスしてみます。
http://localhost:8080/my

f:id:pppurple:20170322222701p:plain

きちんと表示されました。

おわり。


サンプルコードは下記にあげました。
github.com