--- title: Javaユーザー待望のJava標準MVCフレームワーク 「MVC 1.0」(JSR371)の紹介 tags: ["JSR-371", "Java", "Java EE 8", "MVC", "Ozark"] categories: ["Programming", "Java", "javax", "mvc"] date: 2015-02-18T08:45:50Z updated: 2015-02-19T00:13:28Z --- 「MVC 1.0」はJava EE 8から追加されるJava標準の**アクション指向**MVCフレームワークです。(Java標準のMVCフレームワーク自体はJSFが既に存在しています。) 次のスケジュールでリリース予定になっています。 | 時期 | マイルストーン | | --- | --- | | 2015 Q1 | Early Draft | | 2015 Q3 | Public Review | | 2016 Q1 | Proposed Final Draft | | 2016 Q3 | Final Release | (商用サーバーで使えるようになるのは2018年くらい...?) しばらくMLをウォッチしていますが、本記事では2015-02-18段階で検討されている内容について簡単に説明します。**今後大きく変わる可能性もあるので、本記事の内容を鵜呑みにしないでくさい。** ### MVC 1.0(JSR-371)について **MVC 1.0はJAX-RS上に作られています**(重要)。ServletベースにするかJAX-RSベースにするか投票が行われましたが、結果的にJAX-RSベースになりました。 JAX-RSの所謂"resourceクラス"のメソッドの返り値の画面に遷移する形で、ほとんどのJAX-RS用アノテーションや設定方法が流用されます。Jersey MVCを使っていた人は比較的近いイメージで利用できると思います。 View以外はJAX-RS + CDI (+ Bean Validation)みたいなイメージです。 対象のクラス・メソッドがMVC用であることを示すために`@javax.mvc.Controller`アノテーションを付けます。メソッドに`@Controller`アノテーションを付けた場合、そのメソッドがMVC用になり、クラスに`@Controller`アノテーションをつけた場合はそのクラスのすべてのメソッドがMVC用になります。 #### Controller Controllerの書き方は次のようになります。 ``` java @Path("hello") public class HelloController { @GET @Controller public String hello() { return "hello.jsp"; } } ``` この例だと`hello`メソッドの返り値`hello.jsp`がビュー名(遷移先)になります。 `hello.jsp`という返り値に対して、実際にどういう画面を返すかは今のところ仕様では決まっておらず、後に説明する`ViewEngine`の実装依存になります。おそらく、`WEB-INF/hello.jsp`がレンダリングされるでしょう。 Controllerメソッドの返り値は次の4つがサポートされています。 * `void` ... `void`で返すとき、メソッドに`@javax.mvc.View`アノテーションでビュー名を指定します。 * `String` ... ビュー名を文字列で直接指定します。 * `Viewable` ... `javax.mvc.Viewable`オブジェクトにビュー名を指定します。`String`で返すのとは異なり、`Viewable`には後に説明する`javax.mvc.Models`や`javax.mvc.engine.ViewEngine`も持たせることができます。 * `Response` ... JAX-RSの`Response`オブジェクトにビュー名を指定します。`Response`オブジェクトを使うことで、HTTPステータスコードやHTTPレスポンスヘッダを指定できます。 それぞれの実装例は以下の通りです(多分動かない)。 ``` java @Controller @Path("hello") public class HelloController { @GET @View("hello.jsp") public void helloVoid() {  } @GET public String helloString() { return "hello.jsp"; } @GET public Viewable helloViewable() {   return new Viewable("hello.jsp"); } @GET public Viewable helloResponse() { return Response.status(Response.Status.OK).entity("hello.jsp").build(); } } ``` 返り値は上記の制限がありますが、メソッドに取れる引数はJAX-RSと同じです。 また、デフォルトのContent-Typeはtext/htmlですが、JAX-RSの`@Produces`アノテーションで変更することもできます。 JAX-RSとは異なり、Controllerのインスタンスは**CDIで管理されたBean**です。EJBにはなりません。 また、デフォルトでリクエストスコープであり、リクエスト毎に作られる必要があります。 #### Model Modelには次の2種類がサポートされています。 * `Models` ... Mapベースのデータ受け渡し用クラス * `@Named`がついたCDI管理Bean ... そのままです。 `Models`を使う場合は、 ``` java @Path("hello") public class HelloController { @Inject Models models  @GET  @Controller  public String hello() { models.put("greeting", new Greeting("Hello World!")); return "hello.jsp";  } } ``` CDI管理Beanを使う場合は ``` java @Named("greeting") @RequestScoped public class Greeting { private String message; // setter/getter/コンストラクタ略 } ``` と ``` java @Path("hello") public class HelloController { @Inject Greeting greeting; @GET @Controller public String hello() { greeting.setMessage("Hello World!"); return "hello.jsp"; } } ``` な感じです。 現時点で、`Models`をサポートするのは必須ですが、CDI管理Beanに関しては**強く推奨**となっています。 一方で、`Models`よりCDI管理Beanを使うことが推奨されていますw (CDI管理Beanを強制しないことによって、Spring MVCもJSR-371を実装できるかも?) CDI管理Beanの場合、CDIでサポートされているスコープが使えます。`@ConversationScoped`も使えるので、Spring MVCユーザー的にはヨダレが出ますね。 #### View まずは例から。JSPの場合 ``` jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> Hello

${greeting.message}

