Jersey MVCでレルム認証する(Jerseyも基本的には方法は同じ!)

Jersey MVCでレルム認証する(Jerseyも基本的には方法は同じ!)

Java EEの認証・認可機能といえばレルムですね。

今回は、Jersey MVCでレルムを使ってみました。MVCじゃないJerseyでも方法は基本的に同じです。

ソースコードGitHubにアップしています。

https://github.com/MasatoshiTada/JerseyMVC_JDBCRealm

やりたいこと

  • ログイン機能をレルムで作る。
  • 未ログイン時に、ログイン時でないと見れない画面のURLを直接指定した場合、401エラー画面に強制遷移する。
  • 複数のロールを用意して、権限のないユーザーが管理者ロール専用の画面に遷移しようとしたら、403エラー画面に強制遷移する。

GlassFishのレルム設定

Java EEでレルムを利用する際は、アプリケーションサーバー側に設定が必要です。

今回、僕はPayara Web ML 4.1.1.154を使っています。

Payaraの設定方法はGlassFishと全く一緒です。寺田さんの下記のブログに手順が詳しく書いてあります。

http://yoshio3.com/2013/12/10/glassfish-jdbc-realm-detail/

依存ライブラリの追加

pom.xmlに依存性を追加します。最低限、javaee-web-apiとjersey-mvc-jsp(MVCでないならばjersey-server)があればOK。

それ以外に、特別な依存性は必要ありません。

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</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>
            <!-- 不要な依存性を除外 -->
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

ResourceConfigサブクラスの作成

ここが1つ目のポイントです。

RolesAllowedDynamicFeatureクラスの登録が必要になります。

これが無いと、リソースメソッドに@RolesAllowed付けても無視されます。

これは、JerseyでもJersey MVCでも同じです。

package com.example.rest;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.server.mvc.jsp.JspMvcFeature;

@ApplicationPath("api")
public class MyResourceConfig extends ResourceConfig {
    public MyResourceConfig() {
        register(JspMvcFeature.class);
        property(JspMvcFeature.TEMPLATE_BASE_PATH, "/WEB-INF/views/");
        packages(true, this.getClass().getPackage().getName());
        register(RolesAllowedDynamicFeature.class); // コレが必要!
    }
}

web.xmlの作成

レルムの利用に必要な設定を記述します。

レルムでの認証方法は、BASIC認証・FORM認証・DIGEST認証・SSL相互認証の4種類があります。

最初は、Java EE標準のFORM認証を使っていたのですが、FORM認証は基本的にはサーブレットJSPのことしか考慮されていないものなので、Jersey MVCだと使いづらかったです(^^;

なので方針を変えて、設定は「BASIC」にしておいて、ログイン処理自体は自前で実装(後述)することにしました。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

    <welcome-file-list>
        <welcome-file>index</welcome-file>
    </welcome-file-list>

    <security-constraint>
        <!-- 保護対象のURL -->
        <web-resource-collection>
            <web-resource-name>Sample System</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <!-- HTTPS化 -->
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <login-config>
        <auth-method>BASIC</auth-method>
        <!-- Payaraに設定したレルム名 -->
        <realm-name>jerseyMvc</realm-name>
    </login-config>

    <!-- ロールの一覧 -->
    <security-role>
        <description/>
        <role-name>ADMINISTRATOR</role-name>
    </security-role>
    <security-role>
        <description/>
        <role-name>MANAGER</role-name>
    </security-role>
    <security-role>
        <description/>
        <role-name>OPERATOR</role-name>
    </security-role>
</web-app>

ウェルカムファイルからログイン画面にリダイレクトするサーブレットの作成

web.xmlにウェルカムファイルを指定しましたが、Jersey MVCコントローラー経由のパスは指定できません。

なので、一旦サーブレットでリクエストを受け取って、ログイン画面にリダイレクトします。

package com.example.servlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/index")
public class RedirectServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.sendRedirect("./api/login");
    }
}

ログイン画面の作成

FORM認証ではないので、j_security_checkj_usernamej_passwordは必要ありません。いたって普通のJSPです。

今回は、Jersey MVC相対パスを使うので、このJSPはsrc/main/webapp/WEB-INF/views/com/example/rest/resource/LoginResourceフォルダに作成します。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>ログイン画面</title>
    </head>
    <body>
        <c:if test="${not empty param['retry']}">
            <p>ログインIDまたはパスワードが違います。</p>
        </c:if>
        <form action="./login" method="post">
            ログインID:<input type="text" name="loginId"><br>
            パスワード:<input type="password" name="password"><br>
            <input type="submit" value="ログイン">
        </form>
    </body>
</html>

ログイン用コントローラーの作成

先ほどのJSPからのリクエストパラメータで、ログインIDとパスワードを受け取り、ログイン処理を行います。

ログイン処理は、HttpServletRequest#login()メソッドを使います。

あんまりサーブレットAPIに依存させたくないんですが、レルムでログインするには仕方がないですね・・・。

ログインに成功したらインデックス画面にリダイレクトし、失敗したらログイン画面に戻ります。

package com.example.rest.resource;

import java.net.URI;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import org.glassfish.jersey.server.mvc.Viewable;

@Path("login")
public class LoginResource {

    @Context
    private SecurityContext securityContext;
    @Context
    private UriInfo uriInfo;
    @Context
    private HttpServletRequest httpServletRequest;

