読者です 読者をやめる 読者になる 読者になる

Java EE 事始め!

主にJava EEについて、つらつらとマイペースに書いていきます。「Java EEを勉強するときに、一番最初に読んでもらえるブログ」を目指して頑張ります!

Thymeleafのビューから@NamedなCDI管理ビーンにアクセスする

やりたいこと

こんなCDI管理ビーンがあって、

@Named
@SessionScoped
public class SessionDto implements Serializable {

    private String id;

    public String getId() {
        return id;
    }
}

Thymeleafのビューからこんな感じで参照したいです。

<p th:text="${sessionDto.id}">...</p>

本当は上記みたいにやりたかったのですが、現時点では、下記のような感じで参照することができました。

<p th:text="${#cdi.bean('sessionDto').id}">...</p>

以下、やり方を解説します。

名前からCDI管理ビーンを取得するクラスの作成

package com.example.rest.thymeleaf;

import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.CDI;
import java.util.Set;

public class Cdi {

    public Object bean(String name) {
        CDI<Object> cdi = CDI.current();
        BeanManager beanManager = cdi.getBeanManager();
        Set<Bean<?>> beans = beanManager.getBeans(name);
        Bean<?> bean = beanManager.resolve(beans);
        Class<?> beanClass = bean.getBeanClass();
        return cdi.select(beanClass).get();
    }
}

引数のnameをもとに、ビーンを取得するメソッドを作ります。

まずBeanManagerを取得して、名前からSet<Bean<?>>を取得します。

名前だけではビーンを1つに特定できない(セッションスコープとかだとユーザー数だけ同じ名前のビーンがあるため)ので、resolve()でビーンを1つに特定します。

そこからビーンのClassオブジェクトを取得し、cdi.select(beanClass).get()でようやっと目的のCDI管理ビーンそのもののインスタンスを取得できます。

何か効率的ではないような感じがするので、今後変更するかもしれません。

参考資料

羽生田さんのスライド 3.Java EE7 徹底入門 CDI&EJB

かずひらさんのブログ CDIのBeanManagerを使う - CLOVER

CDI管理ビーンを参照するDialectを自作する

ThymeleafのDialectを自作します。

まず、IExpressionObjectFactory実装クラスを作成します。

この中で、先ほど作成したCdiクラスをnewしています。 ビューからは、getAllExpressionObjectNames()で返している名前でアクセスできます。

package com.example.rest.thymeleaf;

import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.expression.IExpressionObjectFactory;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class CdiExpressionFactory implements IExpressionObjectFactory {

    private static final String EXPRESSION_OBJECT_NAME = "cdi";

    private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(EXPRESSION_OBJECT_NAME)));

    @Override
    public Set<String> getAllExpressionObjectNames() {
        return ALL_EXPRESSION_OBJECT_NAMES;
    }

    @Override
    public Object buildObject(IExpressionContext context, String expressionObjectName) {
        if (EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) {
            return new Cdi();
        }
        return null;
    }

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

次に、Dialectクラスを作成します。

IDialectというインタフェースがあり、そのサブインタフェースとして下記の5つがあります。

  • IProcessorDialect
  • IPreProcessorDialect
  • IPostProcessorDialect
  • IExpressionObjectDialect
  • IExecutionAttributeDialect

