【書評】「パーフェクトJava EE」は最強のJava EE 7リファレンス!
https://www.amazon.co.jp/パーフェクト-Java-EE-井上-誠一郎/dp/4774183164www.amazon.co.jpgihyo.jp
著者の1人である@kikutaro_さんから献本いただきました。ありがとうございます!
これまでのJava EE書籍の課題
まずは、Java EEの最新バージョンである「Java EE 7」対応の日本語書籍が少ないことでした。
なので、下記のEE 5本やEE 6本で勉強した後は、ブログ情報を検索したり、海外の英語情報に頼るしかありませんでした。
https://www.amazon.co.jp/マスタリングJavaEE5-第2版-DVD付-Programmer’s-SELECTION/dp/4798120545
2014年暮れあたりから、徐々にEE 7対応の書籍も出始めました(下記)。
これらはそれぞれ非常に良い本なのですが、3冊ともチュートリアル形式で、1つのアプリケーションを作っていく中でJava EEの機能を学習していくというスタイルの本です。
なので、初期学習には非常に良いのですが、「困った時に頼りになるリファレンス」というような本ではありません。リファレンスとなるような本は「マスタリングJava EE 5」しか無い、というのが現状でした。
待望の「Java EE 7で困った時に頼りになるリファレンス」が登場!
今回登場した「パーフェクトJava EE」は、待望の「リファレンスとして使える本」です。
内容はJSF・JAX-RS・CDI・JPAが中心です(WebSocket・Bean Validation・JTA・EJBも1章ずつ解説があります)。つまりJava EEのWebプロファイルですね。
それぞれがその技術のエキスパートによって書かれています(JSFは菊田さん、JAX-RSは井上さん、CDIは上妻さん、JPAは槙さん)。この技術だったらこの方だろう、という人ばかりですね。
内容の幅も深さも、他の書籍とは一線を画しています。
例えばWebSocketの章では、エンコーダー・デコーダーを利用してJSON形式でデータをやり取りする方法が紹介されています(WebSocketってチャットアプリ作っておしまい、みたいな解説が多いですよね...)。
その他、CDIのクライアントプロキシ、JAX-RSのMessageBodyReader/Writerの性質、JSFのPrimeFaces、JPAのエンティティグラフやL2キャッシュなど。
その他にも重要な内容がたくさん書かれていますので、ぜひ読んでみてください。Java EEを開発で使っているすべての方必見の内容が詰まっています!
かなり高度な内容も書かれていますので、基礎知識なしで読むのはハードルが高いかもしれません。なので、上記3冊のチュートリアル形式の本をどれでもいいので読了してから、パーフェクトJava EEを読むことをお勧めします。
ちょっと気になった点
FacesContextのインジェクト(P.36)
@Inject
FacesContext facesContext;
このように書けるようになるのは、確かJava EE 8からだったと記憶しています。
Payara Web ML 4.1.1.162で上記のコードを試しましたが、デプロイ時にエラーになりました。
セッションIDの変更(P.352)
セッションIDを変更するためにHttpSessionの破棄および再生成を行っていますが、下記のようにHttpServletRequest#changeSessionId()メソッドを利用すれば、再生成の必要は無いと思います。
HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance() .getExternalContext().getRequest(); request.changeSessionId();
レルムの説明が少ない
レルムでの認証・認可は、サーブレットの章でサラッと数ページで説明されているだけでした。
レルムはあんまり使わないよ、というメッセージなのでしょうか?深読みし過ぎ?
最後に
著者の井上さん・槙さん・上妻さん・菊田さんには、本当に感謝の言葉しかありません。
これだけの質と量の書籍を書くという作業は、本当に大変だったと思います。
とても貴重な書籍を書いていただき、ありがとうございました!
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
で指定したビーン名を指定します。
今回は@Named
のvalue
属性をしていていないので、「クラス名の頭文字を小文字にした名前」がビーン名になります。
まとめ
今回の方法は「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に、一通りの機能が説明されています。
JJUG CCC 2016 SpringでアクションベースMVCの発表してきた&楽しんできた #jjug_ccc
発表してきた
「ネクストStruts/Seasar2としてのJava EEアクションベースMVC入門」というタイトルで発表してきました。
165人の部屋が、ほぼ満席となるくらいの方々にお越しいただきました。ありがとうございます。
やっぱり、StrutsやSeasar2からの移行をどうするかは、皆さん喫緊の課題だと思います。
内容はJava EE 8のMVC 1.0、そしてEE 7における代替としてJersey MVCとRESTEasy HTMLです。
ビューは頑張ってThymeleafをリリースされたばかりの「3」にしたり、
クライアント側のロケールに合わせて画面のメッセージや検証エラーメッセージを国際化したりしました。
MVC 1.0のサンプルのみ、検証エラーメッセージの国際化には対応していません。現在、MVC 1.0の仕様策定で検討中のためです。
資料
コード
楽しんできた
テスト自動化のまわりみち(@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さん、ありがとうございます!
EclipseLinkとHibernateではTemporalType.DATEなフィールドの型が違う
かなり久々の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.Date
はjava.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)がとても簡単な件
先日、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を使おう!
この記事について
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