    @GET
    public Viewable index() {
        return new Viewable("login");
    }

    @POST
    public Response login(@FormParam("loginId") String loginId,
            @FormParam("password") String password) {
        // 未ログインならばログイン処理を行う
        if (securityContext.getUserPrincipal() == null) {
            System.out.println("ログイン処理開始...");
            try {
                // コンテナにログイン
                httpServletRequest.login(loginId, password);
                System.out.println("ログイン処理成功!!");
            } catch (ServletException ex) {
                System.out.println("ログイン失敗");
                // ログイン失敗時はログイン画面に戻る
                URI loginPage = uriInfo.getBaseUriBuilder()
                        .path(this.getClass())
                        .queryParam("retry", Boolean.TRUE)
                        .build();
                return Response.status(Response.Status.SEE_OTHER)
                        .location(loginPage)
                        .build();
            }
        }
        // ログイン成功時はインデックス画面に遷移
        URI indexPage = uriInfo.getBaseUriBuilder()
                .path(SecretResource.class)
                .path("index")
                .build();
        return Response.status(Response.Status.FOUND)
                .location(indexPage)
                .build();
    }

}

ログイン後のインデックス画面

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデックス画面</title>
    </head>
    <body>
        <h1>ようこそ、${pageContext.request.userPrincipal.name}さん!</h1>
        <a href="./admin">管理者専用画面へ</a>
        <hr>
        <form action="${pageContext.request.contextPath}/api/logout" method="post">
            <input type="submit" value="ログアウト">
        </form>
    </body>
</html>

管理者専用画面

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>管理者専用画面</title>
    </head>
    <body>
        <h1>管理者専用画面です。</h1>
        <a href="./index">インデックス画面へ</a>
    </body>
</html>

ログイン後の画面用コントローラーの作成

ここで、セキュリティアノテーションを使います。

インデックス画面は全ロールに許可するために@PermitAll、管理者専用画面は管理者のみ許可するために@RolesAllowedを付加します。

@Authenticatedは自作アノテーションなのですが、後ほど説明します。

package com.example.rest.resource;

import com.example.rest.filter.binding.Authenticated;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import org.glassfish.jersey.server.mvc.Viewable;

@Path("secret")
@Authenticated // ログインチェック用のフィルターが適用される。メソッドに個別につけてもOK
public class SecretResource {

    @GET
    @Path("index")
    @PermitAll // 全ロールに許可
    public Viewable index() {
        return new Viewable("index");
    }

    @GET
    @Path("admin")
    @RolesAllowed("ADMINISTRATOR") // 管理者のみ許可
    public Viewable admin() {
        return new Viewable("admin");
    }

}

ログインチェック用のフィルター作成

サーブレットフィルターではなく、JAX-RSフィルターです。

まず、このフィルターを適用する部分を決めるためのアノテーションを作成します。

package com.example.rest.filter.binding;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.ws.rs.NameBinding;

@NameBinding // これがポイント!
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authenticated {}

@NameBindingを付加しているのがポイントです。

そして、フィルターにはこのアノテーションを付加します。

package com.example.rest.filter;

import com.example.rest.filter.binding.Authenticated;
import java.io.IOException;
import javax.annotation.Priority;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;

@Provider
@Authenticated // このアノテーションが付いているメソッドにのみ、このフィルターが適用される
@Priority(Priorities.AUTHENTICATION) // 優先度をAUTHENTICATION(=1000)にする
public class AuthenticationFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        SecurityContext securityContext = requestContext.getSecurityContext();
        // ログインしてなかったら401例外をスロー
        if (securityContext.getUserPrincipal() == null) {
            throw new NotAuthorizedException("not login");
        }
    }

}

もう1つのポイントは、@Priority(Priorities.AUTHENTICATION)です。

JAX-RSフィルターは、複数のフィルターが定義されていた場合の実行順序を指定できます。

Priorities.AUTHENTICATIONは「1000」という整数値です。

@Priorityに指定した整数値が小さいものから先に、フィルターが実行されます。

最初に設定クラスを作成した時、RolesAllowedDynamicFeatureクラスを登録しました。

このクラスを登録すると、@RolesAllowedによる権限チェックを行うJAX-RSフィルターも登録されます。

(この権限チェックフィルターは、権限なしの場合は403例外(ForbiddenException)をスローします。)

で、この権限チェックフィルターのPriorityは、Priorities.AUTHORIZATION(=2000)が指定されているんですね。

ログインチェック(=認証)は、権限チェック(=認可)よりも先に行うべきです。

なので、ログインチェックフィルターの優先度はPriorities.AUTHENTICATION(=1000)にしています。

(ちなみに、@Priotityアノテーションを付加しない場合、デフォルトではPriorities.USER(=5000)となります)

ExceptionMapperの作成

ログインチェックフィルターと権限チェックフィルターで発生させる、NotAuthorizedExceptionとForbiddenExceptionをキャッチするExceptionMapperを作ります。

それぞれ、401.jspと403.jspにフォワードします。

package com.example.rest.exception.mapper;

import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.server.mvc.Viewable;

@Provider
public class NotAuthorizedExceptionMapper implements ExceptionMapper<NotAuthorizedException> {