今回は、IExpressionObjectDialectを使いました。他の4つはまだ試せていません...(^^;

package com.example.rest.thymeleaf;

import org.thymeleaf.dialect.AbstractDialect;
import org.thymeleaf.dialect.IExpressionObjectDialect;
import org.thymeleaf.expression.IExpressionObjectFactory;

public class CdiDialect extends AbstractDialect implements IExpressionObjectDialect {

    private static final IExpressionObjectFactory EXPRESSION_OBJECT_FACTORY = new CdiExpressionFactory();

    public CdiDialect() {
        super("cdi");
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() {
        return EXPRESSION_OBJECT_FACTORY;
    }
}

参考資料

Thymeleafのドキュメント Tutorial: Extending Thymeleaf

Thymeleafのソースコード

自作Dialectの追加

TemplateEngineをnewしている場所で、自作Dialectを追加します。

templateEngine = new TemplateEngine();
templateEngine.addDialect(new CdiDialect());

ビューの作成

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
      ...
<p th:text="${#cdi.bean('sessionDto').id}">...</p>

#cdi.bean()で自作したCdiクラスのbean()メソッドを呼び出しています。

引数には@Namedで指定したビーン名を指定します。

今回は@Namedvalue属性をしていていないので、「クラス名の頭文字を小文字にした名前」がビーン名になります。

まとめ

今回の方法は「Expression Object」(#で始まるやつ)を使ってみました。

この方法は非常に簡単でいいですね。他の方法も試してみたいです。

コードはこちら。

https://github.com/MasatoshiTada/jjug-action-based-mvc/tree/master/jjug-my-mvc

ThymeleafでJava SE 8のDate and Time APIを使う方法

やりたいこと

Thymeleafのビューで、java.time.LocalDateなどJava SE 8で入った日時クラス(Date and Time API)を使いたい。

Date and Time APIについてはこちらをどうぞ。

Java日付時刻APIメモ(Hishidama's Java8 Date and Time API Memo)

(蓮沼さんの資料は何故か見つからなかった・・・)

Thymeleafの拡張機能を追加

Thymeleafには、Date and Time APIを利用するための拡張機能があります。

https://github.com/thymeleaf/thymeleaf-extras-java8time

本体に入っていないのは恐らく、Thymeleaf本体はJava SE 6でコンパイルされているからだと思われます。

thymeleaf/CONTRIBUTING.markdown at 3.0-master · thymeleaf/thymeleaf · GitHub

pom.xmlに依存性を追加します。

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
    <version>3.0.0.RELEASE</version>
    <scope>compile</scope>
</dependency>

Dialectの追加

TemplateEngineをnewしている場所で、Java8TimeDialectを追加します。

templateEngine = new TemplateEngine();
templateEngine.addDialect(new Java8TimeDialect());

ビューからの利用

こんなクラスがあって、

public class Employee {
    ...
    private java.time.LocalDate joinedDate;
    // getter/setter
}

ThymeleafでjoinedDateをフォーマットして表示するには、下記のようにします。

<tr th:object="${employee}">
    ...
    <td th:text="*{#temporals.format(joinedDate, 'yyyy-MM-dd')}">2020-01-01</td>

簡単ですね!

その他の機能

こちらのREADMEに、一通りの機能が説明されています。

https://github.com/thymeleaf/thymeleaf-extras-java8time

JJUG CCC 2016 SpringでアクションベースMVCの発表してきた&楽しんできた #jjug_ccc

発表してきた

「ネクストStruts/Seasar2としてのJava EEアクションベースMVC入門」というタイトルで発表してきました。

165人の部屋が、ほぼ満席となるくらいの方々にお越しいただきました。ありがとうございます。

やっぱり、StrutsSeasar2からの移行をどうするかは、皆さん喫緊の課題だと思います。

内容はJava EE 8のMVC 1.0、そしてEE 7における代替としてJersey MVCとRESTEasy HTMLです。

ビューは頑張ってThymeleafをリリースされたばかりの「3」にしたり、

クライアント側のロケールに合わせて画面のメッセージや検証エラーメッセージを国際化したりしました。

MVC 1.0のサンプルのみ、検証エラーメッセージの国際化には対応していません。現在、MVC 1.0の仕様策定で検討中のためです。

資料

コード

GitHub - MasatoshiTada/jjug-action-based-mvc: JJUG CCC 2016 Spring「ネクストStruts/Searar2としてのJava EEアクションベースMVC入門」のサンプル

楽しんできた

テスト自動化のまわりみち(@irofさん)

テスト自動化などの前に、テスト技法なりテスト項目書の書き方なり、基本的なことをきちんとやりましょう、ということだと理解しました。

例えば、「◯◯が正しいこと」ではなく「◯◯の値が『10』になっていること」と書くとか。

「正しいこと」なんて、そりゃJUnitで表現できませんよね・・・。

テスト技法の研修などもやったりする身としては、身が引き締まる思いでした。

Thymeleaf 3を使ってみよう!(@bufferingsさん)

Thymeleaf公式ドキュメントの和訳も出されている椎葉さんのセッションでした。

アクションベースMVCのサンプルを作る時に一通りドキュメントを読んだので、概ね知っている内容が多かったのですが、

初めて知ったことや、いまひとつ理解できていなかったことが理解できたことも多かったです。

さらに、椎葉さんは実業務でもThymeleafを使っていらっしゃるということで、デザイナーさんとの協業の話も非常に参考になりました。

Spring BootでBootした後に作るWebアプリケーション基盤(エムスリー吉田さん)

Spring Boot + Spring MVCの実用的な使い方のお話でした。

特に例外処理やロギングなどのお話が印象的でした。スライドはアップされないのかな?

Spring Framework/Bootの最新情報とPivotalが進めるクラウドネイティブなアプローチ(@makingさん)

最初に、Spring Boot 1.4およびSpring Framework 4.3の新機能のお話でした。

テスト関連の機能が特に印象的でした。テストしやすいフレームワークっていいよなあ・・・。Java EEにも頑張ってほしい。

後半は、Cloud FoundryやConcourse CIなどの紹介をしつつ、「クラウドネイティブ」なアプリケーションとは何か、

それをCloud Foundryではどう実現しているか、というお話でした。

以前にCloud Foundryワークショップにも参加したので、もっと触れていきたいなあと思います。

Seasar2で作った俺たちのサービスの今(@jukutyoさん)

Seasar2(Cubby + S2Dao)からSpring MVC + Domaへ移行するお話でした。

段階的に移行していて、今は同一WARファイルの中にSpringとSeasar2が同居している状態だそうです。

確かに、一度にまるっと移行することは、人的・時間的リソースや、サービスを維持・発展させなければならない、

といった制約もあって現実的でないこともあると思いますので、こういった実例を知れたのは非常に良かったです。

余談ですが、じゅくちょーさんは元塾講師ということで、塾講師っぽい喋り方が印象的でした。

僕も学生時代は塾講師のバイトをずっとしていたので、なんだか懐かしい気持ちでしたw

マイクロフレームワークenkan(とkotowari)ではじめるREPL駆動開発(@kawasimaさん)

システムエンジニアアドベントカレンダーで世の中を驚かせた、川島さんの発表でした。

川島さんとしては色々な問題意識をお持ちで、このenkanを作るに至ったそうです。

確かに、XMLアノテーションになっても、設定が面倒なのはあるし、CoCと分かりやすさはトレードオフです。

最後の方はチュートリアルをやってみる時間がありましたので、やってみました。

最初にMavenでのJARのダウンロード祭りがありますが、それでも5分くらいあれば、このチュートリアルは終えられました。

運営について

今回から昼休みが90分になったり、午後にも2セッションごとに40分の休憩があったり、

懇親会の開始時間が早まったり、新たな取り組みがありました。

特に午後の長めの休憩は、非常に嬉しかったです。

10分休憩だけで午後に6セッションとか7セッションとか続くと、かなり体力的にキツかったですし、

休憩時間が短いとセッション間の移動も混雑して大変でした。

所々に長めの休憩があることで、体力も回復できるし、余裕を持って移動できるし、

いろんな方とお話しすることもできるし、個人的にはいいことずくめでした。

幹事やボランティアスタッフの皆様、ありがとうございました!

おまけ

このタイムテーブル、とても役に立ちました!

@YujiSoftwareさん、ありがとうございます!

http://yujisoftware.github.io/jjug-ccc/2016-Spring/

EclipseLinkとHibernateではTemporalType.DATEなフィールドの型が違う

Java EE 7 GlassFish Payara JPA WildFly

かなり久々のJPAネタ。

こんなエンティティクラスがあって、

@Entity
public class Employee implements Serializable {
    @Id
    @Column(name = "emp_id")
    private Integer empId;
    
    @Temporal(TemporalType.DATE)
    @Column(name = "joined_date")
    private java.util.Date joinedDate;
    

joinedDateというフィールドはjava.util.Date型、そして@Temporal(TemporalType.DATE)が付いています。

これをEclipseLinkで実行すると、joinedDateにはjava.util.Date型のインスタンスが代入される。

一方、Hibernateで実行すると、joinedDateにはjava.sql.Date型のインスタンスが代入される。(java.sql.Datejava.util.Dateのサブクラスです)

Webアプリのビューでそのまま表示すると、Payara(JPA実装がEclipseLink内包)だと「Wed Apr 01 00:00:00 JST 2015」という形式なのに、

WildFly(JPA実装がHibernate)だと「2015-04-01」という形式になったので、アレ?と思って、

employee.getJoinedDate().getClass().getName()してクラス名を表示したらこんな感じになってました。

jug-action-based-mvcプロジェクトにテストコードを追加しました。

EmployeeDaoのテストコードを追加 · MasatoshiTada/jjug-action-based-mvc@2edd12b · GitHub

Payara Microの実行可能JAR(Uber JAR)がとても簡単な件

Java EE 7 Payara

先日、Payara 4.1.1.162がリリースされました!

その組み込みサーバー版であるPayara Micro 4.1.1.162には、「Uber JAR」というSpring BootやWildFly Swarmのような単体で実行可能なJARを作る機能が追加されました。

Payara 4.1.1.162 がリリースされました - GlassFish Japan

従来のPayara Microでは、一旦アプリケーションを普通にWARにして、Payara Micro起動→WARをデプロイする必要がありました。

今回の新機能により、本当に単体で実行可能なJARを作れるように

手順は蓮沼さんのブログに書かれてある通りなのですが、自分でもやってみてあまりに簡単で感動したので、このブログにも書くことにしました。

手順

普通にWebアプリケーションを作る

本当に普通に作ってください。pom.xmlのpackageは「war」です。

組み込みじゃない据え置きのGlassFish/Payaraにデプロイするアプリケーションと全く同じ形で作ってください。

下記は、今月のJJUG CCC 2016 Springで紹介する予定の、MVC 1.0のサンプルアプリケーションです。

アプリケーション側は全く変えることなく、据え置きサーバー・実行可能JARの両方に対応できます。

jjug-action-based-mvc/jjug-mvc10 at master · MasatoshiTada/jjug-action-based-mvc · GitHub

そして、MavenでWARファイルにビルドします。

cd jug-mvc10
mvn clean package

jjug-mvc10/targetフォルダにjjug-mvc10-1.0-SNAPSHOT.warが作成されます。

実行可能JARを作成する

上記のWARファイルを元に、Payara Microで実行可能JARを作ります。

あらかじめ、Payara Micro 4.1.1.162のJARは下記からダウンロードし、適当なフォルダに保存してください。

Payara Server & Payara Micro - Downloads

そして、次のコマンドを実行します。

java -jar ~/Java/ap-server/payara-micro-4.1.1.162.jar\
 --deploy target/jjug-mvc10-1.0-SNAPSHOT.war\
 --outputUberJar target/jjug-mvc10.jar

--outputUberJarというオプションがポイントです。

こうすると、targetフォルダにjjug-mvc10.jarというJARファイルが作成されます。これが実行可能JARです。

ちなみに、このオプションがない場合、これまでのPayara Microと同じ挙動になります。

-jar の後は先ほどダウンロードしたPayara MicroのJARファイルのパス、--deployの後はWARファイルのパスです。

ちなみに、jar tf target/jjug-mvc10.jarとすると、実行可能JARの中身を確認することができます。

とても長いので全部は載せませんが、Payara Microの中身が解凍・再パッケージされていることが確認できます。

META-INF/services/javax.enterprise.inject.spi.CDIProvider
META-INF/services/org.glassfish.tyrus.core.ComponentProvider
META-INF/services/javax.management.remote.JMXConnectorProvider
(中略)
META-INF/
META-INF/MANIFEST.MF
__cp_jdbc_ra.rar
__dm_jdbc_ra.rar
__ds_jdbc_ra.rar
__xa_jdbc_ra.rar
META-INF/configuration/
META-INF/hk2-locator/
META-INF/maven/
META-INF/maven/org.glassfish.main.concurrent/
META-INF/maven/org.glassfish.main.concurrent/concurrent-connector/
org/
org/glassfish/
org/glassfish/concurrent/
org/glassfish/concurrent/config/
META-INF/configuration/context-service-conf.xml
META-INF/configuration/managed-executor-service-conf.xml
META-INF/configuration/managed-scheduled-executor-service-conf.xml
META-INF/configuration/managed-thread-factory-conf.xml
META-INF/maven/org.glassfish.main.concurrent/concurrent-connector/pom.properties
META-INF/maven/org.glassfish.main.concurrent/concurrent-connector/pom.xml
org/glassfish/concurrent/config/ConcurrencyResource.class
org/glassfish/concurrent/config/ContextService$ContextServiceConfigActivator.class
org/glassfish/concurrent/config/ContextService$Duck.class
org/glassfish/concurrent/config/ContextService.class
org/glassfish/concurrent/config/ContextServiceInjector.class
org/glassfish/concurrent/config/LocalStrings.properties
(以下略)

一番最後の方に、自分で作ったWARファイルが見えます。

...
META-INF/deploy/
META-INF/deploy/jjug-jersey-mvc-1.0-SNAPSHOT.war
META-INF/deploy/payaramicro.properties

META-INF/deployに直接WARファイルが入ってるんですね!

実行する

java -jar target/jjug-mvc10.jar

これで実行できます。

後はブラウザで開いてださい。

http://localhost:8080/jjug-mvc10-1.0-SNAPSHOT/

ちなみに、僕はここまでの処理は1つのシェルスクリプトにまとめて、実行する時はそれを叩くだけにしています。

jjug-action-based-mvc/build-jar.sh at master · MasatoshiTada/jjug-action-based-mvc · GitHub

まとめ

WARに固めてから実行可能JARにする、というのが少し違和感があるかもしれませんが、アプリケーション側に何も変更なく実行可能JARが出来るというのは非常に面白いと感じました。

ぜひご自分でも試してみてください!

Jersey MVCでThymeleafを使おう!

Java EE 7 JAX-RS 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を付加します。

次に、フィールドインジェクションでHttpServletRequestHttpServletResponseを取得するのですが、@Providerが付加されたクラスはJAX-RSの仕様では、デフォルトでシングルトンと定められています。

なので、直接HttpServletRequestHttpServletResponseをインジェクションすると、スレッドセーフでない可能性があると考えました。

僕はスレッドセーフか否かといったあたりはあまり詳しくないので、直接取得でも大丈夫な可能性もありますが・・・。

そこで、Jersey MVCJSPTemplateProcessorである下記のクラスを参考に作りました。

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

Jersey MVC + Thymeleafしたら文字化けた時の対策

Jersey MVC Thymeleaf

環境

  • OS X 10.11.4
  • Google Chrome
  • Payara Web ML 4.1.1.161.1 (Jersey 2.22.1)
  • Thymeleaf 2.1.4.RELEASE

プログラム概要

@bufferingさんの記事を参考に、Thymeleaf用のTemplateProcessorを作成。

@Provider
public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    @Inject
    private javax.inject.Provider<Ref<HttpServletRequest>> requestProviderRef;
    @Inject
    private javax.inject.Provider<Ref<HttpServletResponse>> responseProviderRef;

    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));
        templateResolver.setSuffix(".html");
        templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
    }

    @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();
        WebContext webContext = new WebContext(
                httpServletRequest, httpServletResponse, 
                super.getServletContext(), httpServletRequest.getLocale());
        Object model = viewable.getModel();
        if (model instanceof Map) {
            Map<String, Object> map = (Map) model;
            webContext.setVariables(map);
        } else {
            Map<String, Object> variables = new HashMap<>();
            variables.put("model", viewable.getModel());
            webContext.setVariables(variables);
        }
        templateEngine.process(viewable.getTemplateName(), webContext, 
                httpServletResponse.getWriter());
    }

}

