「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.Models
やjavax.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
ですね)
ViewEngine
はViewable
にも指定でき、こちらで指定があればそれが優先されます。
Viewable
にViewEngine
の指定がなければ、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用の実装も用意されています。JspViewEngine
のsupports
メソッドが変わっただけですが。
以上が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月段階の情報ですが、以下のスライドも参照してください。