Java EE 7でもアクションベースMVC! -MVC 1.0への移行を睨んだJersey MVCの活用- #javaee

この記事について

このブログは、Java EE Advent Calendar 2015 - Qiitaの25日目(最終日)です。

昨日はhondaYoshitakaさんの「Java - JAX-RSによるExcel/CSV/PDFファイルダウンロード - Qiita」でした。

MVC 1.0はJava EE 8から

アクションベースMVCと言えば、Java EE 8で導入される「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」は既にGitHubMaven上で公開されていますが、まだ策定途中のため仕様が変更される可能性が多々あり、学習目的以外では使わないほうがよいでしょう。

Jersey MVCならJava EE 7でも使える!

JAX-RSの参照実装「Jersey」には、独自機能「Jersey MVC」があります。

Jersey MVC自体はすでに完成しているフレームワークであり、Java 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の独自機能であり、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にアップしていますので、こちらをご参照ください。

GitHub - MasatoshiTada/JavaEEAdventCalendar2015-JerseyMVC

Mavenプロジェクトの作成

それでは、手順を説明していきます。

Mavenでプロジェクトを作成し、pom.xmlに依存性を追加します。

    <properties>
        <javaee.version>7.0</javaee.version>
        <jersey.version>2.22.1</jersey.version>
    </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>
        <!-- Java EE 7 Web Profile -->
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>${javaee.version}</version>
            <scope>provided</scope>
        </dependency>
        <!-- Jersey MVC + Jersey MVC JSP -->
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-mvc-jsp</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- JerseyでBean Validationを使う -->
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-bean-validation</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Jersey MVCでBean Validationを使う -->
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-mvc-bean-validation</artifactId>
            <!-- Payara(GlassFish)に含まれていないので「compile」でWARに含める -->
            <scope>compile</scope>
        </dependency>
        <!-- JSTL -->
        <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")