リソースクラス。

@Path("employee")
@RequestScoped
@Produces(MediaType.TEXT_HTML)
public class EmployeeResource {
    
    @GET
    @Path("index")
    public Viewable index() throws Exception {
        return new Viewable("/employee/index");
    }
}

Thymeleafのビュー。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>入力画面</title>
    <link rel="stylesheet" href="../../css/style.css"/>
</head>
<body>
<p>社員IDを入力してください(77, 88, 99で例外発生)</p>

<!-- 検証エラーメッセージの表示 -->
<ul class="error" th:if="${violations != null}">
    <li th:each="violation : ${violations}" th:text="${violation.message}">ダミーのメッセージ</li>
</ul>

<form action="./result" method="get">
    社員ID:<input type="text" name="id" value="" th:value="${param.id == null} ? '' : ${param.id[0]}"/>
    <input type="submit" value="検索"/>
</form>
</body>
</html>

現象

画面の全日本語メッセージが文字化けている。

f:id:MasatoshiTada:20160501145927p:plain

原因と対策

レスポンスヘッダーやブラウザが現在開いている文字コードを見ると、正しくUTF-8になっていた。

よって、そもそもレスポンスを書き込む際に文字コードが正しく設定されていないっぽい。

TemplateProcessorにhttpServletResponse.setCharacterEncoding("UTF-8");を追加したら、正しく表示された。

f:id:MasatoshiTada:20160501150201p:plain

@Provider
public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    // 中略
    
    @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> map = (Map) model;
            webContext.setVariables(map);
        } else {
            Map<String, Object> variables = new HashMap<>();
            variables.put("model", viewable.getModel());
            webContext.setVariables(variables);
        }
        templateEngine.process(viewable.getTemplateName(), webContext, 
                httpServletResponse.getWriter());
    }

}

ちなみに

Ozark + Thymeleafだと、この記述が無くても正しく表示された。

(ViewEngineクラスにもそれらしい記述は無い)

理由は不明。。。