Jersey MVCでThymeleafを使おう!
この記事について
JJUG CCC 2016 Spring「ネクストStruts/Seasr2としてのJava EEアクションベースMVC入門」の補足記事です。
Jersey MVCで、JSPの代わりのビューとしてThymeleafを使う方法を解説します。
Jersey MVCの基本については、下記の記事を参照してください。
Java EE 7でもアクションベースMVC! -MVC 1.0への移行を睨んだJersey MVCの活用- #javaee - Java EE 事始め!
環境
- Payara Web ML 4.1.1.162
- Thymeleaf 2.1.4.RELEASE
ソースコード
jjug-action-based-mvc/jjug-jersey-mvc at master · MasatoshiTada/jjug-action-based-mvc · GitHub
TemplateProcessorクラスを作る
Jersey MVCで公式にサポートされているビューはJSP/Freemarker/Mustacheの3種類です。
それ以外のビューを使うには、Jersey MVCで提供されているTemplateProcessor
インタフェースを実装する必要があります。
Jersey MVCには、TemplateProcessor
を実装したAbstractTemplateProcessor
という抽象クラスがありますので、これを継承して作成します。
クラスの作成、リクエストとレスポンスの取得
@Provider public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> { @Inject private javax.inject.Provider<Ref<HttpServletRequest>> requestProviderRef; @Inject private javax.inject.Provider<Ref<HttpServletResponse>> responseProviderRef; // 後に続く
まず、AbstractTemplateProcessor
を継承し、クラスには@Provider
を付加します。
次に、フィールドインジェクションでHttpServletRequest
とHttpServletResponse
を取得するのですが、@Provider
が付加されたクラスはJAX-RSの仕様では、デフォルトでシングルトンと定められています。
なので、直接HttpServletRequest
とHttpServletResponse
をインジェクションすると、スレッドセーフでない可能性があると考えました。
僕はスレッドセーフか否かといったあたりはあまり詳しくないので、直接取得でも大丈夫な可能性もありますが・・・。
そこで、Jersey MVCのJSP用TemplateProcessor
である下記のクラスを参考に作りました。
jersey/JspTemplateProcessor.java at 2.x · jersey/jersey · GitHub
これで本当にスレッドセーフになっているのかは、僕には判断がつかないので、この点はご自分で判断してください。
TemplateEngineの作成
// 続き private TemplateEngine templateEngine; @Inject public ThymeleafTemplateProcessor(Configuration config, ServletContext servletContext) { super(config, servletContext, "html", "html"); TemplateResolver templateResolver = new ServletContextTemplateResolver(); templateResolver.setPrefix((String) config.getProperty(MvcFeature.TEMPLATE_BASE_PATH)); // setSuffix()は指定しない templateEngine = new TemplateEngine(); templateEngine.setTemplateResolver(templateResolver); } // 後に続く
フィールドにTemplateEngine
(Thymeleafのクラス)を宣言し、コンストラクタ内で初期化します。
ポイントは、setSuffix()
という、通常は拡張子を指定するメソッドを呼んでいないことです。
これは、MVC 1.0との互換性のために、コントローラーメソッドで拡張子を指定したいからです。
各メソッドのオーバーライド
// 続き @Override protected String resolve(String templatePath, Reader reader) throws Exception { return templatePath; } @Override public void writeTo(String templateReference, Viewable viewable, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException { HttpServletRequest httpServletRequest = requestProviderRef.get().get(); HttpServletResponse httpServletResponse = responseProviderRef.get().get(); httpServletResponse.setCharacterEncoding("UTF-8"); WebContext webContext = new WebContext( httpServletRequest, httpServletResponse, super.getServletContext(), httpServletRequest.getLocale()); Object model = viewable.getModel(); if (model instanceof Map) { Map<String, Object> variables = (Map<String, Object>) model; webContext.setVariables(variables); } else { Map<String, Object> variables = new HashMap<>(); variables.put("model", model); webContext.setVariables(variables); } try (Writer writer = new OutputStreamWriter(out)) { templateEngine.process(viewable.getTemplateName(), webContext, writer); } } }
resolve()
は、引数のtemplatePath
をそのまま返せばOKです。
writeTo()
が、最終的にレスポンスを書き込むメソッドです。
webContext.setVariables()
は、ビューに渡す値をマップ形式で指定します。
コントローラーメソッドでViewable
に渡された値は、viewable.getModel()
で取得できます。
この値がMapだった場合は、それを直接webContext.setVariables()
に渡し、そうでない場合はJersey MVCデフォルトのmodel
という名前でMapにputしています。
こうすることで、ビューから値を参照するときに、${model.employee.id}
のようにmodel
を付けなくてもいいようにしています。
writeTo()
の最後では、templateEngine.process()
を呼んでいます。これが、Thymeleafのレスポンスを書き出しています。
自作Viewableクラスの作成
これはThymeleafを使う上では必須ではないのですが、MVC 1.0との差異をなるべく吸収するために作りました。
前述のブログの通り、Jersey MVCには「/」で始める絶対パスと「/」で始めない相対パスがあり、それぞれビューの保存フォルダが異なります。
ビューの保存フォルダがMVC 1.0とほぼ同じなのは絶対パスですが、MVC 1.0(というか現在のOzark)では「/」で始めるとコンテキストルートからのパスになってしまいます。
なので、「/」で始めない相対パスで指定しつつ、かつビューの保存フォルダが絶対パスになるように、Viewable
のサブクラスを自作しました。
public class ThymeleafViewable extends Viewable { public ThymeleafViewable(String templateName) throws IllegalArgumentException { super(templateName); } public ThymeleafViewable(String templateName, Map<String, Object> models) throws IllegalArgumentException { super(templateName, models); } @Override public Map<String, Object> getModel() { return (Map<String, Object>) super.getModel(); } @Override public String getTemplateName() { String templateName = super.getTemplateName(); if (templateName.startsWith("/") == false) { return "/" + templateName; } else { return templateName; } } @Override public boolean isTemplateNameAbsolute() { return true; } }
コードを読んでいただければ大体わかると思いますが、getTemplateName()
が必ずスラッシュで始まるパス(=絶対パス)を返すようにし、かつisTemplateNameAbsolute()
でtrue
を返して、Jersey MVCに絶対パスと認識させます。
また、コンストラクタはビューのパスとMapを引数で受け取るようにします。
コントローラーメソッド
@GET @Path("index") public ThymeleafViewable index() throws Exception { return new ThymeleafViewable("employee/index.html"); }
@GET @Path("result") public ThymeleafViewable result(@BeanParam EmployeeIdForm form) throws Exception { Integer id = Integer.valueOf(form.getId()); Employee employee = employeeService.findByEmpId(id).orElse(null); HashMap<String, Object> models = new HashMap<>(); models.put("employee", employee); return new ThymeleafViewable("employee/result.html", models); }
パスは「/」で始めない、MVC 1.0と同じ形式にしています。
また、拡張子「.html」を明示的に指定しています。これもMVC 1.0と同じです。
ビュー
<p th:if="${employee == null}">該当する社員はいませんでした。</p> <table border="1" th:unless="${employee == null}"> <tr><th>社員ID</th><th>氏名</th><th>入社年月日</th><th>部署ID</th><th>部署名</th></tr> <tr th:object="${employee}"> <td th:text="*{empId}">99999</td> <td th:text="*{name}">Taro Yamada</td> <td th:text="*{joinedDate}">2020-01-01</td> <td th:text="*{department.deptId}">99</td> <td th:text="*{department.name}">Admin</td> </tr> </table>
model
を指定せず、直接employee
で値を参照していることが分かります。
これにより、Jersey MVCからMVC 1.0に変更した際に、ビューを全く変更しなくて済むようにしています。
まとめ
TemplateProcessor
と自作Viewable
により、極力MVC 1.0と同じ感覚で、ビューとコントローラーを実装できるようにしました。
参考にしたWebサイト
@bufferingsさんのブログ。
jersey-thymeleaf using ViewProcessor - Mitsuyuki.Shiiba
@backpaper0さんのコード。
sealion/ThymeleafProvider.java at master · backpaper0/sealion · GitHub