public class MyApplication extends ResourceConfig {
    public MyApplication() {
        // Jersey MVCの登録、ビューとしてJSPを使う
        register(JspMvcFeature.class);
        // Jersey MVCにおけるBean Validationを有効化する
        register(MvcBeanValidationFeature.class);
        // JSPファイルを保存するフォルダを指定する
        property(JspMvcFeature.TEMPLATE_BASE_PATH, "/WEB-INF/views/");
        // com.example以下の全パッケージを登録対象にする
        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")
@RequestScoped
public class HelloResource {

    @Inject
    private HelloDto helloDto;

    @GET
    @Path("index")
    public Viewable index() {
        return new 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":
                throw new NullPointerException("NULLPO!");
            case "myrun":
                throw new MyRuntimeException("MyRuntime!");
            case "run":
                throw new RuntimeException("Runtime!");
            case "io":
                throw new IOException("IOE!");
            case "myex":
                throw new MyException("MY EXCEPTION!");
            case "ex":
                throw new Exception("EXCEPTION!");
        }

        // 本来の処理
        helloDto.setMessage("Hello, " + name);
        return new 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 (voidResponseも可能)
  • 拡張子の指定が必須
  • 「/」で始める絶対パスの場合、フォワード先のビューはコンテキストルート/コントローラーの戻り値(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(入力画面)

<%@ taglib prefix="mytag" uri="http://example.com/myTag" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>名前入力画面(絶対パス)</title>
    <link rel="stylesheet" href="../../css/style.css">
</head>
<body>
    <h1>名前を入力してください</h1>

    <mytag:errors errorClass="error"/>

    <form method="get" action="./result">
        名前:<input type="text" name="name">
        <input type="submit" value="送信">
    </form>

    <a href="./redirect">リダイレクト</a>
</body>
</html>

result.jsp(出力画面)

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>メッセージ表示画面(絶対パス)</title>
</head>
<body>
    <c:out value="${hello.message}"/>
</body>
</html>

JSP以外のビューを使う方法

Jersey MVCでサポートしているビューは、JSPFreeMarker・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);
        return new Viewable("/hello/result.jsp", helloDto);
    }
<c:out value="${model.message}"/>

しかし、この方法はMVC 1.0には現時点では無く、かつオブジェクトが1つしか渡せないというデメリットがあります。

MVC 1.0にはModelsというマップがあるのですが、これを再発明することは今回の「MVC 1.0の再発明はしない」という方針に反します。

そこで、Jersey MVCMVC 1.0の両方で使える、CDIビーンを使う方法を紹介します。

まず、DTOクラスを作成し、@Named@RequestScopedを付加します。

package com.example.rest.dto;

import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@Named("hello")
@RequestScoped
public class HelloDto {
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

コントローラーでは、このDTOをフィールドインジェクションし、DTOのsetterで値をセットします。

    @Inject
    private HelloDto helloDto;
    
    @GET
    @Path("result")
    public Viewable result(@QueryParam("name") @DefaultValue("")
            String name) throws Exception {
        helloDto.setMessage("Hello, " + name);
        return new Viewable("/hello/result.jsp");
    }

JSPのELでは、@Namedで指定した名前で呼び出します。

<c:out value="${hello.message}"/>

この方法なら、Jersey MVC (というかJava EE 7)でもMVC 1.0でも使えます。

受け渡す値が2つ以上ならば、その数だけDTOクラスを作成することになります。

2016-01-14追記

改めて考えてみたら、CDIビーンを使う方法は微妙な気がしてきました・・・。

データが複数件の場合、Listをフィールドに持つDTOクラスを別途作るのか?例えば、社員DTOと社員リストDTOを両方作るのは微妙です・・・。

データが無かった場合(DBの検索結果が無かったとか)、それを表すフラグフィールドを作るのか?これも何かなあ・・・。

以上のことを考えると、CDIビーンではなく、Viewableコンストラクタに渡す方法がいいかもしれません。

複数の値をビューに渡す場合は、複数の値をHashMapにputして、このマップをViewableコンストラクタに渡せばOKです。

2016-01-27追記

@glory_ofさんから、「複数の値をビューに渡す場合は、それらをラップする大きなDTOクラスを作って、それをViewableコンストラクタ渡す」という方法を教えていただきました。

// @Namedや@RequestScopedなどはつけなくてOK
public class LargeDto {
    private Employee employee;
    private Department department;
    ...
}

// コントローラーメソッド内
LargeDto largeDto = ...;
return new Viewable("/hello/result", largeDto);

この方法のメリットは、クラスを作るので型安全やIDEの補完機能が効くことです。

デメリットは、作るクラスが増えて手間が少しかかることですね。でも、そんなに大きな工数はかからないと思います。

上記のMapを使う方法と、メリット・デメリットを比較して、どちらの方法を使うか考えていただければと思います。

バリデーションと例外処理

MVC 1.0では、BindingResultでバリデーションエラーの有無およびエラーメッセージの表示を行い、例外処理はJAX-RS標準のExceptionMapperを利用します。

Jersey MVCにはBindingResultと同様の動きをするものが存在しません。

色々と考えたのですが、ここは素直にJersey MVCで提供されている機能を使いましょう。

@ErrorTemplateの利用

コントローラーメソッド@ErrorTemplateを付加し、バリデーションエラーおよび例外発生時に遷移するビューを指定します。

このビュー名も、コントローラーと同様のルールで相対パスまたは絶対パスで指定します。

    @GET
    @Path("result")
    @ErrorTemplate(name = "index.jsp") // 相対パス
//    @ErrorTemplate(name = "/hello/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":
                throw new NullPointerException("NULLPO!");
            case "myrun":
                throw new MyRuntimeException("MyRuntime!");
            case "run":
                throw new RuntimeException("Runtime!");
            case "io":
                throw new IOException("IOE!");
            case "myex":
                throw new MyException("MY EXCEPTION!");
            case "ex":
                throw new Exception("EXCEPTION!");
        }

        // 本来の処理
        helloDto.setMessage("Hello, " + name);
//        return new Viewable("/hello/result.jsp"); // 絶対パス
        return new 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;

public class ErrorsHandler extends SimpleTagSupport {

    private String errorClass;

    public void setErrorClass(String errorClass) {
        this.errorClass = errorClass;
    }

    @Override
    public void 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>");
        } else if (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>");
        }

    }

