この記事について
このブログは、Java EE Advent Calendar 2015 - Qiitaの25日目(最終日)です。
昨日はhondaYoshitakaさんの「Java - JAX-RSによるExcel/CSV/PDFファイルダウンロード - Qiita」でした。
と言えば、Java EE 8で導入される、JAX-RSベースの「MVC 1.0」ですね。
MVC 1.0の詳細については、今年のGlassFish勉強会の資料をご覧ください。
Java EE 8先取り!MVC 1.0入門 [EDR2対応版] 2015-10-10更新
EE 8は2017年上半期リリース予定ですので、現在のEE 7ではMVC 1.0は使えません。
MVC 1.0の参照実装「Ozark」は既にGitHub・Maven上で公開されていますが、まだ策定途中のため仕様が変更される可能性が多々あり、学習目的以外では使わないほうがよいでしょう。
しかし、JAX-RSの参照実装「Jersey」の独自機能である「Jersey MVC」であれば、現在のEE 7でも使うことは可能です。
MVC 1.0は、このJersey MVCを参考にして作られていると言われており、実際にも似た部分が多くあります(もちろん、異なる部分もあります)。
Java EE 7の今はJersey MVCで作り、EE 8リリース後にMVC 1.0に移行するという手もアリなのではないでしょうか。
そこで今回は、EE 8でMVC 1.0に移行することを見据えた上で、EE 7とJersey MVCでどのように作るか、ということを考えていきたいと思います。
Jersey MVCは日本語ブログも結構多いのですが、Jersey MVCに必要な設定などが結構変わっているため、最新情報をまとめるという意味で、この記事を書きました。
注意点
Jersey MVCは、あくまでJerseyの独自機能であり、Java EE 7標準のものではありません。
よって、Java EEのメリットの1つである「サーバーベンダーからのサポート」は対象外になる可能性があります。
サーバーベンダーのサポートポリシーや、ご自分のプロジェクトの事情などを考慮した上で、ご利用ください。
今回の方針
- MVC 1.0の再発明はしない。(MVC 1.0はまだ仕様が確定していないため)
- Jersey MVC自体への修正も加えない。(どこか修正すると修正点が芋づる式に増えてキリがないため)
- Jersey MVCおよびJava EE 7の機能の範囲内で、MVC 1.0への移行コストがなるべく小さくなる実装を模索する。
環境
比較対象のMVC 1.0は、2015年10月に公開されたEDR2版とします。
完成版のコード
これ以降のコードは、重要な部分のみ抜き出して、一部省略しています。
完全なコードはGitHubにアップしていますので、こちらをご参照ください。
MasatoshiTada/JavaEEAdventCalendar2015-JerseyMVC · GitHub
それでは、手順を説明していきます。
Mavenでプロジェクトを作成し、pom.xmlに依存性を追加します。
<properties><javaeeversion>7.0</javaeeversion><jerseyversion>2.22.1</jerseyversion></properties><dependencyManagement><dependencies><dependency><groupId>org.glassfish.jersey</groupId><artifactId>jersey-bom</artifactId><version>${jersey.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>javax</groupId><artifactId>javaee-web-api</artifactId><version>${javaee.version}</version><scope>provided</scope></dependency><dependency><groupId>org.glassfish.jersey.ext</groupId><artifactId>jersey-mvc-jsp</artifactId><scope>provided</scope></dependency><dependency><groupId>org.glassfish.jersey.ext</groupId><artifactId>jersey-bean-validation</artifactId><scope>provided</scope></dependency><dependency><groupId>org.glassfish.jersey.ext</groupId><artifactId>jersey-mvc-bean-validation</artifactId><scope>compile</scope></dependency><dependency><groupId>org.glassfish.web</groupId><artifactId>javax.servlet.jsp.jstl</artifactId><version>1.2.4</version><scope>provided</scope></dependency></dependencies>
ほとんどの依存性はPayaraに含まれているのでprovided
ですが、jersey-mvc-bean-validationのみPayaraに含まれていないのでcompile
にしています。
設定クラスの作成
JAX-RSを有効化するためには、通常javax.ws.rs.Application
クラスのサブクラスを作成しますが、今回はJersey独自のApplication
サブクラスであるResourceConfig
クラスを継承します。
package com.example.rest;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.mvc.beanvalidation.MvcBeanValidationFeature;
import org.glassfish.jersey.server.mvc.jsp.JspMvcFeature;
import javax.ws.rs.ApplicationPath;
@ApplicationPath("api")
publicclass MyApplication extends ResourceConfig {
public MyApplication() {
register(JspMvcFeature.class);
register(MvcBeanValidationFeature.class);
property(JspMvcFeature.TEMPLATE_BASE_PATH, "/WEB-INF/views/");
packages(true, this.getClass().getPackage().getName());
}
}
Jersey MVCを利用するには、JspMvcFeature
クラスの登録が必要になります。
Application
クラスを継承しても出来なくはないのですが、全リソースクラスおよび@Provider
が付加されたクラスもすべて登録しなければならず、手間がかかります。
JAX-RSには、もともとAuto Discoveryというリソースクラスなどを自動的に登録する機能があるのですが、1つでもクラスを自前で登録してしまうと、Auto Discoveryが無効になってしまうのです。
ResourceConfig
にはpackage()
という、指定されたパッケージ名内のクラスをすべて登録するメソッドが定義されており、便利です。第1引数をtrue
にすることで、サブパッケージ内のクラスも再帰的に登録します。
コントローラークラスの作成
Jersey MVCでは、リソースメソッドの戻り値をorg.glassfish.jersey.server.mvc.Viewable
することで、リソースメソッドをコントローラーメソッドにすることができます。
package com.example.rest.resource;
import com.example.rest.dto.HelloDto;
import com.example.rest.exception.MyException;
import java.io.IOException;
import com.example.rest.exception.MyRuntimeException;
import org.glassfish.jersey.server.mvc.ErrorTemplate;
import org.glassfish.jersey.server.mvc.Viewable;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
@Path("hello")
@RequestScopedpublicclass HelloResource {
@Injectprivate HelloDto helloDto;
@GET@Path("index")
public Viewable index() {
returnnew Viewable("/hello/index.jsp");
}
@GET@Path("result")
@ErrorTemplate(name = "index")
public Viewable result(@QueryParam("name") @DefaultValue("")
@Size(message = "{name.size}", min = 1, max = 10)
@Pattern(message = "{name.pattern}", regexp = "[a-zA-Z]*")
String name) throws Exception {
switch (name) {
case"null":
thrownew NullPointerException("NULLPO!");
case"myrun":
thrownew MyRuntimeException("MyRuntime!");
case"run":
thrownew RuntimeException("Runtime!");
case"io":
thrownew IOException("IOE!");
case"myex":
thrownew MyException("MY EXCEPTION!");
case"ex":
thrownew Exception("EXCEPTION!");
}
helloDto.setMessage("Hello, " + name);
returnnew Viewable("/hello/result.jsp");
}
}
return new Viewable("/hello/index.jsp");
とすることで、index.jspにフォワードするという意味になります。(拡張子.jsp
は付けなくても動きます)。
Viewable
コンストラクタの引数に指定するJSPファイルへのパスは、「/」で始める絶対パスと、「/」で始めない相対パスの2種類があります。
まず、絶対パスと相対パスで共通するのは、前述のJspMvcFeature.TEMPLATE_BASE_PATH
で指定したフォルダ(今回の場合は「/WEB-INF/views/」)を読むということです。
絶対パスの場合、「/WEB-INF/views/」からの絶対パスになります。例えば、戻り値をreturn new Viewable("/hello/index.jsp");
とした場合、フォワード先のJSPは/WEB-INF/views/index.jsp
です。
相対パスの場合、「/WEB-INF/views/リソースクラスのパッケージのパス/リソースクラス名/コントローラーの戻り値」となります。例えば、リソースクラスのパッケージがcom.example.rest.resource
、リソースクラス名がHelloResource
、戻り値がreturn new Viewable("index.jsp");
の場合、フォワード先のJSPは/WEB-INF/views/com/example/rest/resource/HelloResource/index.jsp
です。
MVC 1.0との比較
今のところEDR2の仕様(一部Ozarkの挙動)では、以下のようになっています。
- コントローラーの戻り値は
String
またはjavax.mvc.Viewable
(void
やResponse
も可能) - 拡張子の指定が必須
- 「/」で始める絶対パスの場合、フォワード先のビューは
コンテキストルート/コントローラーの戻り値
(JSRには「/」で始める場合の記述がないため、今のところOzark独自の挙動っぽい) - 「/」で始めない相対パスの場合、デフォルトでは
/WEB-INF/views/コントローラーの戻り値
絶対パス・相対パス共に、Jersey MVCとは微妙に異なります。
こうなると、Jersey MVCでは相対パス・絶対パスのどちらで書いたほうが移行コスト(=プログラム等の修正箇所)が少ないかは、一概には言えない感じがしますね・・・(^^;
そもそも、現在のMVC 1.0のサブフォルダ名まで指定しなければいけないこと自体がカッコよくない気がするなあ。。。
https://github.com/MasatoshiTada/OzarkSample/blob/master/src/main/java/ozarksample/controller/HelloController.java
似たような議論がMVC 1.0のメーリングリストでもあったのですが、採用されないまま終わっています。
https://java.net/projects/mvc-spec/lists/users/archive/2015-12/message/6
Jersey MVCでは絶対パス・相対パスのどちらがいいのか、まだ悩み中です・・・。
完成版のコードには、絶対パス・相対パスの両方を載せました。
ビューの作成
前述のフォルダに、JSPファイルを作成します。
index.jsp(入力画面)
<%@ taglibprefix="mytag"uri="http://example.com/myTag" %><%@ pagecontentType="text/html;charset=UTF-8"language="java" %><html><head><title>名前入力画面(絶対パス)</title><linkrel="stylesheet"href="../../css/style.css"></head><body><h1>名前を入力してください</h1><mytag:errors errorClass="error"/><formmethod="get"action="./result">名前:<inputtype="text"name="name"><inputtype="submit"value="送信"></form><ahref="./redirect">リダイレクト</a></body></html>
result.jsp(出力画面)
<%@ taglibprefix="c"uri="http://java.sun.com/jsp/jstl/core" %><%@ pagecontentType="text/html;charset=UTF-8"language="java" %><html><head><title>メッセージ表示画面(絶対パス)</title></head><body><c:out value="${hello.message}"/></body></html>
JSP以外のビューを使う方法
Jersey MVCでサポートしているビューは、JSP・FreeMarker・Mustacheです。
https://jersey.java.net/documentation/latest/user-guide.html#d0e15182
また、org.glassfish.jersey.server.mvc.spi.TemplateProcessor
インタフェースを実装することで、他のビューを使うことも可能です。
下記の@glory_ofさんのブログの場合、Thymeleafを使っていらっしゃいます。
JerseyMVCとThymeleafを組み合わせる - シュンツのつまづき日記
コントローラーからビューへの値の受け渡し
Jersey MVCで定義されているのは、Viewable
コンストラクタにオブジェクトを渡し、JSPではELでmodel
という名前で参照する方法です。
@GET@Path("result")
public Viewable result(@QueryParam("name") @DefaultValue("")
String name) throws Exception {
helloDto.setMessage("Hello, " + name);
returnnew Viewable("/hello/result.jsp", helloDto);
}
<c:out value="${model.message}"/>
しかし、この方法はMVC 1.0には現時点では無く、かつオブジェクトが1つしか渡せないというデメリットがあります。
MVC 1.0にはModels
というマップがあるのですが、これを再発明することは今回の「MVC 1.0の再発明はしない」という方針に反します。
そこで、Jersey MVCとMVC 1.0の両方で使える、CDIビーンを使う方法を紹介します。
まず、DTOクラスを作成し、@Named
と@RequestScoped
を付加します。
package com.example.rest.dto;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named("hello")
@RequestScopedpublicclass HelloDto {
private String message;
public String getMessage() {
return message;
}
publicvoid setMessage(String message) {
this.message = message;
}
}
コントローラーでは、このDTOをフィールドインジェクションし、DTOのsetterで値をセットします。
@Injectprivate HelloDto helloDto;
@GET@Path("result")
public Viewable result(@QueryParam("name") @DefaultValue("")
String name) throws Exception {
helloDto.setMessage("Hello, " + name);
returnnew Viewable("/hello/result.jsp");
}
JSPのELでは、@Named
で指定した名前で呼び出します。
<c:out value="${hello.message}"/>
この方法なら、Jersey MVC (というかJava EE 7)でもMVC 1.0でも使えます。
受け渡す値が2つ以上ならば、その数だけDTOクラスを作成することになります。
バリデーションと例外処理
MVC 1.0では、BindingResult
でバリデーションエラーの有無およびエラーメッセージの表示を行い、例外処理はJAX-RS標準のExceptionMapper
を利用します。
Jersey MVCにはBindingResult
と同様の動きをするものが存在しません。
色々と考えたのですが、ここは素直にJersey MVCで提供されている機能を使いましょう。
@ErrorTemplate
の利用
コントローラーメソッドに@ErrorTemplate
を付加し、バリデーションエラーおよび例外発生時に遷移するビューを指定します。
このビュー名も、コントローラーと同様のルールで相対パスまたは絶対パスで指定します。
@GET@Path("result")
@ErrorTemplate(name = "index.jsp") public Viewable result(@QueryParam("name") @DefaultValue("")
@Size(message = "{name.size}", min = 1, max = 10)
@Pattern(message = "{name.pattern}", regexp = "[a-zA-Z]*")
String name) throws Exception {
switch (name) {
case"null":
thrownew NullPointerException("NULLPO!");
case"myrun":
thrownew MyRuntimeException("MyRuntime!");
case"run":
thrownew RuntimeException("Runtime!");
case"io":
thrownew IOException("IOE!");
case"myex":
thrownew MyException("MY EXCEPTION!");
case"ex":
thrownew Exception("EXCEPTION!");
}
helloDto.setMessage("Hello, " + name);
returnnew Viewable("result.jsp");
}
今回は、「1文字以上10文字以下でなければならない」「入力文字列は半角英字でなければならない」というルールにしました。
@Size
や@Pattern
は、Java EEのBean Validationで定義されたアノテーションです。
バリデーションエラー時は、javax.validation.ConstraintViolationException
が発生します。
この例外に対応したExceptionMapper
実装クラスが、jersey-mvc-bean-validationに含まれています(org.glassfish.jersey.server.mvc.beanvalidation.ValidationErrorTemplateExceptionMapper
クラス)。
このValidationErrorTemplateExceptionMapper
には、バリデーションエラー発生時に@ErrorTemplate
で指定されたビューにフォワードする処理が記述されています。
ConstraintViolationException
以外の例外がコントローラーメソッド内で発生した場合、jersey-mvcに含まれているorg.glassfish.jersey.server.mvc.internal.ErrorTemplateExceptionMapper
クラスが動き、@ErrorTemplate
で指定されたビューにフォワードします。
エラーメッセージの表示
バリデーションエラー時もその他の例外発生時も、JSPのELではmodel
という名前で参照します。
バリデーションエラー時はList<ValidationError>
、例外発生時はその例外オブジェクトそのものが、model
に格納されます。
これもどうするか非常に悩んだのですが、JSPカスタムタグを作りました。
package com.example.servlet.tag;
import org.glassfish.jersey.server.validation.ValidationError;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.io.IOException;
import java.util.List;
publicclass ErrorsHandler extends SimpleTagSupport {
private String errorClass;
publicvoid setErrorClass(String errorClass) {
this.errorClass = errorClass;
}
@Overridepublicvoid doTag() throws JspException, IOException {
JspWriter out = getJspContext().getOut();
Object model = getJspContext().findAttribute("model");
if (model instanceof Exception) {
out.println("<ul class=\"" + errorClass + "\">");
Exception exception = (Exception) model;
out.println("<li>");
out.println(exception.getMessage());
out.println("</li>");
out.println("</ul>");
} elseif (isValidationErrorList(model)) {
out.println("<ul class=\"" + errorClass + "\">");
List<ValidationError> validationErrors = (List<ValidationError>) model;
for (ValidationError error : validationErrors) {
out.println("<li>");
out.println(error.getMessage());
out.println("</li>");
}
out.println("</ul>");
}
}
privateboolean isValidationErrorList(Object model) {
if (model instanceof List) {
List list = (List) model;
if ( ! list.isEmpty()) {
Object firstElement = list.get(0);
if (firstElement instanceof ValidationError) {
returntrue;
}
}
}
returnfalse;
}
}
<%@ taglibprefix="mytag"uri="http://example.com/myTag" %><mytag:errors errorClass="error"/>
あんまりカッコよくない実装なので、もっと良い案がありましたら是非コメントください!
例外発生時は別のエラーページに遷移する
ExceptionMapper
実装クラスを作り、Viewable
でエラーページ指定しました。
package com.example.rest.exception.mapper;
import org.glassfish.jersey.server.mvc.Viewable;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
@Providerpublicclass ExceptionMapper implements javax.ws.rs.ext.ExceptionMapper<Exception> {
@Overridepublic Response toResponse(Exception e) {
Viewable viewable = new Viewable("/error/exception", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(viewable).build();
}
}
@Providerpublicclass MyExceptionMapper implements ExceptionMapper<MyException> {
@Overridepublic Response toResponse(MyException e) {
Viewable viewable = new Viewable("/error/exception", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(viewable).build();
}
}
@Providerpublicclass MyRuntimeExceptionMapper implements ExceptionMapper<MyRuntimeException> {
@Overridepublic Response toResponse(MyRuntimeException e) {
Viewable viewable = new Viewable("/error/exception", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(viewable).build();
}
}
@Providerpublicclass RuntimeExceptionMapper implements ExceptionMapper<RuntimeException> {
@Overridepublic Response toResponse(RuntimeException e) {
Viewable viewable = new Viewable("/error/exception", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(viewable).build();
}
}
で、実行していただくと分かるのですが、IOException
とException
の場合のみ、exception.jspではなく、index.jspに遷移します。
これは、org.glassfish.jersey.server.mvc.internal.ErrorTemplateExceptionMapper
クラスが優先されているようです。
IOException
は自作のExceptionMapper
を作っていないので、当然ErrorTemplateExceptionMapper
クラスが動く形になります。
Exception
は自作のExceptionMapper
を作っていますが、ErrorTemplateExceptionMapper
クラスが優先されるようです。
JAX-RSの仕様やJerseyのドキュメントを読んで確認しましたが、@Priority
で優先度をつけることが出来ないようで、回避のしようが無いっぽいです。
リダイレクト
MVC 1.0だと、コントローラメソッドの戻り値の文字列にredirect:
という接頭辞をつけるだけでリダイレクトになりますが、Jersey MVCにはその機能はありません。
なので、JAX-RS標準の機能を使いましょう。ステータスコードを300番台にして、HTTPレスポンスのlocationヘッダーにリダイレクト先を指定します。
@GET@Path("redirect")
public Response redirect(@Context UriInfo uriInfo)
throws URISyntaxException {
URI location = uriInfo.getBaseUriBuilder()
.path(HelloResource.class)
.path("redirect2")
.build();
return Response.status(Response.Status.FOUND)
.location(location).build();
}
@GET@Path("redirect2")
public Viewable redirect2() {
returnnew Viewable("redirect2.jsp");
}
JAX-RS標準の機能なので、MVC 1.0に移行しても特に修正の必要はないはずです。
Payara以外のAPサーバーでのJersey MVCの利用
ここまでの内容は、Payara(およびGlassFish)、つまりJerseyおよびJersey MVCが内包されたAPサーバー前提で書きました。
pom.xmlでjersey関連の依存性をすべてcompile
にすればできるはずです。
検証ができたら後ほど追記します。
内包されているJAX-RS実装はJerseyですが、Jersey MVCは内包されていないようです。
ですので、pom.xmlを記述する際は、jersey-mvc-jspのスコープをcompile
にすればOKだと思います。
未検証なので、どなたかWebLogicを使っている方は試してみてください!
内包されているJAX-RS実装がJerseyではありません(WildFly/JBossはRESTEasy、WebSphereはApache CXF)。
pom.xmlでjersey関連の依存性をすべてcompile
するだけだと、サーバーに内包されている方のJAX-RS実装が動くような気がするので、web.xmlにorg.glassfish.jersey.servlet.ServletContainer
を追加する必要があるかも・・・。
WildFlyで検証しますので、検証ができたら後ほど追記します。
ちなみに、RESTEasyには「HTML Provider」というJersey MVCに似た独自機能が存在します。
機能自体はかなりシンプルですが、WildFly/JBossの場合はこちらを使うのもアリかもしれません。
ただし、こちらはMVC 1.0とは相違点がかなり多いので、その点はご注意ください。
RESTEasyのHTML Providerで遊んでみる - CLOVER
まとめ
- EE 7でアクションベースMVCを使いたい場合は、Jersey MVCを使いましょう!
- Jersey MVCとMVC 1.0の相対パス・絶対パスを理解しよう!
- ビューへ値を渡す時は、CDIビーンを使いましょう!
- バリデーションと例外処理は、
@ErrorTemplate
とExceptionMapper
を併用しましょう!
最後に
繰り返しになりますが、Jersey MVCはあくまで独自機能であり、Java EE 7標準の「範囲外」です。
Java EE 7標準の範囲内では、コンポーネントベースのJSFが、唯一のHTMLを返すフレームワークです。
Java EE 7標準にどこまでこだわるか、プロジェクトの事情を考慮して、利用を検討してください。