JavaEE Model-View-Controller API 1.0 - JSR 371 の基礎

f:id:Naotsugu:20170301230553p:plain

Java EE の MVC1.0 (Model-View-Controller API 1.0 - JSR 371)、Early Draft Review 2 時点のまとめです。

Jersey MVC と同じように JAX-RS の上に乗っかる形となっているため、ほとんどは JAX-RS と同じで、コントローラメソッドに @Controller 付けて戻り値としてViewのパスを返すぐらいな感じです。

コントローラの定義

コントローラは @Controller アノテーションで定義します。

クラスに付けるとクラス中の全てのリソースメソッドがコントローラになります。

@Path("/")
@Controller
public class WelcomeController {
    @GET
    public String welcome() {
        return "welcome.html";
    }
}

メソッドに付与すると、そのメソッドがコントローラメソッドになり、他は JAX-RS のリソースメソッドになります。

@Path("/")
@RequestScoped
public class VetController {

    @Inject
    private Models models;

    @GET
    @Controller
    public String showVetList() {
        Vets vets = new Vets(vets.findAll());
        models.put("vets", vets);
        return "vets/vetList.html";
    }


    @GET
    @Path("json")
    @Produces({MediaType.APPLICATION_JSON})
    public Vets showResourcesVetList() {
        return new Vets(vets.findAll());
    }
}

パラメータの受け取り方や HTTP メソッドの指定は JAX-RS と同じです。

こちらを参照ください。

blog1.mammb.com

View の指定

コントローラからの戻り値の文字列はViewのパスになります。

@GET
public String welcome() {
    return "welcome.html";
}

この場合はデフォルトでは WEB_INF/views/welcome.html の View が使われます。

文字列の他に void String Viewable Response を返すことができます。 その他のオブジェクトを戻り値にした場合には toString() の結果が View のパスとして使われます。

戻り値が void の場合は @View アノテーションで View テンプレートのパスを指定します。

@GET
@View("welcome.html")
public void welcome() {
}

Viewable を使ってパスを指定することもできます。

@GET
public Viewable welcome() {
    return new Viewable("welcome.html");
}

Viewable はモデルのインスタンスやエンジンを指定することができます。

Viewable(String view, Models models, 
        Class<? extends ViewEngine> viewEngine)

JAX-RS の Response も使えます。

@GET
public Response welcome() {
    return Response.status(Response.Status.OK)
        .entity("welcome.html").build();
}

Model

Models を インジェクトし、View で使うデータを put することで View 側からアクセスできます。

@Path("hello")
@Controller
public class HelloController {

  @Inject
  private Models models;

  @GET
  public String hello() {
    models.put("greeting", new Greeting("Hello there!");
    return "hello.html";
  }
}

Models は単なる Map のインスタンスになります。

public interface Models extends Map<String, Object>, Iterable<String> {}

View エンジンが CDI をサポートしている場合は、以下のような CDI ビーンに @Named で名前を付けておき、

@Named("greeting")
@RequestScoped
public class Greeting {
  // ...
}

コントローラで値を設定することで、View からアクセスできます。

@Path("hello")
@Controller
public class HelloController {

  @Inject
  private Greeting greeting;

  @GET
  public String hello() {
    greeting.setMessage("Hello there!");
    return "hello.jsp";
  }
}

View

View は色々なエンジンが使えます。 Ozark の M02 では以下のような Extension が提供されています。

  • Thymeleaf Extension
  • Mustache Extension
  • AsciiDoc Extension
  • JSR 223 Extension
  • Velocity Extension
  • Freemarker Extension
  • Handlebars Extension

JSP の場合は以下のように EL 式で Model にアクセスします。

<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>${greeting.message}</h1>
  </body>
</html>

View エンジンの選択は以下を参照ください。

blog1.mammb.com

リダイレクト

リダイレクトする場合は Response で seeOther を返します。

@GET
@Controller
public Response redirect() {
    return Response.seeOther(URI.create("see/here")).build();
}

または以下のように redirect: のプレフィックスを付けます。

@GET
@Controller
public String redirect() {
    return "redirect:see/here";
}

以下のように @RedirectScoped アノテーションされたクラスがあった場合

@RedirectScoped
public class MyBean { }

post() でリダイレクトした先の get() の View で myBean の内容が引き継げます。

@Controller
@Path("submit")
public class MyController {