    @Override
    public Response toResponse(NotAuthorizedException exception) {
        return Response.status(exception.getResponse().getStatusInfo())
                .entity(new Viewable("/error/401")).build();
    }

}
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>401エラー</title>
    </head>
    <body>
        <p>ログインしていません。</p>
        <a href="${pageContext.request.contextPath}/api/login">ログイン画面へ</a>
    </body>
</html>
package com.example.rest.exception.mapper;

import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.server.mvc.Viewable;

@Provider
public class ForbiddenExceptionMapper implements ExceptionMapper<ForbiddenException> {

    @Override
    public Response toResponse(ForbiddenException exception) {
        return Response.status(exception.getResponse().getStatusInfo())
                .entity(new Viewable("/error/403")).build();
    }

}
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>403エラー</title>
    </head>
    <body>
        <p>アクセス権がありません。</p>
        <a href="${pageContext.request.contextPath}/api/secret/index">インデックス画面へ</a>
    </body>
</html>

ログアウト用コントローラーの作成

package com.example.rest.resource;

import java.net.URI;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

@Path("logout")
public class LogoutResource {

    @Context
    private HttpServletRequest httpServletRequest;
    @Context
    private UriInfo uriInfo;

    @POST
    public Response logout() throws ServletException {
        // ログアウト
        httpServletRequest.logout();
        // セッション破棄
        HttpSession session = httpServletRequest.getSession();
        if (session != null) {
            session.invalidate();
        }
        // ログイン画面にリダイレクト
        URI loginPage = uriInfo.getBaseUriBuilder()
                .path(LoginResource.class)
                .build();
        return Response.status(Response.Status.SEE_OTHER)
                .location(loginPage)
                .build();
    }
}

実行

管理者権限でログインした場合

f:id:MasatoshiTada:20160130173451p:plain

f:id:MasatoshiTada:20160130173532p:plain

f:id:MasatoshiTada:20160130173558p:plain

普通にアクセス可能です。

管理者以外でログインした場合

f:id:MasatoshiTada:20160130173648p:plain

f:id:MasatoshiTada:20160130173707p:plain

f:id:MasatoshiTada:20160130173726p:plain

管理者専用画面にアクセスすると、「アクセス権がありません。」というエラー画面に遷移します。

未ログイン状態でURLを直接していいた場合

f:id:MasatoshiTada:20160130173934p:plain

f:id:MasatoshiTada:20160130174000p:plain

インデックス画面・管理者専用画面ともに、「ログインしていません。」というエラー画面に遷移します。

まとめ

  • ResourceConfigサブクラスにRolesAllowedDynamicFeatureの登録が必要
  • コントローラーメソッドには@RolesAllowedなどで権限を指定する
  • ログインチェックフィルターには@Priority(Priorities.AUTHENTICATION)を指定する

「Seasar2が終わる!」と慌てるべきではない理由。「Seasar2」という言葉をしっかり定義しよう

2016年9月26日、Seasar2のサポートが終了します。

Seasar2から卒業しよう - DJ HIGAYASUWO (元ひがやすを)

僕自身、大好きなフレームワークです。

EclipseプラグインDoltengなど、開発環境までそろったしっかりそろっているフレームワークって、今でもなかなか無いのではと思います。

仕事の方でも、「Struts 1もSeasar2も開発が終了してしまったので、Java EEやSpringに移行したいと考えています」とご相談いただくことが多いです。

しかし、まずは冷静になる必要があります。

Seasarプロジェクトのすべてが開発終了する訳ではない