    private boolean isValidationErrorList(Object model) {
        if (model instanceof List) {
            List list = (List) model;
            if ( ! list.isEmpty()) {
                Object firstElement = list.get(0);
                if (firstElement instanceof ValidationError) {
                    return true;
                }
            }
        }
        return false;
    }
}
<%@ taglib prefix="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;

@Provider
public class ExceptionMapper implements javax.ws.rs.ext.ExceptionMapper<Exception> {

    @Override
    public Response toResponse(Exception e) {
        Viewable viewable = new Viewable("/error/exception", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(viewable).build();
    }
}
// importは省略
@Provider
public class MyExceptionMapper implements ExceptionMapper<MyException> {
    @Override
    public Response toResponse(MyException e) {
        Viewable viewable = new Viewable("/error/exception", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(viewable).build();
    }
}
// importは省略
@Provider
public class MyRuntimeExceptionMapper implements ExceptionMapper<MyRuntimeException> {
    @Override
    public Response toResponse(MyRuntimeException e) {
        Viewable viewable = new Viewable("/error/exception", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(viewable).build();
    }
}
// importは省略
@Provider
public class RuntimeExceptionMapper implements ExceptionMapper<RuntimeException> {
    @Override
    public Response toResponse(RuntimeException e) {
        Viewable viewable = new Viewable("/error/exception", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(viewable).build();
    }
}

で、実行していただくと分かるのですが、IOExceptionExceptionの場合のみ、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() {
        return new Viewable("redirect2.jsp");
    }

JAX-RS標準の機能なので、MVC 1.0に移行しても特に修正の必要はないはずです。

Payara以外のAPサーバーでのJersey MVCの利用

ここまでの内容は、Payara(およびGlassFish)、つまりJerseyおよびJersey MVCが内包されたAPサーバー前提で書きました。

Tomcatの場合

pom.xmlでjersey関連の依存性をすべてcompileにすればできるはずです。

検証ができたら後ほど追記します。

Oracle WebLogic Serverの場合

内包されているJAX-RS実装はJerseyですが、Jersey MVCは内包されていないようです。

ですので、pom.xmlを記述する際は、jersey-mvc-jspのスコープをcompileにすればOKだと思います。

未検証なので、どなたかWebLogicを使っている方は試してみてください!

WildFly/JBoss/IBM WebSphereの場合

内包されているJAX-RS実装がJerseyではありません(WildFly/JBossはRESTEasy、WebSphereはApache CXF)。

pom.xmlでjersey関連の依存性をすべてcompileするだけだと、サーバーに内包されている方のJAX-RS実装が動くような気がするので、web.xmlorg.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 MVCMVC 1.0の相対パス絶対パスを理解しよう!
  • ビューへ値を渡す時は、CDIビーンを使いましょう!Jersey MVCでは素直にViewableコンストラクタに渡しましょう。複数の値を渡す場合は、Mapか大きなDTOを使いましょう。
  • バリデーションと例外処理は、@ErrorTemplateExceptionMapperを併用しましょう!

最後に

繰り返しになりますが、Jersey MVCはあくまで独自機能であり、Java EE 7標準の「範囲外」です。

Java EE 7標準の範囲内では、コンポーネントベースのJSFが、唯一のHTMLを返すフレームワークです。

Java EE 7標準にどこまでこだわるか、プロジェクトの事情を考慮して、利用を検討してください。