IK.AM

@making's tech note


Javaユーザー待望のJava標準MVCフレームワーク 「MVC 1.0」(JSR371)の紹介

🗃 {Programming/Java/javax/mvc}
🏷 JSR-371 🏷 Java 🏷 Java EE 8 🏷 MVC 🏷 Ozark 
🗓 Updated at 2015-02-18T08:45:50Z  🗓 Created at 2015-02-18T08:45:50Z   🌎 English Page

「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の書き方は次のようになります。

@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.Modelsjavax.mvc.engine.ViewEngineも持たせることができます。
  • Response ... JAX-RSのResponseオブジェクトにビュー名を指定します。Responseオブジェクトを使うことで、HTTPステータスコードやHTTPレスポンスヘッダを指定できます。

それぞれの実装例は以下の通りです(多分動かない)。

@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を使う場合は、

@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を使う場合は

@Named("greeting")
@RequestScoped
public class Greeting {
  private String message;
  // setter/getter/コンストラクタ略
}

@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の場合

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>${greeting.message}</h1>
  </body>
</html>

(この例はXSSエスケープをしていないのでよくない例です。)

Modelsに詰めた値をEL式でアクセスできます。当然@Namedで名前付けした、CDI管理Beanもアクセスできます。

Controllerで返したビュー名から実際に対象のファイルを取得し、レンダリングするのがViewEngineです。

現時点で以下のようなインタフェースになっています。

public interface ViewEngine {
  boolean supports(String view);
  void processView(ViewEngineContext context) throws ViewEngineException;
}

(Spring MVCで言う所のViewResolverですね)

ViewEngineViewableにも指定でき、こちらで指定があればそれが優先されます。 ViewableViewEngineの指定がなければ、DIコンテナからViewEngineインスタンスをルックアップします。 優先度の高いViewEngineからsupportメソッドを試してtrueの場合に、processViewメソッドを呼び出しレンダリングします。

優先順位は@javax.annotation.Priorityで指定します。

仕様ではViewEngineの実装までは言及されていませんので、実装依存でサポートするテンプレートエンジンが異なるし解決方法も異なる可能性があります。

参照実装OzarkのJSP実装は以下のようになっています。

@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用の実装も用意されています。JspViewEnginesupportsメソッドが変わっただけですが。

以上がMVCの現時点の仕様です。まだ理解しやすいですね。

入力チェック

入力チェックはBean Validationを使用することになっていますが、ハンドリング方法が議論になっています。 最初の案はJAX-RS風で、以下のようなやり方です。

@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<ConstraintViolation<?>> 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();
  }
}

…これは辛い…

次の案は、例外を引数にとれるパターン(と↑のやり方を選択できる)。

public String post(@Valid @BeanParam FormDataBean form, ConstraintViolationException e) {
  if (e != null) {
    // oops, send form again
  } else {
    // all good
  }
}

エラー画面の遷移が書きやすくなりましたが、まだ気持ち悪いです。

今提案されているのが、次のインタフェースを追加して、

public interface ValidationResult {
  boolean hasErrors();
  ConstraintViolationException getConstraintViolationException();
  Set<ConstraintViolation> getViolations();
  Set<ConstraintViolation> getViolations(String property);
  ConstraintViolation getViolation(String property);
  /* and more */
}

こんな感じに書くやり方です。

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です。

ソースコードはGithub上にもありますので、簡単に参照できます。

サンプルもいくつかあるので、使い勝手を確認できます。

https://github.com/spericas/ozark/tree/master/test

次の記事ではOzarkの動かし方を書きたいと思います。

フィードバックについて

メーリングリストを登録しましょう。意見がある場合は、MLのメッセージに返信してみてください(英語で)。

https://java.net/projects/jjug/pages/JSR-371

ぜひJCPアカウントを作成し、JJUGにひも付けましょう

去年の11月段階の情報ですが、以下のスライドも参照してください。


✒️️ Edit  ⏰ History  🗑 Delete