Seasar2」という言葉は、DIコンテナ「S2Container」を指すこともあれば、「S2Container + S2JDBC + SAStruts」を指すこともあれば、Seasarプロジェクト全体(http://www.seasar.org/)を指すこともあります。

seasar.orgで紹介されているすべてのプロダクトが開発を終了する訳ではありません。

僕の記憶が確かならば、すでにDomaDBFluteは、コミッターの方が「今後も開発は続ける」(というよりだいぶ前にSeasarから独立している)と宣言されていたはずです。

他にも、そのようなプロダクトは多いのではないでしょうか。

必ず、ご利用しているプロダクトの公式サイトやコミッターの方のブログなどを確認して、今後も開発が続くものと、そうでないものを分けて考えてください。

ひがさんが明言されているのは、

2016/9/26にSeasar2S2JDBCSAStrutsのメンテナンスを現在のコミッタが終了するのは決定で、これは変わりません。 続Seasar2から卒業しよう - DJ HIGAYASUWO (元ひがやすを)

ということだけです。

(この文面における「Seasar2」とは、文脈からおそらく「S2Container」を指すと思われます)

2016/03/25追記

http://www.seasar.orgのトップページに、プロダクトに関する記述が追記されました。

EOLとなるのは以下を除いた全プロダクトです。

DBFlute

DBFlute.NET

Doma

Emecha

Mayaa

S2Container.NET

S2Dao.NET

S2Container + S2JDBC + SAStrutsが使えなくなる訳ではない

これらはオープンソースです。ソースコードGitHubに公開されており、Maven経由で利用も可能です。

2016年9月以降に、どのような形で公開されるかはまだ未定のようですが、公開自体は続くでしょう。

[Seasar-user:22108] Re: Seasar2メンテナンス終了

Mavenリポジトリ、ドキュメント、MLなどがどうなるのかは、現在話し合っている最中です。方向性としては、現在、Seasar2を利用している人々に、最も影響の少ない選択肢が選ばれるはずです。 続Seasar2から卒業しよう - DJ HIGAYASUWO (元ひがやすを)

オープンソースなので、何かあった(バグやセキュリティ脆弱性など)場合、自分で修正することができます。

特にJava SE 8対応関連や、Struts 1.xの脆弱性関連については、常に気を配っていたほうがよいでしょう。

メーリングリストの過去ログ(http://ml.seasar.org/archives/seasar-user/)から、関連しそうなものを下記にまとめましたので、ご確認ください。

Doltengでのプロジェクト作成がJava SE 8では出来ないものの、動作自体の問題は報告されていないようです(Javassistのバージョンを上げげる必要はあるようです)。

Java SE 8対応関連

[Seasar-user:22108] Re: Seasar2メンテナンス終了

[Seasar-user:22057] Re: Java8でのSAStrutsを使用する方法

[Seasar-user:21982] Re: SAStrutsの今後について

[Seasar-user:21953] Re: java8について

Struts 1.x脆弱性関連

[Seasar-user:22048] Re: Validator に入力値検査回避の脆弱性

[Seasar-user:21904] Re: Apache Struts 1脆弱性について

[Seasar-user:21902] StrutsのClassLoader脆弱性はSAStrutsに影響しません

[Seasar-user:21909] Re: JavaBeansに対するリフレクションとClassLoader脆弱

Java SE 9以降への対応

2017年にJava SE 9がリリースされます。

これが何を意味しているかというと、その1年後の2018年に、Oracle社によるJava SE 8の無償サポートが終了する可能性が高いことを意味します。

(2014年にJava SE 8がリリースされた時も、その1年後の2015年に、Java SE 7の無償サポートが終了しました)

これも基本的には、ご自分でメンテナンスする必要があります。

S2Container + S2JDBC + SAStrutsを使わないほうがよいケース

一言で言うと、自分でメンテナンスする工数が割けない場合です。

この場合は、素直にSpringなりJava EEなりに移行したほうが良いでしょう。

メンテナンスができない場合は、少なくとも新規開発でS2JDBC + SAStrutsの採用はやめた方がよいでしょう。

SAStruts + S2JDBCから移行するなら?

僕が知っている範囲では、単純に機能面だけ見れば、SAStruts + S2JDBCに近いのは「Spring MVC + Doma」だと思っています。

もしくは、Domaの代わりに@cero_tさんのBootiful SQL Templateを使うか。

Bootiful SQL Templateという名前にしてMavenリポジトリで公開しました。 - 谷本 心 in せろ部屋

Java EEは、SAStruts + S2JDBCとの機能的な近さだけで言えば、Springには劣ります。

Java EEは、アプリケーションサーバーのサポートや長期的な仕様の安定性が魅力なので、これらのメリットを享受したいならばJava EEもアリかと思います。

その場合、研修は私にお任せくd(ry

まとめ

  • 開発が終了するプロジェクトと、そうでないプロジェクトをしっかりと区別する必要があります。「Seasar2が終わってしまう」とひとくくりにして慌てる必要はありません。
  • S2Container、SAStrutsS2JDBCを使い続ける場合、ご自分でメンテナンスをする必要があります。
  • メンテナンスの工数が割けない場合は、S2Container、SAStrutsS2JDBCの利用はやめた方がいいです。

以上です。

最後に、ありがとう、Seasar2。今も大好きです。

DBから取ってきたエンティティをビューやRESTに直接渡すべきではない理由?

とりあえず、見つけた記事を自分用にメモ。

極北データモデリング

パトリオットミサイル: プログラマーの雑記帳

知りたいこと

  • なぜエンティティをDTOに詰め替えすべきなのか?
  • 詰め替えはどのクラスでやるべきなのか?コントローラー?ビジネスロジック?DAO?その他?

研修ばかりやってると、エンティティを直接ビューで表示するようなプログラムばかりなので・・・

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標準にどこまでこだわるか、プロジェクトの事情を考慮して、利用を検討してください。

@Transactional(Spring Framework)のpropagation属性

表題の件を調べていて、ほぼJava EEJTAと同じかと思いきや、NESTEDというJTAには無いものがあったのでまとめました。

@Transactionalとは?

メソッドに付加すると、メソッドの開始がトランザクションの開始、メソッドの終了がトランザクションの終了になります。

メソッド内で非チェック例外(RuntimeException及びそのサブクラス)が発生した場合はロールバックされます。

チェック例外の場合はロールバックされません。

具体的なコードで説明します。

実際にDBにアクセスするTxTestRepositoryクラス(以下「リポジトリ」)があり、それを呼び出しているTxTestService1(以下「サービス1」)とTxTestService2(以下「サービス2」)があります。

そして、サービス1はサービス2も呼び出しているとします。

@Repository
public class TxTestRepository {

    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;

    public void insert(int id) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        params.put("created_at", new Date());
        jdbcTemplate.update("INSERT INTO tx_test VALUES(:id, :created_at)", params);
    }
}
@Service
public class TxTestService2 {

    @Autowired
    private TxTestRepository repository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {

    @Autowired
    private TxTestRepository repository;
    @Autowired
    private TxTestService2 service2;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1);
        service2.insert(2);
        repository.insert(3);

    }

サービス1のinsertメソッドを呼び出すと、「1」と「3」はリポジトリ経由で、「2」はサービス2経由でDBに追加されます。

問題は、サービス1からサービス2を呼び出した時に、この2つが同一トランザクションなのか、別のトランザクションなのかです。

それを決めているのが、@Transactionalのpropagation属性です。

REQUIRED

上記のコードでは、サービス1のpropagationはREQUIRED、サービス2もREQUIREDです。

REQUIREDの場合、そのメソッドが呼び出された時にトランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用します。

上記のコードの場合、まずサービス1のinsertメソッドが呼ばれたら、トランザクションが開始されます。

その中でサービス2のinsertメソッドを読んだ時に、サービス1のトランザクションがそのまま利用されます。つまり、サービス1とサービス2は同一トランザクションです。

ここで、サービス2の中で、わざと例外をスローしてみます。

propagationはREQUIREDのままです。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert(int id) {
        repository.insert(id);
        throw new RuntimeException("ERROR 2");
    }
}

サービス1では、service2.insert(2)をtry-catchで囲みます。

こちらも、propagationはREQUIREDのままです。

@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // ロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
    }
}