``` (この例はXSSエスケープをしていないのでよくない例です。) `Models`に詰めた値をEL式でアクセスできます。当然`@Named`で名前付けした、CDI管理Beanもアクセスできます。 Controllerで返したビュー名から実際に対象のファイルを取得し、レンダリングするのが`ViewEngine`です。 現時点で以下のようなインタフェースになっています。 ``` java public interface ViewEngine { boolean supports(String view); void processView(ViewEngineContext context) throws ViewEngineException; } ``` (Spring MVCで言う所の`ViewResolver`ですね) `ViewEngine`は`Viewable`にも指定でき、こちらで指定があればそれが優先されます。 `Viewable`に`ViewEngine`の指定がなければ、DIコンテナから`ViewEngine`インスタンスをルックアップします。 優先度の高い`ViewEngine`から`support`メソッドを試して`true`の場合に、`processView`メソッドを呼び出しレンダリングします。 優先順位は`@javax.annotation.Priority`で指定します。 仕様では`ViewEngine`の実装までは言及されていませんので、実装依存でサポートするテンプレートエンジンが異なるし解決方法も異なる可能性があります。 参照実装OzarkのJSP実装は以下のようになっています。 ``` java @Priority(Priorities.DEFAULT) public class JspViewEngine implements ViewEngine { private static final String VIEW_BASE = "/WEB-INF/"; @Inject private ServletContext servletContext; @Override public boolean supports(String view) { return view.endsWith("jsp") || view.endsWith("jspx"); } @Override public void processView(ViewEngineContext context) throws ViewEngineException { final Models models = context.getModels(); final HttpServletRequest request = context.getRequest(); final HttpServletResponse response = context.getResponse(); // Set attributes in request for (String name : models) { request.setAttribute(name, models.get(name)); } // Forward request to servlet engine to process JSP RequestDispatcher rd = servletContext.getRequestDispatcher(VIEW_BASE + context.getView()); try { rd.forward(request, response); } catch (ServletException | IOException e) { throw new ViewEngineException(e); } } } ``` Servletプログラミングをカジっていれば何をしているか分かるでしょう。 Facelets用の実装も用意されています。`JspViewEngine`の`supports`メソッドが変わっただけですが。 以上がMVCの現時点の仕様です。まだ理解しやすいですね。 #### 入力チェック 入力チェックはBean Validationを使用することになっていますが、ハンドリング方法が議論になっています。 最初の案はJAX-RS風で、以下のようなやり方です。 ``` java @POST @OnConstraintViolation(view="error.jsp", mapper=FormViolationMapper.class) public String post(@Valid @BeanParam FormDataBean form) { out.setAge(form.getAge()); out.setName(form.getName()); return "data.jsp"; } public static class FormViolationMapper implements ConstraintViolationMapper { @Inject private ErrorDataBean error; @Override public Response toResponse(ConstraintViolationException e, String view) { final Set> set = e.getConstraintViolations(); if (!set.isEmpty()) { final ConstraintViolation cv = set.iterator().next(); final String property = cv.getPropertyPath().toString(); error.setProperty(property.substring(property.lastIndexOf('.') + 1)); error.setValue(cv.getInvalidValue()); error.setMessage(cv.getMessage()); } return Response.status(Response.Status.BAD_REQUEST).entity(view).build(); } } ``` …これは辛い… 次の案は、例外を引数にとれるパターン(と↑のやり方を選択できる)。 ``` java public String post(@Valid @BeanParam FormDataBean form, ConstraintViolationException e) { if (e != null) { // oops, send form again } else { // all good } } ``` エラー画面の遷移が書きやすくなりましたが、まだ気持ち悪いです。 今提案されているのが、次のインタフェースを追加して、 ``` java public interface ValidationResult { boolean hasErrors(); ConstraintViolationException getConstraintViolationException(); Set getViolations(); Set getViolations(String property); ConstraintViolation getViolation(String property); /* and more */ } ``` こんな感じに書くやり方です。 ``` java public String post(@Valid @BeanParam FormDataBean form, ValidationResult result) { if (result.hasErrors()) { // oops, send form again } else { // all good } } ``` どう見てもSpring MVCの`BindingResult`ですね。そしてこっちの方が良いです・・ そのほか、Locale/Languageのハンドリング方法やPortletの実装方法も議論されています。 正直Spring MVCの再開発感が強いですが、標準にして長く使っていくとはこういうことなんでしょうね。 ### 参照実装Ozarkについて JSR-371の参照実装は[Ozark](https://ozark.java.net/)です。 ソースコードはGithub上にもありますので、簡単に参照できます。 * [API](https://github.com/spericas/mvc-spec) * [参照実装](https://github.com/spericas/ozark) サンプルもいくつかあるので、使い勝手を確認できます。 https://github.com/spericas/ozark/tree/master/test 次の記事ではOzarkの動かし方を書きたいと思います。 ### フィードバックについて メーリングリストを登録しましょう。意見がある場合は、MLのメッセージに返信してみてください(英語で)。 https://java.net/projects/jjug/pages/JSR-371 ぜひJCPアカウントを作成し、[JJUGにひも付けましょう](https://java.net/projects/jjug/pages/HowToJoinJCP)。 去年の11月段階の情報ですが、以下のスライドも参照してください。
MVC 1.0 を通じて Adopt a JSR を知ろう! from Taku Miyakawa
MVC 1.0 JSR-371を通してAdopt a JSRに知ろう #jjug_ccc #ccc_r57 from Toshiaki Maki