Quantcast
Channel: 行きあたりばったりエンジニアの日記
Viewing all 88 articles
Browse latest View live

@Transactional(Spring Framework)のpropagation属性

$
0
0

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

@Transactionalとは?

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

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

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

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

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

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

@Repositorypublicclass TxTestRepository {

    @Autowiredprivate NamedParameterJdbcTemplate jdbcTemplate;

    publicvoid 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);
    }
}
@Servicepublicclass TxTestService2 {

    @Autowiredprivate TxTestRepository repository;

    @Transactional(propagation = Propagation.REQUIRED)
    publicvoid insert(int id) {
        repository.insert(id);
    }
}
@Servicepublicclass TxTestService1 {

    @Autowiredprivate TxTestRepository repository;
    @Autowiredprivate TxTestService2 service2;

    @Transactional(propagation = Propagation.REQUIRED)
    publicvoid 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のままです。

@Servicepublicclass TxTestService2 {
    ...@Transactional(propagation = Propagation.REQUIRED)
    publicvoid insert(int id) {
        repository.insert(id);
        thrownew RuntimeException("ERROR 2");
    }
}

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

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

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

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

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

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

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

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

REQUIRES_NEW

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

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

@Servicepublicclass TxTestService2 {
    ...@Transactional(propagation = Propagation.REQUIRES_NEW)
    publicvoid insert(int id) {
        repository.insert(id);
        thrownew RuntimeException("ERROR 2");
    }
}
@Servicepublicclass TxTestService1 {
    ...@Transactional(propagation = Propagation.REQUIRED)
    publicvoid 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件です。

@Servicepublicclass TxTestService2 {
    ...@Transactional(propagation = Propagation.REQUIRES_NEW)
    publicvoid insert(int id) {
        repository.insert(id);
    }
}
@Servicepublicclass TxTestService1 {
    ...@Transactional(propagation = Propagation.REQUIRED)
    publicvoid insert() {
        repository.insert(1); // ロールバックされるtry {
            service2.insert(2); // これはコミットされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされるthrownew 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内で例外をスローしてみます。

@Servicepublicclass TxTestService2 {
    ...@Transactional(propagation = Propagation.NESTED)
    publicvoid insert(int id) {
        repository.insert(id);
        thrownew RuntimeException("ERROR 2");
    }
}
@Servicepublicclass TxTestService1 {
    ...@Transactional(propagation = Propagation.REQUIRED)
    publicvoid 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と違うところですね。

@Servicepublicclass TxTestService2 {
    ...@Transactional(propagation = Propagation.NESTED)
    publicvoid insert(int id) {
        repository.insert(id);
    }
}
@Servicepublicclass TxTestService1 {
    ...@Transactional(propagation = Propagation.REQUIRED)
    publicvoid insert() {
        repository.insert(1); // ロールバックされるtry {
            service2.insert(2); // これもロールバックされる
        } catch (Exception e) {
            e.printStackTrace();
        }
        repository.insert(3); // ロールバックされるthrownew 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 7でもアクションベースMVC! -MVC 1.0への移行を睨んだJersey MVCの活用- #javaee

$
0
0

この記事について

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

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

Java EEにおけるアクションベースMVC

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

しかし、JAX-RSの参照実装「Jersey」の独自機能である「Jersey MVC」であれば、現在の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 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にアップしていますので、こちらをご参照ください。

MasatoshiTada/JavaEEAdventCalendar2015-JerseyMVC · GitHub

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")
publicclass 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")
@RequestScopedpublicclass HelloResource {

    @Injectprivate HelloDto helloDto;

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

        // 本来の処理
        helloDto.setMessage("Hello, " + name);
        returnnew 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(入力画面)

<%@ taglibprefix="mytag"uri="http://example.com/myTag" %><%@ pagecontentType="text/html;charset=UTF-8"language="java" %><html><head><title>名前入力画面(絶対パス)</title><linkrel="stylesheet"href="../../css/style.css"></head><body><h1>名前を入力してください</h1><mytag:errors errorClass="error"/><formmethod="get"action="./result">名前:<inputtype="text"name="name"><inputtype="submit"value="送信"></form><ahref="./redirect">リダイレクト</a></body></html>

result.jsp(出力画面)

<%@ taglibprefix="c"uri="http://java.sun.com/jsp/jstl/core" %><%@ pagecontentType="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);
        returnnew 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")
@RequestScopedpublicclass HelloDto {
    private String message;

    public String getMessage() {
        return message;
    }

    publicvoid setMessage(String message) {
        this.message = message;
    }
}

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

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

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

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

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

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

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

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":
                thrownew NullPointerException("NULLPO!");
            case"myrun":
                thrownew MyRuntimeException("MyRuntime!");
            case"run":
                thrownew RuntimeException("Runtime!");
            case"io":
                thrownew IOException("IOE!");
            case"myex":
                thrownew MyException("MY EXCEPTION!");
            case"ex":
                thrownew Exception("EXCEPTION!");
        }

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

publicclass ErrorsHandler extends SimpleTagSupport {

    private String errorClass;

    publicvoid setErrorClass(String errorClass) {
        this.errorClass = errorClass;
    }

    @Overridepublicvoid 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>");
        } elseif (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>");
        }

    }

    privateboolean isValidationErrorList(Object model) {
        if (model instanceof List) {
            List list = (List) model;
            if ( ! list.isEmpty()) {
                Object firstElement = list.get(0);
                if (firstElement instanceof ValidationError) {
                    returntrue;
                }
            }
        }
        returnfalse;
    }
}
<%@ taglibprefix="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;

@Providerpublicclass ExceptionMapper implements javax.ws.rs.ext.ExceptionMapper<Exception> {

    @Overridepublic Response toResponse(Exception e) {
        Viewable viewable = new Viewable("/error/exception", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(viewable).build();
    }
}
// importは省略@Providerpublicclass MyExceptionMapper implements ExceptionMapper<MyException> {
    @Overridepublic Response toResponse(MyException e) {
        Viewable viewable = new Viewable("/error/exception", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(viewable).build();
    }
}
// importは省略@Providerpublicclass MyRuntimeExceptionMapper implements ExceptionMapper<MyRuntimeException> {
    @Overridepublic Response toResponse(MyRuntimeException e) {
        Viewable viewable = new Viewable("/error/exception", e.getMessage());
        return Response.status(Response.Status.BAD_REQUEST)
                .entity(viewable).build();
    }
}
// importは省略@Providerpublicclass RuntimeExceptionMapper implements ExceptionMapper<RuntimeException> {
    @Overridepublic 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() {
        returnnew Viewable("redirect2.jsp");
    }

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

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

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

Tomcatの場合

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

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

OracleWebLogic 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ビーンを使いましょう!
  • バリデーションと例外処理は、@ErrorTemplateExceptionMapperを併用しましょう!

最後に

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

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

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

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

$
0
0

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

極北データモデリング

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

知りたいこと

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

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

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

$
0
0

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」を指すと思われます)

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がリリースされます。

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

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。今も大好きです。

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

$
0
0

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><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")
publicclass 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")
publicclass RedirectServlet extends HttpServlet {

    @Overrideprotectedvoid 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フォルダに作成します。

<%@ taglibprefix="c"uri="http://java.sun.com/jsp/jstl/core" %><%@pagecontentType="text/html" pageEncoding="UTF-8"%><!DOCTYPE html><html><head><metahttp-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><formaction="./login"method="post">ログインID:<inputtype="text"name="loginId"><br>パスワード:<inputtype="password"name="password"><br><inputtype="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")
publicclass LoginResource {

    @Contextprivate SecurityContext securityContext;
    @Contextprivate UriInfo uriInfo;
    @Contextprivate HttpServletRequest httpServletRequest;

    @GETpublic Viewable index() {
        returnnew Viewable("login");
    }

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

}

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

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

管理者専用画面

<%@pagecontentType="text/html" pageEncoding="UTF-8"%><!DOCTYPE html><html><head><metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"><title>管理者専用画面</title></head><body><h1>管理者専用画面です。</h1><ahref="./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// ログインチェック用のフィルターが適用される。メソッドに個別につけてもOKpublicclass SecretResource {

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

    @GET@Path("admin")
    @RolesAllowed("ADMINISTRATOR") // 管理者のみ許可public Viewable admin() {
        returnnew 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)にするpublicclass AuthenticationFilter implements ContainerRequestFilter {

    @Overridepublicvoid filter(ContainerRequestContext requestContext) throws IOException {
        SecurityContext securityContext = requestContext.getSecurityContext();
        // ログインしてなかったら401例外をスローif (securityContext.getUserPrincipal() == null) {
            thrownew 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;

@Providerpublicclass NotAuthorizedExceptionMapper implements ExceptionMapper<NotAuthorizedException> {

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

}
<%@pagecontentType="text/html" pageEncoding="UTF-8"%><!DOCTYPE html><html><head><metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"><title>401エラー</title></head><body><p>ログインしていません。</p><ahref="${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;

@Providerpublicclass ForbiddenExceptionMapper implements ExceptionMapper<ForbiddenException> {

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

}
<%@pagecontentType="text/html" pageEncoding="UTF-8"%><!DOCTYPE html><html><head><metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"><title>403エラー</title></head><body><p>アクセス権がありません。</p><ahref="${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")
publicclass LogoutResource {

    @Contextprivate HttpServletRequest httpServletRequest;
    @Contextprivate UriInfo uriInfo;

    @POSTpublic 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)を指定する

指定したフォルダ配下の全.javaファイルのタブをスペースに一括変換する

Jersey Test + MockitoでJAX-RSリソースクラスの単体テスト

$
0
0

Jersey TestでJAX-RSリソースクラスの単体テストするときに、Mockitoでモックのビジネスロジックを差し込みます。

環境

  • Payara Web ML 4.1.1.161
  • Jersey 2.22.1

pom.xml

  • JUnit
  • Jersey Test
  • Jersey Testの実行環境
  • Mockito

が必要です。

jersey-test-framework-provider-grizzly2を依存性に含めると、Jersey Testと実行環境が両方入ります。

<properties><jersey.version>2.22.1</jersey.version><jee-webapi.version>7.0</jee-webapi.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><dependency><groupId>org.glassfish.jersey.media</groupId><artifactId>jersey-media-json-jackson</artifactId><scope>provided</scope></dependency><dependency><groupId>javax</groupId><artifactId>javaee-web-api</artifactId><version>${jee-webapi.version}</version><scope>provided</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><dependency><groupId>org.glassfish.jersey.test-framework.providers</groupId><artifactId>jersey-test-framework-provider-grizzly2</artifactId><scope>test</scope></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>1.10.19</version><scope>test</scope></dependency></dependencies>

テスト対象のリソースクラスなど

package sample;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
publicclass MyApplication extends Application {
     // 中身は空
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.MediaType;

@Path("/hello")
@Produces(value = MediaType.APPLICATION_JSON)
@RequestScopedpublicclass HelloResource {

    // テスト時はここをモックに差し替える@Injectprivate HelloLogic helloLogic;

    @GETpublic Response getAll() throws Exception {
        List<HelloDto> list = helloLogic.selectAll();
        return Response.ok(list).build();
    }
}
package sample;

publicclass HelloDto {
    private String name;
    // setter/getter/コンストラクタ省略
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;

@RequestScopedpublicclass HelloLogic {
    public List<HelloDto> selectAll() {
        // 本来は何らかの複雑な処理があると考えてくださいreturn Arrays.asList(new HelloDto("AAA"), new HelloDto("BBB"), new HelloDto("CCC"));
    }
}

テストクラスの作成

package sample;

import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.utilities.Binder;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;

publicclass HelloResourceTest extends JerseyTest {

    /**      * HelloLogicのモックのファクトリークラス*/privatestaticclass MockHelloLogicFactory implements Factory<HelloLogic> {
        @Overridepublic HelloLogic provide() {
            // mock() / when() / thenReturn() はMockitoのメソッド
            HelloLogic mockLogic = mock(HelloLogic.class);
            when(mockLogic.selectAll()).thenReturn(Arrays.asList(
                    new HelloDto("仮のA"),
                    new HelloDto("仮のB")
            ));
            return mockLogic;
        }

        @Overridepublicvoid dispose(ProductQueryDao instance) {
            // 何もしない
        }
    }

    @Overrideprotected Application configure() {
        Binder binder = new AbstractBinder() {
            @Overrideprotectedvoid configure() {
                // bindFactory() + to() でモックに差し替え
                bindFactory(MockHelloLogicFactory.class)
                        .to(HelloLogic.class);
            }
        };
        returnnew ResourceConfig()
                .packages(true, MyApplication.class.getPackage().getName())
                .register(binder);
    }

    @Testpublicvoid全件取得でき件数が2件() {
        List<HelloDto> list = target("hello")
                .request()
                .get(new GenericType<List<HelloDto>>(){});
        assertThat(list.size(), is(2));
    }
}

ポイントは、ファクトリークラスを作成することと、bindFactory()メソッドを利用すること。

下記の@backpaper0さんの資料にあるbind()メソッドは、Javadoc

Does NOT bind the service type itself as a contract type.

とあり、Mockitoで作ったmockLogicを引数に指定すると、例外でテストがこけた。

bind()の引数はインタフェースのみ指定可能なのかも?

参考資料

Mockito 初めの一歩 - Qiita

java - Mocking EJB's with Mockito and Jersey Test Framework - Stack Overflow

http://backpaper0.github.io/ghosts/jaxrs-getting-started-and-practice.html#/12/8

AbstractBinder (HK2 API module 2.2.0-b11-SNAPSHOT API)

Factory (HK2 API module 2.2.0-b11-SNAPSHOT API)

Payara 161.1 で @Transactional のバグが修正されました!

$
0
0

この記事は、半分ポエムです。

最初のきっかけ

下記の@opengl_8080さんの記事でした。

JavaEE使い方メモ(JTA) - Qiita

CDIビーンのメソッドに@Transactionalアノテーションを付加しても、非チェック例外発生時にロールバックされない、というバグがあるのこと。

JTAの仕様としては、チェック例外ならばコミット、非チェック例外ならばロールバックされます)

早速、自分でも試してみたのですが、やっぱりロールバックされませんでした。

はじめてのISSUE報告(2015/11/1)

僕は、トランザクション管理はJava EEの要だと思っていますので、PayaraのGitHubにISSUEを書きました。

英語あんまり得意じゃないですが・・・(^^;

テスト用プログラム(上記の@opengl_8080さんの記事を参考に作りました)

GitHub - MasatoshiTada/TransactionalSample-Doma

ISSUE

Payara does NOT rollback when RuntimeException occurs in CDI @Transactional method using JDBC · Issue #505 · payara/Payara · GitHub

いま読み返しても、ひどい英語だ・・・。でも、もうこれは気合いでした。

Payaraの中の人から反応が!(2015-11-15)

Payaraの中の人たちは、すぐに反応してくれました。

テスト用プログラムのおかげで再現性がすぐに認められ、バグ修正も速やかに行われました!

PAYARA-510 fixes 505 ensures resources are enlisted into the transaction by smillidge · Pull Request #524 · payara/Payara · GitHub

最初にバグを見つけたのは@opengl_8080さんですし、僕はISSUEを書いただけです。

しかし、僕は現役のエンジニアではなく、僕が作ったプログラムが世の中で動くわけではないので、

本当に、ほんのちょっとでもPayaraに貢献できたことが、本当に嬉しかったんです。

修正されたPayaraがリリース!しかし・・・(2016-01)

上記の修正は、Payara 4.1.1.161に含まれました。

ウキウキして早速試してみました。

すると・・・

直ってないやん(--;)

TxTypeREQUIREDの挙動は直っていたんですが、REQUIRES_NEWの挙動は直っていませんでした。

再びISSUE報告(2015-02)

数日後、改めてISSUEを書きました。

#505 is not fixed when TxType is REQUIRES_NEW · Issue #667 · payara/Payara · GitHub

すると、やはりすぐに反応が返ってきました。

前回のことを覚えてくれていたのか、すぐに修正も行われました。

ちなみに、2回とも修正対応してくれたsmillidgeさん、Payaraを開発している英国C2B2社の創業者の方だそうです。

もし今年のJava Day Tokyoとかにいらっしゃったりすれば、是非お話してみたいなあ・・・。

リリースが思わぬ速さで!(2015-03)

Payaraは通常、四半期に1回のリリースなんですが、たまに四半期の間でもパッチ版がリリースされます。

僕は、「修正版が出るのは次の四半期かなー」と思っていたんですが、つい先日、パッチ版のPayara 4.1.1.161.1がリリースされました!

(「1」が多いので、以降は「Payara 161.1」と書きます)

このリリースに、上記バグの修正が含まれています。

このバグ修正の何が嬉しいのか?

JPA以外のORマッパーをPayara上で使いつつ、かつ@Transactionalでトランザクション管理を行うことができます。

(上記バグは、以前からJPAでは発生しませんでした)

最近、「100%Java EE標準」にこだわらないのであれば、JPA以外のORマッパー(Doma・MyBatis・jOOQ・DBFluteなど)を使う選択肢もアリだと思っています。

このような構成でも、安心してPayaraを使うことができます。

最後に

このバグを見つけてくれた@opengl_8080さんには、本当に感謝です。

繰り返しますが、僕はISSUEを書いただけです。

でも、将来的には、自分でバグ修正までしてプルリクエストを送れるくらいになりたいなあ・・・。


Jersey MVCのレスポンスのContent-Typeが「*/*」になる問題対策

$
0
0

詳細は後ほど追記します。

ContianerResponseFilterだと何故かできなかった・・・。

Jersey MVCは、MessageBodyWriterの中でContent-Typeを上書きしています。

ContianerResponseFilterはMessageBodyWriterよりも前に実行されるので、そりゃあ効かないですね・・・。

なので、必ずWriterInterceptorの「後処理」として書く必要があります。

MessageBodyWriterでなぜ「*/*」に上書きされるのかは、現在ソースを読んで調査中です。

package com.example.rest.interceptor;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;

@Providerpublicclass ContentTypeWriterInterceptor implements WriterInterceptor {
    @Overridepublicvoid aroundWriteTo(WriterInterceptorContext context) 
            throws IOException, WebApplicationException {
        // デバッグログ
        System.out.println("======== BEFORE : " + context.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
        // レスポンスへの書き込み実行(必須)
        context.proceed(); 
        // デバッグログ
        System.out.println("======== AFTER : " + context.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
        // レスポンスヘッダーに "Content-Type: text/html"を上書きする
        MultivaluedMap<String, Object> headers = context.getHeaders();
        headers.putSingle(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML);
    }
}

TOEIC受験記(2016年3月)

$
0
0

なぜ受験しようと思ったか

僕の仕事は研修講師(主にプログラミング)なので、色々と調べ物をする時にどうしても英語の仕様書・書籍・ブログ記事などを読む必要が出てきます。やはり、ITの本場はアメリカです。

なので、もっとリーディング力を高めたいと思ったのがきっかけです。

勉強前の英語レベルは?

中学までは英語は得意科目でしたが、高校になるとマークシート形式ならなんとか、という感じでした(センター試験だと8〜9割くらい)。でも、記述式はまったくダメでした。

10年くらい前、まだ大学生の頃、試しにTOEICを受けたことがありましたが、たしか440〜450点だったと思います。

結果は?

Listening Reading Total
395 430 825

勉強法は?

僕の勉強法はいたってシンプルで、市販書籍でインプットとアウトプットをひたすら繰り返すのみです。

勉強時間は、主に通勤時間と土日です。家族に感謝。

改訂版 TOEIC(R) TEST 英文法 出るとこだけ!

Amazon.co.jp | 音声DL付 改訂版 TOEIC(R) TEST 英文法 出るとこだけ! | 本 ・TOEIC 通販

まずはじめに読んだ本です。文法のインプット・アウトプットに使いました。ページは少ないけど文法を理解するコツがしっかりと解説されていて、記憶力も根気も無い自分にはピッタリの本でした。あえて受験テクニック的な見出しが付いていますが、文法的な背景も解説されています。

僕は暗記物がとても苦手なので、いきなり単語集をやると確実に挫折します。というか、実際に挫折しました・・・。なので、文法で理論的な部分をある程度身につけてから、単語に取り掛かりました。その方がモチベーションも続きます。

この本は3回くらい繰り返しました。

改訂版 TOEIC(R)TEST 英単語 出るとこだけ!

Amazon.co.jp | CD-ROM付 改訂版 TOEIC(R)TEST 英単語 出るとこだけ! | 本 ・TOEIC 通販

文法の勉強をしたはいいが、やはり単語が分からないことにはリーディングができないことに気付き、文法の本と同じ小石裕子さんの本を読みました。

TOEICは出る単語が(文法もですが)決まっている感じがするので、TOEICを受けるならTOEIC専用の単語集を使ったほうがいいと思います。

この本は、5回くらい繰り返しました。

TOEICテスト公式プラクティス リーディング編

Amazon.co.jp | TOEICテスト公式プラクティス リーディング編 | 本 ・TOEIC 通販

TOEICを実施しているETSが出している、公式の練習問題集です。これは2回くらい繰り返しました。

改訂版 TOEIC(R) TEST リスニング 出るとこだけ!

Amazon.co.jp | CD付 改訂版 TOEIC(R) TEST リスニング 出るとこだけ! | 本 ・TOEIC 通販

今回はリーディングに絞って勉強するつもりだったのですが、時間に少し余裕が出来たので、直前の2週間だけリスニングの勉強をしました。

読んだのは、やはり小石裕子さんの本です。これは1回通りやるのが精一杯でした。

TOEICテスト新公式問題集< Vol.6>

Amazon.co.jp | TOEICテスト新公式問題集< Vol.6> | 本 ・TOEIC 通販

本番の試験と同じ形式の模試が、2回分入っています。1回目は普通に問題集として解き、2回目を実際の時間(2時間)を測って解きました。

下記サイトによると、Vol.6が最も本番の難易度に近いそうです。 僕はこのVol.6しか解いていませんが、確かに本番の試験と難易度は似ていると感じました。

TOEIC 公式問題集はどれから解けば良いの?

勉強してどうだったか

英語の文書を読むのが少し楽になったように感じます。

これは、勉強したことにより地力が鍛えられたこともありますが、スコアを取ったことによる自信も大きいと思います。

勉強する→結果が出る→自信がつく→モチベーションが湧く→勉強する→・・・

このサイクルを回し続けることが肝心です。

今後、また受験するかは未定ですが、英語文書をより読んでいって更に英語力を鍛えていこうと考えています。

使ってないけどやっておいたほうが良かったかなと思う本

TOEICテスト公式プラクティス リスニング編

Amazon.co.jp | TOEICテスト公式プラクティス リスニング編 | 本 ・TOEIC 通販

公式プラクティスのリスニング版です。今回は時間の都合上やりませんでした。

TOEICテスト 公式問題で学ぶボキャブラリー

Amazon.co.jp | TOEICテスト 公式問題で学ぶボキャブラリー | 本 ・TOEIC 通販

これも時間の都合上やりませんでした。

TOEICテスト新公式問題集〈Vol.4〉

Amazon.co.jp | TOEICテスト新公式問題集〈Vol.4〉 | 本 ・TOEIC 通販

前述のサイトによると、公式問題集の中でも一番難しいものだそうです。ハイスコアを目指す人はやっておくといいかもしれません。

改訂版 TOEIC(R) TEST 文法・語彙出るとこだけ! 問題集

Amazon.co.jp | 音声DL付 改訂版 TOEIC(R) TEST 文法・語彙出るとこだけ! 問題集 | 本 ・TOEIC 通販

これも時間の都合上やりませんでした。

TOEICテスト中学英文法で600点!

Amazon.co.jp | 新TOEICテスト中学英文法で600点! | 本 ・TOEIC 通販

中学英語に自信が無い人は、これから始めるといいかもしれません。

Cloud Foundryワークショップに参加してきました! #cfws

$
0
0

2016-04-05(火)に開催された、Pivotalさん主催のCloud Foundryワークショップに参加してきました!

クラウド関連のことはあまり知識がないので、クラウドの世界ってどんなものなんだろう?ということ知りたかったというのが動機です。

講師は@makingさん。冒頭は、3月のJJUGナイトセミナーの資料でCloud Foundryの概要説明でした。

理解できた範囲で要約すると、

  • IaaS上にPaaSを構築・管理するためのソフトウェア群
  • Pivotal社単独ではなく、「Cloud Foundry Foundation」という団体が仕様を策定している
  • Pivotal Web Servicesは、AWS上にCloud FoundryでPaaSを構築済みのもの。アカウントを作成すればすぐに利用できる
  • PCF Devは、ローカルPC上にCloud Foundry環境を構築するもの
  • Cloud Foundry上へのアプリケーションのデプロイなどは、CLIツールをインストールしてcfコマンドで行う

その他、Cloud Foundry内部の仕組みや、Blue-Green Deployなどの説明もありました。 (なんか理解が間違ってたらツッコミください・・・)

その後、下記の資料でハンズオン。

GitHub - Pivotal-Japan/cf-workshop: Cloud Foundry Workshop

Cloud Foundryワークショップ資料 - Qiita

(上記のGitHubとQiitaは、両方同じ資料です)

特に印象深かったのは、「5. スケールアウト」と「7. Blue-Greenデプロイ」です。

例えば、より多くのクライアントに対応するためにインスタンス数を増やして負荷分散したいという場合は、ローカルPCから下記のコマンドを叩くだけ。

$ cf scale -i 4 hello-redis-tada

上記だとインスタンス数が4になります。かかった時間も、ものの数秒でした。で、ロードバランシングとかはCloud Foundry内のRouterがよろしくやってくれる、と。

Blue-Greenデプロイは、名前だけは聞いたことがあったものの、どんなものかはよく知りませんでした。

新しいバージョンのアプリケーションをデプロイする際に、ダウン時間をなるべくゼロに近づけるための手法だと理解しました。

これも、Cloud Foundryを使えば非常に簡単ですね。

クラウドって実際に触ったことがない」という人にはとてもオススメのワークショップです!

これから何回か開催予定ということなので、是非参加してみてはいかがでしょうか。

ハンズオンはボリュームがそこそこありますので、当日は余裕を持って話を聞きたいという方は「3. 簡単なアプリケーションをデプロイ」くらいまでは事前に予習しておくといいかもしれません。

Jersey MVC + Thymeleafしたら文字化けた時の対策

$
0
0

環境

  • OS X 10.11.4
  • Google Chrome
  • Payara Web ML 4.1.1.161.1 (Jersey 2.22.1)
  • Thymeleaf 2.1.4.RELEASE

プログラム概要

@bufferingさんの記事を参考に、Thymeleaf用のTemplateProcessorを作成。

@Providerpublicclass ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    @Injectprivate javax.inject.Provider<Ref<HttpServletRequest>> requestProviderRef;
    @Injectprivate javax.inject.Provider<Ref<HttpServletResponse>> responseProviderRef;

    private TemplateEngine templateEngine;

    @Injectpublic ThymeleafTemplateProcessor(Configuration config, ServletContext servletContext) {
        super(config, servletContext, "html", "html");
        TemplateResolver templateResolver = new ServletContextTemplateResolver();
        templateResolver.setPrefix((String)config.getProperty(MvcFeature.TEMPLATE_BASE_PATH));
        templateResolver.setSuffix(".html");
        templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
    }

    @Overrideprotected String resolve(String templatePath, Reader reader) throws Exception {
        return templatePath;
    }

    @Overridepublicvoid writeTo(String templateReference, Viewable viewable, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException {
        HttpServletRequest httpServletRequest = requestProviderRef.get().get();
        HttpServletResponse httpServletResponse = responseProviderRef.get().get();
        WebContext webContext = new WebContext(
                httpServletRequest, httpServletResponse, 
                super.getServletContext(), httpServletRequest.getLocale());
        Object model = viewable.getModel();
        if (model instanceof Map) {
            Map<String, Object> map = (Map) model;
            webContext.setVariables(map);
        } else {
            Map<String, Object> variables = new HashMap<>();
            variables.put("model", viewable.getModel());
            webContext.setVariables(variables);
        }
        templateEngine.process(viewable.getTemplateName(), webContext, 
                httpServletResponse.getWriter());
    }

}

リソースクラス。

@Path("employee")
@RequestScoped@Produces(MediaType.TEXT_HTML)
publicclass EmployeeResource {
    
    @GET@Path("index")
    public Viewable index() throws Exception {
        returnnew Viewable("/employee/index");
    }
}

Thymeleafのビュー。

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"      xmlns:th="http://www.thymeleaf.org"><head><metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"/><title>入力画面</title><linkrel="stylesheet"href="../../css/style.css"/></head><body><p>社員IDを入力してください(77, 88, 99で例外発生)</p><!-- 検証エラーメッセージの表示 --><ulclass="error" th:if="${violations != null}"><li th:each="violation : ${violations}" th:text="${violation.message}">ダミーのメッセージ</li></ul><formaction="./result"method="get">社員ID:<inputtype="text"name="id"value="" th:value="${param.id == null} ? '' : ${param.id[0]}"/><inputtype="submit"value="検索"/></form></body></html>

現象

画面の全日本語メッセージが文字化けている。

f:id:MasatoshiTada:20160501145927p:plain

原因と対策

レスポンスヘッダーやブラウザが現在開いている文字コードを見ると、正しくUTF-8になっていた。

よって、そもそもレスポンスを書き込む際に文字コードが正しく設定されていないっぽい。

TemplateProcessorにhttpServletResponse.setCharacterEncoding("UTF-8");を追加したら、正しく表示された。

f:id:MasatoshiTada:20160501150201p:plain

@Providerpublicclass ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    // 中略@Overridepublicvoid writeTo(String templateReference, Viewable viewable, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException {
        HttpServletRequest httpServletRequest = requestProviderRef.get().get();
        HttpServletResponse httpServletResponse = responseProviderRef.get().get();

        // これを追加
        httpServletResponse.setCharacterEncoding("UTF-8");

        WebContext webContext = new WebContext(
                httpServletRequest, httpServletResponse, 
                super.getServletContext(), httpServletRequest.getLocale());
        Object model = viewable.getModel();
        if (model instanceof Map) {
            Map<String, Object> map = (Map) model;
            webContext.setVariables(map);
        } else {
            Map<String, Object> variables = new HashMap<>();
            variables.put("model", viewable.getModel());
            webContext.setVariables(variables);
        }
        templateEngine.process(viewable.getTemplateName(), webContext, 
                httpServletResponse.getWriter());
    }

}

ちなみに

Ozark + Thymeleafだと、この記述が無くても正しく表示された。

(ViewEngineクラスにもそれらしい記述は無い)

理由は不明。。。

Jersey MVCでThymeleafを使おう!

$
0
0

この記事について

JJUG CCC 2016 Spring「ネクストStruts/Seasr2としてのJava EEアクションベースMVC入門」の補足記事です。

Jersey MVCで、JSPの代わりのビューとしてThymeleafを使う方法を解説します。

Jersey MVCの基本については、下記の記事を参照してください。

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

環境

  • Payara Web ML 4.1.1.162
  • Thymeleaf 2.1.4.RELEASE

ソースコード

jjug-action-based-mvc/jjug-jersey-mvc at master · MasatoshiTada/jjug-action-based-mvc · GitHub

TemplateProcessorクラスを作る

Jersey MVCで公式にサポートされているビューはJSP/Freemarker/Mustacheの3種類です。

それ以外のビューを使うには、Jersey MVCで提供されているTemplateProcessorインタフェースを実装する必要があります。

Jersey MVCには、TemplateProcessorを実装したAbstractTemplateProcessorという抽象クラスがありますので、これを継承して作成します。

クラスの作成、リクエストとレスポンスの取得

@Providerpublicclass ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    @Injectprivate javax.inject.Provider<Ref<HttpServletRequest>> requestProviderRef;
    @Injectprivate javax.inject.Provider<Ref<HttpServletResponse>> responseProviderRef;
    // 後に続く

まず、AbstractTemplateProcessorを継承し、クラスには@Providerを付加します。

次に、フィールドインジェクションでHttpServletRequestHttpServletResponseを取得するのですが、@Providerが付加されたクラスはJAX-RSの仕様では、デフォルトでシングルトンと定められています。

なので、直接HttpServletRequestHttpServletResponseをインジェクションすると、スレッドセーフでない可能性があると考えました。

僕はスレッドセーフか否かといったあたりはあまり詳しくないので、直接取得でも大丈夫な可能性もありますが・・・。

そこで、Jersey MVCJSPTemplateProcessorである下記のクラスを参考に作りました。

jersey/JspTemplateProcessor.java at 2.x · jersey/jersey · GitHub

これで本当にスレッドセーフになっているのかは、僕には判断がつかないので、この点はご自分で判断してください。

TemplateEngineの作成

// 続きprivate TemplateEngine templateEngine;

    @Injectpublic ThymeleafTemplateProcessor(Configuration config, ServletContext servletContext) {
        super(config, servletContext, "html", "html");
        TemplateResolver templateResolver = new ServletContextTemplateResolver();
        templateResolver.setPrefix((String) config.getProperty(MvcFeature.TEMPLATE_BASE_PATH));
        // setSuffix()は指定しない
        templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
    }
    // 後に続く

フィールドにTemplateEngine(Thymeleafのクラス)を宣言し、コンストラクタ内で初期化します。

ポイントは、setSuffix()という、通常は拡張子を指定するメソッドを呼んでいないことです。

これは、MVC 1.0との互換性のために、コントローラーメソッド拡張子を指定したいからです。

メソッドのオーバーライド

// 続き@Overrideprotected String resolve(String templatePath, Reader reader) throws Exception {
        return templatePath;
    }

    @Overridepublicvoid writeTo(String templateReference, Viewable viewable, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException {
        HttpServletRequest httpServletRequest = requestProviderRef.get().get();
        HttpServletResponse httpServletResponse = responseProviderRef.get().get();
        httpServletResponse.setCharacterEncoding("UTF-8");
        WebContext webContext = new WebContext(
                httpServletRequest, httpServletResponse, 
                super.getServletContext(), httpServletRequest.getLocale());
        Object model = viewable.getModel();
        if (model instanceof Map) {
            Map<String, Object> variables = (Map<String, Object>) model;
            webContext.setVariables(variables);
        } else {
            Map<String, Object> variables = new HashMap<>();
            variables.put("model", model);
            webContext.setVariables(variables);
        }
        try (Writer writer = new OutputStreamWriter(out)) {
            templateEngine.process(viewable.getTemplateName(), webContext, writer);
        }
    }
}

resolve()は、引数のtemplatePathをそのまま返せばOKです。

writeTo()が、最終的にレスポンスを書き込むメソッドです。

webContext.setVariables()は、ビューに渡す値をマップ形式で指定します。

コントローラーメソッドViewableに渡された値は、viewable.getModel()で取得できます。

この値がMapだった場合は、それを直接webContext.setVariables()に渡し、そうでない場合はJersey MVCデフォルトのmodelという名前でMapにputしています。

こうすることで、ビューから値を参照するときに、${model.employee.id}のようにmodelを付けなくてもいいようにしています。

writeTo()の最後では、templateEngine.process()を呼んでいます。これが、Thymeleafのレスポンスを書き出しています。

自作Viewableクラスの作成

これはThymeleafを使う上では必須ではないのですが、MVC 1.0との差異をなるべく吸収するために作りました。

前述のブログの通り、Jersey MVCには「/」で始める絶対パスと「/」で始めない相対パスがあり、それぞれビューの保存フォルダが異なります。

ビューの保存フォルダがMVC 1.0とほぼ同じなのは絶対パスですが、MVC 1.0(というか現在のOzark)では「/」で始めるとコンテキストルートからのパスになってしまいます。

なので、「/」で始めない相対パスで指定しつつ、かつビューの保存フォルダが絶対パスになるように、Viewableのサブクラスを自作しました。

publicclass ThymeleafViewable extends Viewable {

    public ThymeleafViewable(String templateName) throws IllegalArgumentException {
        super(templateName);
    }

    public ThymeleafViewable(String templateName, Map<String, Object> models) throws IllegalArgumentException {
        super(templateName, models);
    }

    @Overridepublic Map<String, Object> getModel() {
        return (Map<String, Object>) super.getModel();
    }

    @Overridepublic String getTemplateName() {
        String templateName = super.getTemplateName();
        if (templateName.startsWith("/") == false) {
            return"/" + templateName;
        } else {
            return templateName;
        }
    }

    @Overridepublicboolean isTemplateNameAbsolute() {
        returntrue;
    }
}

コードを読んでいただければ大体わかると思いますが、getTemplateName()が必ずスラッシュで始まるパス(=絶対パス)を返すようにし、かつisTemplateNameAbsolute()trueを返して、Jersey MVC絶対パスと認識させます。

また、コンストラクタはビューのパスとMapを引数で受け取るようにします。

コントローラーメソッド

@GET@Path("index")
    public ThymeleafViewable index() throws Exception {
        returnnew ThymeleafViewable("employee/index.html");
    }
@GET@Path("result")
    public ThymeleafViewable result(@BeanParam EmployeeIdForm form) throws Exception {
        Integer id = Integer.valueOf(form.getId());
        Employee employee = employeeService.findByEmpId(id).orElse(null);
        HashMap<String, Object> models = new HashMap<>();
        models.put("employee", employee);
        returnnew ThymeleafViewable("employee/result.html", models);
    }

パスは「/」で始めない、MVC 1.0と同じ形式にしています。

また、拡張子「.html」を明示的に指定しています。これもMVC 1.0と同じです。

ビュー

<p th:if="${employee == null}">該当する社員はいませんでした。</p><tableborder="1" th:unless="${employee == null}"><tr><th>社員ID</th><th>氏名</th><th>入社年月日</th><th>部署ID</th><th>部署名</th></tr><tr th:object="${employee}"><td th:text="*{empId}">99999</td><td th:text="*{name}">Taro Yamada</td><td th:text="*{joinedDate}">2020-01-01</td><td th:text="*{department.deptId}">99</td><td th:text="*{department.name}">Admin</td></tr></table>

modelを指定せず、直接employeeで値を参照していることが分かります。

これにより、Jersey MVCからMVC 1.0に変更した際に、ビューを全く変更しなくて済むようにしています。

まとめ

TemplateProcessorと自作Viewableにより、極力MVC 1.0と同じ感覚で、ビューとコントローラーを実装できるようにしました。

参考にしたWebサイト

@bufferingsさんのブログ。

jersey-thymeleaf using ViewProcessor - Mitsuyuki.Shiiba

@backpaper0さんのコード。

sealion/ThymeleafProvider.java at master · backpaper0/sealion · GitHub

Payara Microの実行可能JAR(Uber JAR)がとても簡単な件

$
0
0

先日、Payara 4.1.1.162がリリースされました!

その組み込みサーバー版であるPayara Micro 4.1.1.162には、「Uber JAR」というSpring BootやWildFly Swarmのような単体で実行可能なJARを作る機能が追加されました。

Payara 4.1.1.162 がリリースされました - GlassFish Japan

従来のPayara Microでは、一旦アプリケーションを普通にWARにして、Payara Micro起動→WARをデプロイする必要がありました。

今回の新機能により、本当に単体で実行可能なJARを作れるように

手順は蓮沼さんのブログに書かれてある通りなのですが、自分でもやってみてあまりに簡単で感動したので、このブログにも書くことにしました。

手順

普通にWebアプリケーションを作る

本当に普通に作ってください。pom.xmlのpackageは「war」です。

組み込みじゃない据え置きのGlassFish/Payaraにデプロイするアプリケーションと全く同じ形で作ってください。

下記は、今月のJJUG CCC 2016 Springで紹介する予定の、MVC 1.0のサンプルアプリケーションです。

アプリケーション側は全く変えることなく、据え置きサーバー・実行可能JARの両方に対応できます。

jjug-action-based-mvc/jjug-mvc10 at master · MasatoshiTada/jjug-action-based-mvc · GitHub

そして、MavenでWARファイルにビルドします。

cd jug-mvc10
mvn clean package

jjug-mvc10/targetフォルダにjjug-mvc10-1.0-SNAPSHOT.warが作成されます。

実行可能JARを作成する

上記のWARファイルを元に、Payara Microで実行可能JARを作ります。

あらかじめ、Payara Micro 4.1.1.162のJARは下記からダウンロードし、適当なフォルダに保存してください。

Payara Server & Payara Micro - Downloads

そして、次のコマンドを実行します。

java -jar ~/Java/ap-server/payara-micro-4.1.1.162.jar\
 --deploy target/jjug-mvc10-1.0-SNAPSHOT.war\
 --outputUberJar target/jjug-mvc10.jar

--outputUberJarというオプションがポイントです。

こうすると、targetフォルダにjjug-mvc10.jarというJARファイルが作成されます。これが実行可能JARです。

ちなみに、このオプションがない場合、これまでのPayara Microと同じ挙動になります。

-jar の後は先ほどダウンロードしたPayara MicroのJARファイルのパス、--deployの後はWARファイルのパスです。

ちなみに、jar tf target/jjug-mvc10.jarとすると、実行可能JARの中身を確認することができます。

とても長いので全部は載せませんが、Payara Microの中身が解凍・再パッケージされていることが確認できます。

META-INF/services/javax.enterprise.inject.spi.CDIProvider
META-INF/services/org.glassfish.tyrus.core.ComponentProvider
META-INF/services/javax.management.remote.JMXConnectorProvider
(中略)
META-INF/
META-INF/MANIFEST.MF
__cp_jdbc_ra.rar
__dm_jdbc_ra.rar
__ds_jdbc_ra.rar
__xa_jdbc_ra.rar
META-INF/configuration/
META-INF/hk2-locator/
META-INF/maven/
META-INF/maven/org.glassfish.main.concurrent/
META-INF/maven/org.glassfish.main.concurrent/concurrent-connector/
org/
org/glassfish/
org/glassfish/concurrent/
org/glassfish/concurrent/config/
META-INF/configuration/context-service-conf.xml
META-INF/configuration/managed-executor-service-conf.xml
META-INF/configuration/managed-scheduled-executor-service-conf.xml
META-INF/configuration/managed-thread-factory-conf.xml
META-INF/maven/org.glassfish.main.concurrent/concurrent-connector/pom.properties
META-INF/maven/org.glassfish.main.concurrent/concurrent-connector/pom.xml
org/glassfish/concurrent/config/ConcurrencyResource.class
org/glassfish/concurrent/config/ContextService$ContextServiceConfigActivator.class
org/glassfish/concurrent/config/ContextService$Duck.class
org/glassfish/concurrent/config/ContextService.class
org/glassfish/concurrent/config/ContextServiceInjector.class
org/glassfish/concurrent/config/LocalStrings.properties
(以下略)

一番最後の方に、自分で作ったWARファイルが見えます。

...
META-INF/deploy/
META-INF/deploy/jjug-jersey-mvc-1.0-SNAPSHOT.war
META-INF/deploy/payaramicro.properties

META-INF/deployに直接WARファイルが入ってるんですね!

実行する

java -jar target/jjug-mvc10.jar

これで実行できます。

後はブラウザで開いてださい。

http://localhost:8080/jjug-mvc10-1.0-SNAPSHOT/

ちなみに、僕はここまでの処理は1つのシェルスクリプトにまとめて、実行する時はそれを叩くだけにしています。

jjug-action-based-mvc/build-jar.sh at master · MasatoshiTada/jjug-action-based-mvc · GitHub

まとめ

WARに固めてから実行可能JARにする、というのが少し違和感があるかもしれませんが、アプリケーション側に何も変更なく実行可能JARが出来るというのは非常に面白いと感じました。

ぜひご自分でも試してみてください!

EclipseLinkとHibernateではTemporalType.DATEなフィールドの型が違う

$
0
0

かなり久々のJPAネタ。

こんなエンティティクラスがあって、

@Entitypublicclass Employee implements Serializable {
    @Id@Column(name = "emp_id")
    private Integer empId;
    
    @Temporal(TemporalType.DATE)
    @Column(name = "joined_date")
    private java.util.Date joinedDate;
    

joinedDateというフィールドはjava.util.Date型、そして@Temporal(TemporalType.DATE)が付いています。

これをEclipseLinkで実行すると、joinedDateにはjava.util.Date型のインスタンスが代入される。

一方、Hibernateで実行すると、joinedDateにはjava.sql.Date型のインスタンスが代入される。(java.sql.Datejava.util.Dateのサブクラスです)

Webアプリのビューでそのまま表示すると、Payara(JPA実装がEclipseLink内包)だと「Wed Apr 01 00:00:00 JST 2015」という形式なのに、

WildFly(JPA実装がHibernate)だと「2015-04-01」という形式になったので、アレ?と思って、

employee.getJoinedDate().getClass().getName()してクラス名を表示したらこんな感じになってました。

jug-action-based-mvcプロジェクトにテストコードを追加しました。

EmployeeDaoのテストコードを追加 · MasatoshiTada/jjug-action-based-mvc@2edd12b · GitHub


JJUG CCC 2016 SpringでアクションベースMVCの発表してきた&楽しんできた #jjug_ccc

$
0
0

発表してきた

「ネクストStruts/Seasar2としてのJava EEアクションベースMVC入門」というタイトルで発表してきました。

165人の部屋が、ほぼ満席となるくらいの方々にお越しいただきました。ありがとうございます。

やっぱり、StrutsSeasar2からの移行をどうするかは、皆さん喫緊の課題だと思います。

内容はJava EE 8のMVC 1.0、そしてEE 7における代替としてJersey MVCとRESTEasy HTMLです。

ビューは頑張ってThymeleafをリリースされたばかりの「3」にしたり、

クライアント側のロケールに合わせて画面のメッセージや検証エラーメッセージを国際化したりしました。

MVC 1.0のサンプルのみ、検証エラーメッセージの国際化には対応していません。現在、MVC 1.0の仕様策定で検討中のためです。

資料

コード

GitHub - MasatoshiTada/jjug-action-based-mvc: JJUG CCC 2016 Spring「ネクストStruts/Searar2としてのJava EEアクションベースMVC入門」のサンプル

楽しんできた

テスト自動化のまわりみち(@irofさん)

テスト自動化などの前に、テスト技法なりテスト項目書の書き方なり、基本的なことをきちんとやりましょう、ということだと理解しました。

例えば、「◯◯が正しいこと」ではなく「◯◯の値が『10』になっていること」と書くとか。

「正しいこと」なんて、そりゃJUnitで表現できませんよね・・・。

テスト技法の研修などもやったりする身としては、身が引き締まる思いでした。

Thymeleaf 3を使ってみよう!(@bufferingsさん)

Thymeleaf公式ドキュメントの和訳も出されている椎葉さんのセッションでした。

アクションベースMVCのサンプルを作る時に一通りドキュメントを読んだので、概ね知っている内容が多かったのですが、

初めて知ったことや、いまひとつ理解できていなかったことが理解できたことも多かったです。

さらに、椎葉さんは実業務でもThymeleafを使っていらっしゃるということで、デザイナーさんとの協業の話も非常に参考になりました。

Spring BootでBootした後に作るWebアプリケーション基盤(エムスリー吉田さん)

Spring Boot + Spring MVCの実用的な使い方のお話でした。

特に例外処理やロギングなどのお話が印象的でした。スライドはアップされないのかな?

Spring Framework/Bootの最新情報とPivotalが進めるクラウドネイティブなアプローチ(@makingさん)

最初に、Spring Boot 1.4およびSpring Framework 4.3の新機能のお話でした。

テスト関連の機能が特に印象的でした。テストしやすいフレームワークっていいよなあ・・・。Java EEにも頑張ってほしい。

後半は、Cloud FoundryやConcourse CIなどの紹介をしつつ、「クラウドネイティブ」なアプリケーションとは何か、

それをCloud Foundryではどう実現しているか、というお話でした。

以前にCloud Foundryワークショップにも参加したので、もっと触れていきたいなあと思います。

Seasar2で作った俺たちのサービスの今(@jukutyoさん)

Seasar2(Cubby + S2Dao)からSpring MVC + Domaへ移行するお話でした。

段階的に移行していて、今は同一WARファイルの中にSpringとSeasar2が同居している状態だそうです。

確かに、一度にまるっと移行することは、人的・時間的リソースや、サービスを維持・発展させなければならない、

といった制約もあって現実的でないこともあると思いますので、こういった実例を知れたのは非常に良かったです。

余談ですが、じゅくちょーさんは元塾講師ということで、塾講師っぽい喋り方が印象的でした。

僕も学生時代は塾講師のバイトをずっとしていたので、なんだか懐かしい気持ちでしたw

マイクロフレームワークenkan(とkotowari)ではじめるREPL駆動開発(@kawasimaさん)

システムエンジニアアドベントカレンダーで世の中を驚かせた、川島さんの発表でした。

川島さんとしては色々な問題意識をお持ちで、このenkanを作るに至ったそうです。

確かに、XMLアノテーションになっても、設定が面倒なのはあるし、CoCと分かりやすさはトレードオフです。

最後の方はチュートリアルをやってみる時間がありましたので、やってみました。

最初にMavenでのJARのダウンロード祭りがありますが、それでも5分くらいあれば、このチュートリアルは終えられました。

運営について

今回から昼休みが90分になったり、午後にも2セッションごとに40分の休憩があったり、

懇親会の開始時間が早まったり、新たな取り組みがありました。

特に午後の長めの休憩は、非常に嬉しかったです。

10分休憩だけで午後に6セッションとか7セッションとか続くと、かなり体力的にキツかったですし、

休憩時間が短いとセッション間の移動も混雑して大変でした。

所々に長めの休憩があることで、体力も回復できるし、余裕を持って移動できるし、

いろんな方とお話しすることもできるし、個人的にはいいことずくめでした。

幹事やボランティアスタッフの皆様、ありがとうございました!

おまけ

このタイムテーブル、とても役に立ちました!

@YujiSoftwareさん、ありがとうございます!

http://yujisoftware.github.io/jjug-ccc/2016-Spring/

ThymeleafでJava SE 8のDate and Time APIを使う方法

$
0
0

やりたいこと

Thymeleafのビューで、java.time.LocalDateなどJava SE 8で入った日時クラス(Date and Time API)を使いたい。

Date and Time APIについてはこちらをどうぞ。

Java日付時刻APIメモ(Hishidama's Java8 Date and Time API Memo)

(蓮沼さんの資料は何故か見つからなかった・・・)

Thymeleafの拡張機能を追加

Thymeleafには、Date and Time APIを利用するための拡張機能があります。

https://github.com/thymeleaf/thymeleaf-extras-java8time

本体に入っていないのは恐らく、Thymeleaf本体はJava SE 6でコンパイルされているからだと思われます。

thymeleaf/CONTRIBUTING.markdown at 3.0-master · thymeleaf/thymeleaf · GitHub

pom.xmlに依存性を追加します。

<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-java8time</artifactId><version>3.0.0.RELEASE</version><scope>compile</scope></dependency>

Dialectの追加

TemplateEngineをnewしている場所で、Java8TimeDialectを追加します。

templateEngine = new TemplateEngine();
templateEngine.addDialect(new Java8TimeDialect());

ビューからの利用

こんなクラスがあって、

publicclass Employee {
    ...private java.time.LocalDate joinedDate;
    // getter/setter
}

ThymeleafでjoinedDateをフォーマットして表示するには、下記のようにします。

<tr th:object="${employee}">
    ...
    <td th:text="*{#temporals.format(joinedDate, 'yyyy-MM-dd')}">2020-01-01</td>

簡単ですね!

その他の機能

こちらのREADMEに、一通りの機能が説明されています。

https://github.com/thymeleaf/thymeleaf-extras-java8time

Thymeleafのビューから@NamedなCDI管理ビーンにアクセスする

$
0
0

やりたいこと

こんなCDI管理ビーンがあって、

@Named@SessionScopedpublicclass SessionDto implements Serializable {

    private String id;

    public String getId() {
        return id;
    }
}

Thymeleafのビューからこんな感じで参照したいです。

<p th:text="${sessionDto.id}">...</p>

本当は上記みたいにやりたかったのですが、現時点では、下記のような感じで参照することができました。

<p th:text="${#cdi.bean('sessionDto').id}">...</p>

以下、やり方を解説します。

名前からCDI管理ビーンを取得するクラスの作成

package com.example.rest.thymeleaf;

import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.CDI;
import java.util.Set;

publicclass Cdi {

    public Object bean(String name) {
        CDI<Object> cdi = CDI.current();
        BeanManager beanManager = cdi.getBeanManager();
        Set<Bean<?>> beans = beanManager.getBeans(name);
        Bean<?> bean = beanManager.resolve(beans);
        Class<?> beanClass = bean.getBeanClass();
        return cdi.select(beanClass).get();
    }
}

引数のnameをもとに、ビーンを取得するメソッドを作ります。

まずBeanManagerを取得して、名前からSet<Bean<?>>を取得します。

名前だけではビーンを1つに特定できない(セッションスコープとかだとユーザー数だけ同じ名前のビーンがあるため)ので、resolve()でビーンを1つに特定します。

そこからビーンのClassオブジェクトを取得し、cdi.select(beanClass).get()でようやっと目的のCDI管理ビーンそのもののインスタンスを取得できます。

何か効率的ではないような感じがするので、今後変更するかもしれません。

参考資料

羽生田さんのスライド 3.Java EE7 徹底入門 CDI&EJB

かずひらさんのブログ CDIのBeanManagerを使う - CLOVER

CDI管理ビーンを参照するDialectを自作する

ThymeleafのDialectを自作します。

まず、IExpressionObjectFactory実装クラスを作成します。

この中で、先ほど作成したCdiクラスをnewしています。 ビューからは、getAllExpressionObjectNames()で返している名前でアクセスできます。

package com.example.rest.thymeleaf;

import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.expression.IExpressionObjectFactory;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

publicclass CdiExpressionFactory implements IExpressionObjectFactory {

    privatestaticfinal String EXPRESSION_OBJECT_NAME = "cdi";

    privatestaticfinal Set<String> ALL_EXPRESSION_OBJECT_NAMES =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(EXPRESSION_OBJECT_NAME)));

    @Overridepublic Set<String> getAllExpressionObjectNames() {
        return ALL_EXPRESSION_OBJECT_NAMES;
    }

    @Overridepublic Object buildObject(IExpressionContext context, String expressionObjectName) {
        if (EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) {
            returnnew Cdi();
        }
        returnnull;
    }

    @Overridepublicboolean isCacheable(String expressionObjectName) {
        returnfalse;
    }
}

次に、Dialectクラスを作成します。

IDialectというインタフェースがあり、そのサブインタフェースとして下記の5つがあります。

  • IProcessorDialect
  • IPreProcessorDialect
  • IPostProcessorDialect
  • IExpressionObjectDialect
  • IExecutionAttributeDialect

今回は、IExpressionObjectDialectを使いました。他の4つはまだ試せていません...(^^;

package com.example.rest.thymeleaf;

import org.thymeleaf.dialect.AbstractDialect;
import org.thymeleaf.dialect.IExpressionObjectDialect;
import org.thymeleaf.expression.IExpressionObjectFactory;

publicclass CdiDialect extends AbstractDialect implements IExpressionObjectDialect {

    privatestaticfinal IExpressionObjectFactory EXPRESSION_OBJECT_FACTORY = new CdiExpressionFactory();

    public CdiDialect() {
        super("cdi");
    }

    @Overridepublic IExpressionObjectFactory getExpressionObjectFactory() {
        return EXPRESSION_OBJECT_FACTORY;
    }
}

参考資料

Thymeleafのドキュメント Tutorial: Extending Thymeleaf

Thymeleafのソースコード

自作Dialectの追加

TemplateEngineをnewしている場所で、自作Dialectを追加します。

templateEngine = new TemplateEngine();
templateEngine.addDialect(new CdiDialect());

ビューの作成

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"      xmlns:th="http://www.thymeleaf.org">
      ...
<p th:text="${#cdi.bean('sessionDto').id}">...</p>

#cdi.bean()で自作したCdiクラスのbean()メソッドを呼び出しています。

引数には@Namedで指定したビーン名を指定します。

今回は@Namedvalue属性をしていていないので、「クラス名の頭文字を小文字にした名前」がビーン名になります。

まとめ

今回の方法は「Expression Object」(#で始まるやつ)を使ってみました。

この方法は非常に簡単でいいですね。他の方法も試してみたいです。

コードはこちら。

https://github.com/MasatoshiTada/jjug-action-based-mvc/tree/master/jjug-my-mvc

【書評】「パーフェクトJava EE」は最強のJava EE 7リファレンス!

$
0
0

https://www.amazon.co.jp/パーフェクト-Java-EE-井上-誠一郎/dp/4774183164www.amazon.co.jpgihyo.jp

著者の1人である@kikutaro_さんから献本いただきました。ありがとうございます!

これまでのJava EE書籍の課題

まずは、Java EEの最新バージョンである「Java EE 7」対応の日本語書籍が少ないことでした。

なので、下記のEE 5本やEE 6本で勉強した後は、ブログ情報を検索したり、海外の英語情報に頼るしかありませんでした。

https://www.amazon.co.jp/マスタリングJavaEE5-第2版-DVD付-Programmer’s-SELECTION/dp/4798120545

https://www.amazon.co.jp/Beginning-6%7EGlassFish-3で始めるエンタープライズJava-Programmers-SELECTION/dp/4798124605

2014年暮れあたりから、徐々にEE 7対応の書籍も出始めました(下記)。

www.shuwasystem.co.jp

www.shuwasystem.co.jp

www.shoeisha.co.jp

これらはそれぞれ非常に良い本なのですが、3冊ともチュートリアル形式で、1つのアプリケーションを作っていく中でJava EEの機能を学習していくというスタイルの本です。

なので、初期学習には非常に良いのですが、「困った時に頼りになるリファレンス」というような本ではありません。リファレンスとなるような本は「マスタリングJava EE 5」しか無い、というのが現状でした。

待望の「Java EE 7で困った時に頼りになるリファレンス」が登場!

今回登場した「パーフェクトJava EE」は、待望の「リファレンスとして使える本」です。

内容はJSFJAX-RSCDIJPAが中心です(WebSocket・Bean Validation・JTAEJBも1章ずつ解説があります)。つまりJava EEのWebプロファイルですね。

それぞれがその技術のエキスパートによって書かれています(JSFは菊田さん、JAX-RSは井上さん、CDIは上妻さん、JPAは槙さん)。この技術だったらこの方だろう、という人ばかりですね。

内容の幅も深さも、他の書籍とは一線を画しています。

例えばWebSocketの章では、エンコーダーデコーダーを利用してJSON形式でデータをやり取りする方法が紹介されています(WebSocketってチャットアプリ作っておしまい、みたいな解説が多いですよね...)。

その他、CDIのクライアントプロキシ、JAX-RSのMessageBodyReader/Writerの性質、JSFのPrimeFaces、JPAのエンティティグラフやL2キャッシュなど。

その他にも重要な内容がたくさん書かれていますので、ぜひ読んでみてください。Java EEを開発で使っているすべての方必見の内容が詰まっています!

かなり高度な内容も書かれていますので、基礎知識なしで読むのはハードルが高いかもしれません。なので、上記3冊のチュートリアル形式の本をどれでもいいので読了してから、パーフェクトJava EEを読むことをお勧めします。

ちょっと気になった点

FacesContextのインジェクト(P.36)

@Inject
FacesContext facesContext;

このように書けるようになるのは、確かJava EE 8からだったと記憶しています。

Payara Web ML 4.1.1.162で上記のコードを試しましたが、デプロイ時にエラーになりました。

セッションIDの変更(P.352)

セッションIDを変更するためにHttpSessionの破棄および再生成を行っていますが、下記のようにHttpServletRequest#changeSessionId()メソッドを利用すれば、再生成の必要は無いと思います。

HttpServletRequest request = 
    (HttpServletRequest) FacesContext.getCurrentInstance()
        .getExternalContext().getRequest();
request.changeSessionId();

レルムの説明が少ない

レルムでの認証・認可は、サーブレットの章でサラッと数ページで説明されているだけでした。

レルムはあんまり使わないよ、というメッセージなのでしょうか?深読みし過ぎ?

最後に

著者の井上さん・槙さん・上妻さん・菊田さんには、本当に感謝の言葉しかありません。

これだけの質と量の書籍を書くという作業は、本当に大変だったと思います。

とても貴重な書籍を書いていただき、ありがとうございました!

Java EE 7を勉強する方法(2016年ver.)

$
0
0

今年になって、Java EEの学習環境がガラッと変わりました。

2016年も半分以上過ぎてしましましたが(笑)、今からJava EE 7を勉強するにはどうすべきか、改めて書きたいと思います。

1. まずは作りながら学ぶ

以下3冊のいずれか1つで、Webアプリを作りながらJava EEの概要を理解しましょう。本屋さんで軽く立ち読みして、ご自分に合いそうな本を1つ選んでください。

www.shuwasystem.co.jp

www.shuwasystem.co.jp

www.shoeisha.co.jp

これらの本は、いずれもWebアプリを作りながらJava EEの基本を学ぶ、チュートリアルのような形式です。

「わかりやすいJava EE」と「パーフェクトマスター」は、サーブレットJSPの学習が終わったJava初心者の方向けです。ただし、RESTful Webサービスを作る技術「JAX-RS」の説明は、2冊とも無いので注意してください。

ある程度のJavaの経験(Webフレームワーク、DIコンテナ、ORマッパーを使った開発経験)があるならば、「Java EE 7徹底入門」が良いでしょう。この本には、JAX-RSの章があります。

2. ブログ情報を調べてみる

上記の入門書籍だけでは、実開発には足りない部分があると思います。

多くの方がブログやスライドを書かれていますので、ぜひ参考にしてみてください。

JSF

JSFでは、まずは菊田さん(@kikutaro_)のブログを読んでいただくのが良いでしょう。

JSF カテゴリーの記事一覧 - Challenge Java EE !

その他の読んでおくべき情報は、下記の記事にリンクをまとめておきました。

JSFを使うなら読んでおきたいリンクまとめ - Java EE 事始め!

JAX-RS

JAX-RSでは、まずはうらがみさん(@backpaper0)のスライドを読みましょう。

JAX-RS入門および実践

ブログはこちら。

この投稿のタグ JAX-RS — 裏紙

アクションベースMVC

Java EEの情報を調べていると、「JSFコンポーネントベースで、Strutsはアクションベースで・・・」という情報が見つかることも多いと思います。

アクションベースMVCは、現在はまだJava EE 7標準ではありませんが、使うこと自体は可能です。僕のスライドを読んでいただくのが一番良いと思います。

Java EEアクションベースMVC入門 #jjug_ccc #ccc_cd4 // Speaker Deck

CDI

上妻さん(@n_agetsu)のブログが良いでしょう。リンクは下記にまとめています。

CDI/JTAを使うなら読んでおきたいリンクまとめ - Java EE 事始め!

JPA

まずは、拙著スライドからどうぞ。

はまる!JPA(初学者向けライト版)
http://www.slideshare.net/masatoshitada7/jpa-46874399

より高度な内容については、下記のスライドを読んでみてください。

はまる!JPA
http://www.slideshare.net/makingx/jpa-29150059

金魚本に載ってないJPQLの話
http://d.hatena.ne.jp/megascus/20120925/1348575449

アプリケーションサーバ

サーバーそれぞれのエキスパートの方々がブログを書かれています。

GlassFish Japan

nekop's blog

yamadamnのはてな日記

Java EE全般

  • Java EE 7 Tutorial日本語訳まとめ

Java EE 7 Tutorialは、Oracle公式のチュートリアルです。全部ではありませんが、@kagamihogeさんが日本語訳をまとめていらっしゃいます。

The Java EE 7 Tutorialのテキトー翻訳まとめ - Qiita

  • ○○使い方メモ

@opengl_8080さんのブログです。Java EEに限らず、いろんな情報が書かれていてとても素晴らしいです。

特に、Bean ValidationとJTAの記事は必読です。

JavaEE使い方メモ(Bean Validation) - Qiita

JavaEE使い方メモ(JTA) - Qiita

各種Advent Calender

Java EE Advent Calendar 2015 - Qiita

Java EE Advent Calendar 2014 - Qiita

GlassFish Advent Calendar 2014 - Adventar

GlassFish Advent Calendar 2013 - Adventar

JBoss / WildFly (全部俺) Advent Calendar 2013 - Adventar

3. 仕上げは「パーフェクトJava EE」!

Java EE 7日本語書籍の決定版とも言うべき本です。内容の質・量とも素晴らしいです。開発でJava EEを使う方は必読でしょう。

gihyo.jp

4. 公式情報も確認しよう

かなり日本語情報が充実してきましたが、できれば英語の公式情報にも目を通しておきましょう。日本語情報が無い場合には、英語の1次情報をあたるのが最も良い方法です。OracleJava EE 7公式情報は、こちらにまとめられています。
Home: Java Platform, Enterprise Edition (Java EE) 7 Release 7

また、仕様書であるJSRも重要です。JSRはこちらから検索できます。「JPA JSR」のように「技術名 JSR」でググってもOKです。
The Java Community Process(SM) Program

まとめ

  • まずは書籍を読んで、手を動かしながらJava EEの基本を身につける!
  • 重要なブログ情報をチェック!
  • パーフェクトJava EEで仕上げ!
  • 公式情報もしっかりチェック!

もはや「Java EEは日本語情報が少ない」とは言えません。日本語情報は、現存の技術の中ではかなり充実している方だと思います。

ただし、日本語情報はGlassFish前提で書かれているものが多く、その他のサーバーでは細かい挙動が異なることも多々あります。

トラブル時に大事になってくるのは、英語の公式情報です。英語情報もしっかりとチェックしましょう。

それでは、Enjoy Java EE

Viewing all 88 articles
Browse latest View live