こうすると、サービス2が非チェック例外発生→ロールバックしたら、同一トランザクションであるサービス1もロールバックされます。

すなわち、追加される件数は0件です。

例外をサービス1で発生させても同じです。

サービス2では例外が発生していないので、サービス2側はコミットされるかと思いきや、サービス1側で例外が発生してロールバックされるので、同一トランザクションであるサービス2で追加したレコードもロールバックされます。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // ロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
        throw new RuntimeException("ERROR 1");
    }
}

REQUIRES_NEW

REQUIRES_NEWの場合、そのメソッドが呼び出された時にトランザクションが開始されていようがなかろうが、常に新規のトランザクションを開始します。

サービス2のみをREQUIRES_NEWに変更し、サービス2内で例外をスローしてみます。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insert(int id) {
        repository.insert(id);
        throw new RuntimeException("ERROR 2");
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // コミットされる
        try {
            service2.insert(2); // これはロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // コミットされる
    }
}

こうすると、サービス1とサービス2は別のトランザクションなので、サービス2で例外発生→ロールバックされても、サービス1には影響がありません。

よって、サービス2で追加した「2」だけロールバックされ、サービス1で追加した「1」「3」はコミットされます。つまり、追加される件数は2件です。

サービス1で例外が発生した場合、サービス1で追加した「1」「3」はロールバックされ、サービス2で追加した「2」はコミットされます。つまり、追加される件数は1件です。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // これはコミットされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
        throw new RuntimeException("ERROR 1");
    }
}

NESTED

NESTEDの場合、REQUIREDと同様に、そのメソッドが呼び出された時にトランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用します。

しかし、「部分的ロールバック」が可能になっており、サービス2で例外発生→ロールバックされても、サービス1はロールバックされません。

部分的ロールバックについてはこちら→http://qiita.com/yuba/items/9b5b86bc3e128a84db5e

内部的には、JDBC 3.0以降の機能であるjavax.sql.Savepointを利用しているようです。

Savepointについてはこちら→http://java-reference.sakuraweb.com/java_db_savepoint.html

サービス2のみをNESTEDに変更し、サービス2内で例外をスローしてみます。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.NESTED)
    public void insert(int id) {
        repository.insert(id);
        throw new RuntimeException("ERROR 2");
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // コミットされる
        try {
            service2.insert(2); // これはロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // コミットされる
    }
}

サービス1とサービス2は同一トランザクションですが、サービス2には部分的ロールバックが適用され、サービス1には影響がありません。

よって、サービス2で追加した「2」だけロールバックされ、サービス1で追加した「1」「3」はコミットされます。つまり、追加される件数は2件です。

サービス1で例外が発生した場合、サービス1で追加した「1」「3」はロールバックされるのは当然ですが、あくまで同一トランザクションなので、サービス2で追加した「2」もロールバックされます。つまり、追加される件数は0件です。

ここが、完全に別トランザクションであるREQUIRES_NEWと違うところですね。