  @Inject
  private MyBean myBean;

  @POST
  public String post() {
   myBean.setValue("Redirect about to happen");
   return "redirect:/submit";
}

  @GET
  public String get() {
    return "mybean.html";
  }
}

例外マッパ

JAX-RS と同じように ExceptionMapper を定義します。

ExceptionMapper は以下の定義になっています。

public interface ExceptionMapper<E extends Throwable> {
    Response toResponse(E var1);
}

実装は以下のような感じになります。

例外マッパを扱いたい例外に応じて定義します。

@Provider
@ApplicationScoped
public class AppExceptionMapper implements ExceptionMapper<Throwable> {

    @Inject
    private Models models;

    @Override
    public Response toResponse(Throwable e) {
        models.put("message", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST).entity("error.html").build();
    }
}

@Provider を付けることで JAX-RS が例外マッパとして拾ってくれます。

CDI 1.2 の場合は @ApplicationScoped などを指定して CDI 管理対象とすることで、Models など CDI でインジェクトできます。

例えば Thymeleaf テンプレートでは以下のようにModel の値にアクセスできます。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<body>
<h2>Something happened...</h2>
<p th:text="${message}">Exception message</p>
</body>

</html

この後で出てくる ParamConverter についてもそうですが、現時点では例外マッパに優先度(HK2のpriority)を指定できないため、ExceptionMapper<Exception> などとすると、組込の ExceptionMapper が優先されてしまう問題があります(直接HK2で扱えば解決できますが、あまり現実的ではありません)。

Validation Exceptions

Bean Validation の扱いなども JAX-RS と同じです。

Entity に対する Bean Validation などは以下のように @Valid@BeanParam を付けることで Bean Validation が動きます。

@Path("owners")
@Controller
@RequestScoped
public class OwnerController {

    @EJB
    private OwnerRepository repository;

    @Inject
    private BindingResult bindingResult;

    @Inject
    private Models models;

    @POST
    @Path("new")
    @ValidateOnExecution(type = ExecutableType.NONE)
    public String processCreationForm(@Valid @BeanParam Owner owner) {
        if (bindingResult.isFailed()) {
            models.put("owner", owner);
            models.put("bindingResult", bindingResult);
            return "owners/createOrUpdateOwnerForm.html";
        }
        repository.save(owner);

        return "redirect:/owners/" + owner.getId();
    }
}

通常は ValidationException が発生した場合はメソッドの呼び出しがおこなれませんが、@ValidateOnExecution(type = ExecutableType.NONE) のようにしておくことで、コントローラメソッド が呼び出されます。

この際、Validation の結果は BindingResult をインジェクトしておけば、そこから内容がとれます。JAX-RS と同じですね。

MvcContext

MvcContext をインジェクトすることで、設定、セキュリティやパスの情報にアクセスできます。

JSP などで以下のようにコンテキストパスを得たりすることができます。

<link rel="stylesheet" type="text/css" href="${mvc.contextPath}/my.css">

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

public interface MvcContext {

    Configuration getConfig();

    String getContextPath();

    String getApplicationPath();

    String getBasePath();

    Csrf getCsrf();

    Encoders getEncoders();
}

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

@Named("mvc")
@ApplicationScoped
public class MvcContextImpl implements MvcContext {
  // ...
}

まとめ

まだ Early Draft Review 2 なので今後変更になる可能性がありますが、JSR 371 Model-View-Controller API 1.0 の基本について見てきました。

次回は Spring PetClinic Sample Application を MVC1.0 で実装してみます。