@Service
public class TxTestService2 {
    ...
    @Transactional(propagation = Propagation.NESTED)
    public void insert(int id) {
        repository.insert(id);
    }
}
@Service
public class TxTestService1 {
    ...
    @Transactional(propagation = Propagation.REQUIRED)
    public void insert() {
        repository.insert(1); // ロールバックされる
        try {
            service2.insert(2); // これもロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされる
        throw new RuntimeException("ERROR 1");
    }
}

ちなみに、サービス1とサービス2をNESTEDにした場合でも、追加件数は0件です。

部分的ロールバックというのは、あくまで内部トランザクション(今回はサービス2)がロールバックされた時に適用され、外部トランザクション(今回はサービス1)がロールバックされた場合は、内部トランザクションも一緒にロールバックされます。

NESTEDの注意点

http://docs.spring.io/spring-framework/docs/4.2.x/javadoc-api/org/springframework/transaction/annotation/Propagation.html#NESTED

Javadocを読むと、「特定のトランザクションマネージャでのみ動作する」と書いてあります。

http://docs.spring.io/spring-framework/docs/4.2.x/javadoc-api/org/springframework/jdbc/datasource/DataSourceTransactionManager.html

DataSourceTransactionManagerの場合は「nestedTransactionAllowedはデフォルトでtrue」と書かれていますが、

http://docs.spring.io/spring-framework/docs/4.2.x/javadoc-api/org/springframework/orm/jpa/JpaTransactionManager.html

JpaTransactionManagerの場合は「nestedTransactionAllowedはデフォルトでfalse」と書かれています。

他のトランザクションマネージャの種類によって違うっぽいので注意ですね。

参考資料

Spring Framework Reference

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/transaction.html#tx-propagation

Java EE 7 Tutorial (JTA)

https://docs.oracle.com/javaee/7/tutorial/transactions003.htm

Java EEチュートリアルなのでNESTEDの説明はありませんが、Table 51-1にその他の属性とトランザクションが新規か否かの関係がまとめられており、分かりやすいです。

Java EEハンズオン事前準備手順(JJUG CCC 2015 Fall) #jjug_ccc #ccc_m1

JJUG CCC 2015 Fallが近づいてきました。

今回、僕は2時間のJava EEハンズオンを担当することになりました。

■脱Struts&独自フレームワーク!今からでも遅くないJava EE Web開発入門ハンズオン!

http://www.java-users.jp/?page_id=2064#M-1

このハンズオンは、StrutsなどのWebフレームワークは経験があるけど、Java EEは初めてという方を対象にしていますので、「Java EEってそもそも何?」という方も歓迎です!

基礎からの講義・解説をしながらハンズオンを進めますし、演習をサポートするチューターも何人か参加しますので、Java EE初心者でも安心のセッションです!

内容は、Java EEのWebフレームワークである「JSF」「JAX-RS」「Jersey MVC」を、実際にご自分のPCでプログラムを作りながら学ぶものになっています。

ハンズオン課題は、DBと連携した基本的なCRUDアプリケーションです。

前半50分がJSF、10分休憩を挟んで、後半50分がJAX-RS + Jersey MVCです。

参加にあたって当日必要なもの

  • 事前準備済かつ十分に充電されたご自分のノートPC

事前準備について

ハンズオンに参加していただくにあたって、事前準備をお願いしています。

事前準備にはインターネット接続が必要ですが、会場にはWiFiはありません。また、ハンズオン当日は大人数なので、個人でWiFiルーターを持ち込まれる際も、混雑してつながりにくくなることが多いためです。

インターネット回線の速度やPCスペックにもよりますが、所用時間は30分前後です。

お手数ではありますが、下記の手順に沿ってご準備をお願いします。

  • 手順1. 開発環境をインストールする
  • 手順2. GitHubから演習用プロジェクトのクローンしてビルドする

事前準備についてご不明な点がありましたら、お気軽にご連絡ください。

ご連絡は、このブログ記事にコメントを書いていただくか、もしくは僕のTwitterアカウント@suke_masaまでよろしくお願いします。

手順1. 開発環境をインストールする

開発環境の概要

  • JDK 8u66
  • NetBeans 8.1 (8.0.2でも大丈夫です)
  • Payara Web ML 4.1.1.154
  • curl (MacLinuxの場合は、OSに組み込まれているのでインストールの必要なし)

データベースは、Payara内包のJavaDBを利用するので、インストールする必要はありません。

データベースのテーブル作成は、JPAスキーマ生成機能を利用するので、これも事前準備は必要ありません。

ビルドにはMavenGitHubからのクローンにはGitを使いますが、これらはNetBeansに内包されているので、インストールする必要はありません。

JDK

下記のWebサイトから、お使いのOSに合わせた「JDK 8u66」のインストーラーをダウンロードして、実行してください。

http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

環境変数JAVA_HOMEおよびPATHを設定してください。

NetBeans

下記のWebサイトから、お使いのOSに合わせたインストーラーをダウンロードして、実行してください。

ダウンロードバンドルは、「Java EE」または「すべて」を選択してください。

https://netbeans.org/downloads/

インストーラーを実行した後は、すべて「次へ」などでOKです。

Payara

Payaraは、英国C2B2社がGlassFish(Java EE参照実装サーバー)をベースに、バグフィックスや機能追加をして開発しているAPサーバーです。

使い方などは、GlassFishと全く変わりません。

下記のWebサイトから、「Payara-Web-ML 4.1.1.154 (Multi-Language Web Profile)」をダウンロードしてください。

http://www.payara.co.uk/payara_multi-language

ダウンロードしたZIPファイルは、適当なフォルダに展開してください。

次に、NetBeansにPayaraを認識させます。

NetBeansを起動したら、画面左の[サービス]タブを開き、[サーバー]を右クリック→[サーバーの追加]を選択します。

f:id:MasatoshiTada:20151115173716p:plain

[サーバー]で[GlassFish Server]を選択し、[名前]の欄には適当な名前を入力後、[次>]をクリックします。

f:id:MasatoshiTada:20151115175106p:plain

[インストール場所]の[参照]ボタンをクリックして、先ほどPayaraを展開したフォルダを選択します。僕の場合(下記の画像)ではフォルダ名を変えていますが、展開してそのままでは「payara41」というフォルダ名になっていますので、そのpayara41フォルダを選択してください。選択したら[ローカルドメイン]・[ライセンス契約を読んで同意しました...]を選択して、[次>]をクリックします。

f:id:MasatoshiTada:20151115175235p:plain

デフォルトで下記の画像のような状態になっていますので、このまま[終了]をクリックします。

f:id:MasatoshiTada:20151115175622p:plain

サーバーにPayaraが追加されました。

f:id:MasatoshiTada:20151115175821p:plain

curl

curlは、HTTPリクエストを送るための簡易ツールです。JAX-RSのハンズオンで利用します。

MacLinuxの場合は、OSに最初から組み込まれていますので、この手順はスキップしてください。

以下、Windows 7を前提に説明します。

下記のWebサイトから、ZIPファイルをダウンロードしてください。

http://curl.haxx.se/latest.cgi?curl=win64-nossl

ダウンロード後、ZIPファイルを適当なフォルダに展開してください。

その展開したフォルダ(curl.exeが入っているフォルダです)のパスを、環境変数PATHに追加してください。

コマンドプロンプトを開いて「curl」コマンドを実行し、下記のように表示されればOKです。

C:\User\User01 > curl
curl: try 'curl --help' or 'curl --manual' for more information

C:\User\User01 >

2. GitHubから演習用プロジェクトのクローンしてビルドする

クローン

今回、プロジェクトは4つ用意しています。

これらのプロジェクトをクローンして、ビルドするところまで実施してください。

これらはMavenプロジェクトで、初回ビルド時に依存ライブラリのダウンロードが実行されます。

GitHubアカウントの作成

今回のハンズオン用プロジェクトは、すべてGitHubにアップしています。

GitHubからプロジェクトをクローンするためには、ご自身のGitHubアカウントが必要になります。

お持ちでない方は、下記のURLからSign Upでアカウントを作成してください。

https://github.com

NetBeansGitHubからjjug-logicプロジェクトをクローン

NetBeansを開いたら、画面上部のメニューから[チーム]-[Git]-[クローン]と選択します。

f:id:MasatoshiTada:20151121172426p:plain

[リポジトリURL]に「https://github.com/MasatoshiTada/jjug-logic.git」と入力します。

[ユーザー]と[パスワード]は、ご自身のGitHubアカウント情報を入力します。

[クローン先]は、クローンしたプロジェクトを保存するローカルのフォルダ名を指定します。

入力したら、[次>]をクリックします。

f:id:MasatoshiTada:20151121172633p:plain

[master]が選択されていることを確認して、[次>]をクリックします。

f:id:MasatoshiTada:20151121173327p:plain

デフォルトのまま[終了]をクリックします。

f:id:MasatoshiTada:20151121173425p:plain

[プロジェクトを開く]をクリックします。

f:id:MasatoshiTada:20151121173613p:plain

画面上部のメニューから[表示]-[バージョン・ラベルを表示]を選択します。

f:id:MasatoshiTada:20151121173733p:plain

プロジェクトを右クリックして[依存性でビルド]を選択します。

f:id:MasatoshiTada:20151121173849p:plain

残り3つのプロジェクトをクローン

プロジェクトが何も選択されていない状態で、画面上部のメニューから[チーム]-[Git]-[クローン]と選択します。(一度、[プロジェクト]ウィンドウ内で右クリックすると、プロジェクトの選択が外れます)

f:id:MasatoshiTada:20151121175036p:plain

[リポジトリURL]に「https://github.com/MasatoshiTada/jjug-jsf.git」と入力します。

その他の情報は、先ほど入力したものが残っていると思いますので、そのまま[次>]をクリックします。

f:id:MasatoshiTada:20151121175224p:plain

exercisemasterという2つのリモート分岐がありますので、2つとも選択した状態にして[次>]をクリックします。

f:id:MasatoshiTada:20151121175406p:plain

デフォルトのまま[終了]をクリックします。

f:id:MasatoshiTada:20151121175646p:plain

[プロジェクトを開く]をクリックします。

f:id:MasatoshiTada:20151121175838p:plain

プロジェクトを右クリックして、[Git]-[分岐/タグ]-[分岐に切り替え]を選択します。

f:id:MasatoshiTada:20151121180048p:plain

[origin/exercise]を選択し、[新しい分岐としてチェックアウト]にチェックを入れて[切替え]をクリックします。

f:id:MasatoshiTada:20151121180232p:plain

プロジェクト名の横に[exercise]という分岐名が表示されていることを確認したら、プロジェクトを右クリックして[依存性でビルド]を選択します。

f:id:MasatoshiTada:20151121180652p:plain

プロジェクトを右クリックして[プロパティ]を選択すると、下記のようなプロパティのウィンドウが開きます。[実行]を選択し、[サーバー]で今回インストールしたPayara Web ML 4.1.1.154を選択後、[OK]をクリックします。

f:id:MasatoshiTada:20151121183905p:plain

残り2つのプロジェクトについても、以上の手順を行ってください。

リポジトリURLは下記の通りです。

https://github.com/MasatoshiTada/jjug-jax-rs.git

https://github.com/MasatoshiTada/jjug-jersey-mvc.git

NetBeansでTODOコメントを一覧表示

ハンズオン用のソースコードは、所々を穴埋め形式にして、その箇所はTODOコメントを書いてあります。

NetBeansのTODOコメント一覧表示機能を使うと、ダブルクリックでその場所にジャンプできるので、ハンズオン中はこの機能を使います。

NetBeans画面上部のメニューから[ウィンドウ]-[アクション項目]と選択します。

f:id:MasatoshiTada:20151121182417p:plain

黄色い四角のマークをクリックすると、現在選択中のプロジェクトのTODOのみが表示されます。

f:id:MasatoshiTada:20151121182942p:plain

漏斗(フィルター)のマークをクリックして、[編集]を選択します。[タイプ]で[TODO]のみにチェックを入れます。

f:id:MasatoshiTada:20151121183103p:plain

[説明]を1回クリックすると、TODOコメントが並び替えられて見やすくなります。

f:id:MasatoshiTada:20151121183329p:plain

スライドのダウンロード

当日は下記のスライドでセッションを進めます。あらかじめダウンロードしてあると、手元でも資料を確認することができます。

www.slideshare.net

何か分からないことがあったら

手順は以上になりますが、何か分からないことがあったら僕までご連絡ください。出来る限りご対応します。

この記事へのコメントかツイッター(@suke_masa)でご連絡ください。

(多分、ツイッターの方が僕が気付くのが早いと思います)

これ以上の予習などは必要ありませんが、もし事前に動作確認などを行いたい場合は、各プロジェクトの分岐を[master]に切替え、[消去してビルド]後に[実行]でデプロイできます。

それでは、皆様のご参加をお待ちしております!

【全部俺】JavaのIDE、どれを使う?【3大IDE頂上決戦】

皆さんはJavaIDE、何をお使いでしょうか。

2015年現在では、おそらく以下の3つのいずれかだと思います。

(ちなみに、僕自身が使った経験がほとんどないので、VimEmacsなどのエディタは今回のスコープから除外しますm( )m)

僕自身は、社会人になってからJavaを学び始め、新人研修はサクラエディタで受講し、それ以降はしばらく数年はEclipseのみを使っていました。

NetBeans歴は2年くらい、IntelliJ IDEA歴は1年くらいで、どのIDEも普段使いにはほぼ問題ない、というくらいのレベルです。

逆に言えば、どれもまだまだ使いこなせてないんじゃないかとも自分では思っています(^^;

その程度のレベルの僕ですが、現時点でどのような見解を持っているのか、それぞれのIDEの特徴を比較しつつ説明したいと思います。

Eclipse

良い点

  • プラグインが豊富
  • 様々な開発環境に対応可能
  • 日本語化が容易
  • 日本語情報も豊富、使っている人が多いのでノウハウが蓄積されている

なにがしかのフレームワークだったりAPサーバーだったりをサポートするプラグインは、三大IDEの中では真っ先に作られる印象です。

うーん?

  • 他のIDEと比較すると、補完能力が弱い
  • やや動きが重い

あくまで僕の感覚ですが、メモリ使用量はほぼ同じでも、IntelliJの方が軽快に動作する印象です。あまりスペックが高くないPCだと、その傾向が顕著です。

NetBeans

良い点

  • ウィザードが充実している
  • Java EEJavaFXのサポート機能が豊富
  • 日本語版がある(プラグイン追加すらしなくてOK)
  • 学習コストが低い

僕はJava EEをやりだした時はEclipseを使っていて、研修のコンテンツもすべてEclipseで作っていました。しかし、今一つやり辛さを感じていたので、全てNetBeansに置き換えました。もちろん最初は慣れませんでしたが、ものの数日で感覚は掴めました。

うーん?

  • プラグインが少ない
  • 多様な環境には対応できていない

例えば、Spring Bootをサポートするプラグインは、僕の知る限りありません。また、8.1が出るまでは、WildFly連携も不安定な感じでした。GlassFishなどのOracle製品との相性は良いです。

IntelliJ IDEA

良い点

  • 補完が至る所で効く
  • 何か書いた後のカーソル位置が「ここ!」というところに来てくれる
  • JavaScriptサポート機能も強い

一言で言うと「コーディングがとても快適!」です。この点は、EclipseNetBeansとは大きな差があります。

うーん?

  • ぜんぶ英語(今のところ日本語化は不可能)
  • コーディング以外の手順はやや複雑
  • 基本的に有償

英語であることを差し引いても、使い方の手順を身に付けるには時間がかかりました。4カ月くらいでようやっと慣れたかな?という感じでした。

あと、三大IDEの中で唯一有償です。無償バージョンもあるのですが、これはAPサーバー連携やらHTML/JavaScriptやらのWeb開発機能が付いていません。

で、結局どれを使う?

いま僕は、これらのIDEを使い分けています。

Eclipseは研修用です。既存の研修コンテンツがEclipseプロジェクトが多いのと、受講者の方が慣れているという理由です。あと、前述の通りNetBeansWildFly対応が今一つな感じがするので、Java EEでもWildFlyを使う時はEclipseです。

NetBeansは、Maven/GlassFish/JavaDB/JSF/JPAを使いたい時用です。

上記以外ではIntelliJ IDEAです。Gradleを使う時や、Java EEでもJPAを使わない(Domaなど)場合はIntelliJです。あとはSpringの場合もIntelliJです。

まとめ

どのIDEもそれぞれ良いところがあり、でも特定の環境だとやり辛さを感じたりで、まだどれか1つに統一できていないのが、僕の現状です。

ただ、もともとはEclipse一辺倒だった自分自身の反省なのですが、「食わず嫌い」ではもったいないと思います。

上述の通りEclipseにはEclipseの良さがあり、否定するつもりはありません。ただ、NetBeansIntelliJ IDEAも良いIDEであり、少しずつつまみ食いしながら知っていって、その上で自分の感覚や開発環境などに応じて、ひとりひとりのベストなIDEを選べばいいんじゃないかというのが、僕の意見です!

この記事を読んだ方の「ベストなIDE」選びの一助になれば嬉しいです。