@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」選びの一助になれば嬉しいです。

GlassFish/Payaraで@Transactional境界内で発生した例外が失われる現象が治ってた!

普段から僕は、Java EEやるときはGlassFish/Payaraを使っているのですが、1つだけ懸念事項がありました。それが表題の現象です。それがPayara 4.1.1.154(おそらくGlassFish 4.1.1)で治っていた、というお話です。

どんな現象?

Java EEでWeb作るときは、永続化層(JPA)・ビジネスロジック層(CDI/EJB)・プレゼンテーション層(JSF/JAX-RS)というような階層構造にすることが多いと思います。

永続化層

@Dependent
public class EmployeeDao {
 @PersistenceContext(unitName = "empPU")
 private EntityManager em;

 @Transactional(TxType.REQUIRED)
 public Employee findById(Integer id) {
//  Employee emp = em.find(Employee.class, id);
//  return emp;
  throw new RuntimeException("ERROR IN DAO!!!");
 }

}

ビジネスロジック

@Dependent
public class EmployeeService {
 @Inject
 private EmployeeDao dao;

 @Transactional(TxType.REQUIRED)
 public Employee findById(Integer id) {
  Employee emp = dao.findById(id);
  return emp;
 }

}

プレゼンテーション層

@Path("employees")
public class EmployeeResource {
 @Inject
 private EmployeeService servicee;

 @GET
 @Path("{id}")
 public Response findById(@PathParam("id") Integer id) {
  Employee emp = service.findById(id);
  return Response.ok().entity(emp).build();
 }

}

永続化層で、わざとRuntimeExceptionをスローしています。

これを実行すると

これで実行してGlassFish/Payaraのログを見ると、javax.transaction.RollbackExceptionがスローされていると書かれています。RuntimeExceptionのスタックトレースは、表示されません。元の例外が失われてしまうのです。

なぜ、こうなるのか

上妻さんのブログで詳しく書かれています。昨年のGlassFishアドベントカレンダーの記事です。

GlassFish4.1をなおしてみた - 見習いプログラミング日記

簡単に言うと、javax.transaction.RollbackExceptionクラスにThrowable causeを引数に取るコンストラクタがそもそも定義されていないことや、トランザクションロールバック確定であるにも関わらずコミットしようとするコードが存在していたことが問題でした。

この問題はとても悩みの種でした。研修中にビジネスロジック以下の層で例外が発生した時にデバッグするのがとても難しかったり、開発現場やシステムの実運用環境でコレが起こったら大変だよなあ・・・と思ったり。

Java EEの大きなメリットの1つは、トランザクション管理をAPサーバーに任せることができるという点だと僕は思っているので、これはどうしたもんかなあと、長い間思っていました。

Payara 4.1.1.154で治ってた!

先日、Payara 4.1.1.154がリリースされました。で、ぼんやりとリリースノートを読んでいたら、「Upstream Fixes」に"GLASSFISH-21172 - javax.transaction.RollbackException from @Transactional bean has no cause set"の文言があるじゃありませんか!

http://www.payara.co.uk/release_notes

そして、リンクを辿るとGlassFishのJIRAへ。

[GLASSFISH-21172] javax.transaction.RollbackException from @Transactional bean has no cause set - Java.net JIRA

コメント一覧からさらにリンクを辿ると、ソースコードのDIFFが読めます。

https://java.net/projects/glassfish/sources/svn/revision/64135

おお、治ってる!しかも、上妻さんがブログに書かれていた修正案とほぼ、いや、全く同じ!

僕はPayara 4.1.1.154で動作確認しましたが、修正日付を見ると、おそらくGlassFish 4.1.1で修正されていたものと思われます。

で、修正後の挙動は?

これも上妻さんが書かれていた通りなのですが、@Transactional境界内で発生した例外がそのままスローされます。上記のコードならRuntimeExceptionですね。

まとめ

  • GlassFish/Payaraで例外が失われる現象が治っていました!
  • 上妻さん凄すぎる!

10分で出来る!初めてのTwitter4J & Twitterアプリ作り方メモ in 2015-10-18

最近はJava EE系でもJAX-RS研修の担当が多くなっている関係上、OAuthやRESTクライアントにも興味が出てきました。ということで、初めてTwitter4Jでプログラムを作ってみました。参考にしたのは、@kikutaro_さんのこちらのブログです。

Twitter4Jを使ったら10分でつぶやきJavaプログラムが作れました! ~NetBeans編~ - Challenge Java EE !

分かりやすくまとまっていて、基本的な手順は今も変わっていないのですが、管理画面のUIが変わっていたので、本日時点での画面キャプチャ付きで解説します。

目標

Twitter4Jを使って、Javaのmain()メソッドからつぶやきを送信する!

Twitter Developer登録

プログラムを作る前に、Twitter Developer登録を行って、アプリ開発に必要なアクセストークンなどの情報を取得します。この手順は、JavaやTwitter4Jによらないはずです。

まず、下記のTwitterのDeveloperサイトにアクセスします。

https://dev.twitter.com/

すると、トップ画面が表示されます。画面右上のリンクから、ご自分のTwitterアカウントでサインインしてください。このアカウントは、普段のつぶやきなどで使っているアカウントでOKです。

f:id:MasatoshiTada:20151018133412p:plain

トップ画面で下の方にスクロールして、[TOOLS]-[Managing Your App]をクリックします。

f:id:MasatoshiTada:20151018134406p:plain

すると、アプリケーションの管理画面に移ります。まだ何もアプリケーションを作っていないので、[Create New App]ボタンだけが表示されます。

f:id:MasatoshiTada:20151018134644p:plain

では、[Create New App]ボタンをクリックします。すると、アプリケーションの作成画面に移ります。適当なアプリケーション名、アプリの説明、自分のブログなどのURLを入力します。[Callback URL]は必須項目ではないので、今回は空欄のままにします。

f:id:MasatoshiTada:20151018134833p:plain

上記の画面で下の方にスクロールすると、[Developer Agreement]が記載されているので、内容を確認した上で[Yes, I agree]にチェックを入れて、[Create your Twitter app]ボタンをクリックします。

f:id:MasatoshiTada:20151018135333p:plain

そうすると、下記のような画面が表示されます。[Consumer Key]の部分は、後で使います。実際には英数字&記号の羅列が表示されていますが、秘密の情報のため画像を加工して伏せています。

f:id:MasatoshiTada:20151018143316p:plain

次に、この画面の[Keys and Access Tokens]タブをクリックします。表示されている[Consumer Key (API Key)]と[Consumer Secret (API Secret)]という2つの文字列は、後で使います。

f:id:MasatoshiTada:20151018143357p:plain

この画面で下の方にスクロールして、[Create my access token]ボタンをクリックします。

f:id:MasatoshiTada:20151018143448p:plain

画面が遷移するので、また下の方にスクロールすると、[Access Token]と[Access Token Secret]が表示されています。この2つの文字列も、後で使います。

f:id:MasatoshiTada:20151018143521p:plain

以上で、準備は完了です。

Twitter4JでJavaプログラムを作る

さて、後は簡単です。IDEEclipseでもNetBeansでもIntelliJ IDEAでも何でも構いません。ビルドツールは、MavenまたはGradleを使います。

まず、Twitter4Jを依存性に含めます。

Gradleならこうです。

compile 'org.twitter4j:twitter4j-core:4.0.4'

Maven

<dependency>
    <groupId>org.twitter4j</groupId>
    <artifactId>twitter4j-core</artifactId>
    <version>4.0.4</version>
</dependency>

次に、src/main/resourcesに「twitter4j.properties」というプロパティファイルを作ります。そして、下記のように記述します。

debug=true
#右辺にConsumer Key (API Key)をコピーして貼り付ける
oauth.consumerKey=XXXXXXXXXXXXXXXX
#右辺にConsumer Secret (API Secret))をコピーして貼り付ける
oauth.consumerSecret=XXXXXXXXXXXXXXXXXX
#右辺にAccess Tokenをコピーして貼り付ける
oauth.accessToken=XXXXXXXXXXXXXXXX
#右辺にAccess Token Secretをコピーして貼り付ける
oauth.accessTokenSecret=XXXXXXXXXXXXXXXX

src/main/javaにクラスを作ります。菊田さんのブログのプログラムそのままです(^^;

import twitter4j.*;

public class UserInfoMain {
    public static void main(String[] args) throws TwitterException {
        Twitter twitter = TwitterFactory.getSingleton();
        User user = twitter.verifyCredentials();
        System.out.println(user.getName());
        System.out.println(user.getScreenName());
        System.out.println(user.getFriendsCount());
        System.out.println(user.getFollowersCount());

        Status status = twitter.updateStatus("Twitter4Jから初めてのツイート! #twitter4j");
    }
}

これだけです。いざ、実行します。すると・・・

多田真敏
suke_masa
63
199
. . . (他にもデバッグログがいっぱい表示されます)

f:id:MasatoshiTada:20151018142359p:plain

おおお、自分のユーザー情報が表示されて、つぶやきが投稿されてる!

まとめ

  • Developer登録をして、アクセストークンなどの情報を取得する
  • アクセストークンなどは、コピーしてtwitter4j.propertiesに貼り付ける
  • Twitter4J使えば、REST APIやらJSONやら認証やらのコードは書かなくてOK!

感想

あまりに簡単すぎて感動すら覚えました・・・。

Twitter4Jのソースも、これから読んでみたいと思います!

Payara(GlassFish)でMariaDBのJDBC接続プールが作成できない

表題の通りです。

環境

試していませんが、おそらくWindowsGlassFishでも同じ現象が起こると思います。

現象

  • MariaDBJDBCドライバのJARを「<PAYARA_HOME>/glassfish/lib」や「<PAYARA_HOME>/glassfish/domains/domain1/lib」に置く
  • Payaraを起動してlocalhost:4848にアクセスし、管理コンソールからURL/USER/PASSWORDを設定する

という一般的な手順で作成しても、MariaDBへのpingが通りません。

ping時のPayara管理コンソール(ブラウザ上)でのエラーメッセージは、こんなのが出ます。

Access denied for user ‘’@’localhost’ (using password: NO) 

どうやら、ユーザー名やパスワード設定が間違っている時に出てくるMariaDBのエラーメッセージのようなのですが、全く同じURL/USER/PASSWORDでメインメソッドから普通にJDBC接続したら出来ました。 (URL/USER/PASSWORDはPayara管理コンソールからコピペしたので間違いないはずです)

Payaraのプロパティ名も間違えていません。

しかも、ごくたまにpingが通ります。しかし、またすぐにpingが通らなくなります。

対策

今回は、MariaDBをPayaraで使うこと自体が目的ではないので、MySQL 5.6に変更しました。そうすると無事にJDBC接続プールが作成できました。

原因は分かりませんが、Payara管理コンソールで選択するときのDBの種類にはMariaDBは入っていないし、Payara(というか恐らくGlassFish)はMariaDBに対応していないのかもしれません。 (完全に僕の憶測です)

注意

この問題は、あくまでPayara+MariaDBの組み合わせによるものです。

上述の通り、通常のJDBC接続であればMariaDBは問題なく動作しました。

【注意事項あり】Doma 2だけどCDI/EJB使ってJTAでトランザクション管理したい!そしてJAX-RSでREST作りたい!

Doma 2とは?基本的な使い方は?

Doma 2は、SQLを外部ファイルに書くことができるORマッパーです。ネイティブSQLが書けること、依存ライブラリが無い事、国産 OSSで日本語ドキュメントが充実している事などが魅力です。

下記の記事もご参考になさってください。2014年のJavaアドベントカレンダー向けに書いたものです。

美しき青きDoma!~SQLとIDEが奏でる美しきORマッピング~ - Java EE 事始め!

Domaでは、プログラマが作るのはHogeDaoインターフェイスだけで、その実装クラスHogeDaoImplはGradleでビルド時にAnnotation Processorで自動生成されます。

CDI/EJB使ってHogeDao型のフィールドにHogeDaoImplのインスタンスをインジェクションしたり、JTAトランザクション管理するには、実装クラス側(HogeDaoImpl)に@RequestScoped(CDI)や@Stateless(EJB)などのアノテーションを付加する必要があります。

しかし、実装クラスはビルド時に生成されるし、生成後に手作業でアノテーションを付けても、再ビルド時に上書きされて消えてしまいます。

アノテーションをつける方法がずっと分からなかったのですが、遂にやり方が分かったのでご紹介します。

ソースはGitHubに公開しています。

MasatoshiTada/doma-jaxrs · GitHub

APサーバーはPayara Web 4.1.153ですが、Jerseyには依存しないように書いたので、WildFlyでもそのまま動作します(9.0.1.Finalで確認済み)。DBはMySQL 5.6、IDEIntelliJ IDEA 14.1.5です。

CDI/EJBJTAについて

これ以降の内容は、CDI/EJBJTAの知識が前提となります。下記の資料が参考になります。

JavaDayTokyo2015での寺田さんの発表資料です。CDIについて詳しくまとまっています。

http://www.oracle.co.jp/jdt2015/pdf/2-2.pdf

@opengl-8080さんのJTAの解説ブログです。いつもながら勉強になります。

JavaEE使い方メモ(JTA) - Qiita

アノテーションをつける答えは@AnnnotateWith

ツイッターである方々のやり取りを見ていたら、Doma 2の公式ドキュメントの一部が目に入りました。

http://doma.readthedocs.org/ja/stable/config/#id22

@AnnnotateWithというアノテーションを使っていますね。これがポイントです。

1.アノテーションの自作

まず、こんなアノテーションを自作します。自作するアノテーション名は任意ですが、公式ドキュメントに従って@InjectConfigという名前にしておきます。

CDIの場合
package com.example.dao.config;

import org.seasar.doma.AnnotateWith;
import org.seasar.doma.Annotation;
import org.seasar.doma.AnnotationTarget;

import javax.enterprise.context.Dependent;
import javax.inject.Inject;

@AnnotateWith(annotations = {
        @Annotation(target = AnnotationTarget.CLASS, type = Dependent.class)
        , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
})
public @interface InjectConfig {
}
EJBの場合
package com.example.dao.config;

import org.seasar.doma.AnnotateWith;
import org.seasar.doma.Annotation;
import org.seasar.doma.AnnotationTarget;

import javax.ejb.Stateless;
import javax.inject.Inject;

@AnnotateWith(annotations = {
        @Annotation(target = AnnotationTarget.CLASS, type = Stateless.class)
        , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
})
public @interface InjectConfig {
}

注意!! Payara 4.1.1.154までのAPサーバーでは、CDI+JPA以外のORマッパー(というよりJDBC)では、JTAが正しく動作しない可能性があることが判明しました。なので、現段階ではEJBを使ってください。このことについては、PayaraのGitHubにIssueとして報告済みです。次バージョンでの改善を期待しています。WildFlyでは、CDIでもEJBでも正しく動作します。 参考URL @OpenGL_8080さんのブログ(コメント欄をご確認ください) JavaEE使い方メモ(JTA) - Qiita PayaraのGitHub Payara does NOT rollback when RuntimeException occurs in CDI @Transactional method using JDBC · Issue #505 · payara/Payara · GitHub

@Annotationのtype属性はアノテーション名.class、target属性はtypeで指定したアノテーションを付加する対象を表します。

この例だと、クラスに@Dependentコンストラクタ@Injectを付けるということになります。

2. 自作アノテーションをDaoインターフェイスに付加

次に、自作した@InjectConfingアノテーションを、自作Daoインターフェイスに付加します。

package com.example.dao;

import com.example.dao.config.InjectConfig;
import com.example.entity.Employee;
import org.seasar.doma.Dao;
import org.seasar.doma.Script;
import org.seasar.doma.Select;

import java.util.List;
import java.util.Optional;

@Dao
@InjectConfig
public interface EmployeeDao {

    @Script
    void create();

    @Select
    Optional<Employee> selectById(Integer empId);

    @Select
    List<Employee> selectLikeName(String name);
}

3.Gradleでビルド

Gradleでビルドすると、EmployeeDaoインターフェイスの実装クラスが生成されます。

CDIの場合
package com.example.dao;

/** */
@javax.enterprise.context.Dependent()
@javax.annotation.Generated(value = { "Doma", "2.5.0" }, date = "2015-10-15T20:48:55.929+0900")
public class EmployeeDaoImpl extends org.seasar.doma.internal.jdbc.dao.AbstractDao implements com.example.dao.EmployeeDao {

    /**
     * @param config the config
     */
    @javax.inject.Inject()
    public EmployeeDaoImpl(org.seasar.doma.jdbc.Config config) {
        super(config);
    }
EJBの場合
package com.example.dao;

/** */
@javax.ejb.Stateless()
@javax.annotation.Generated(value = { "Doma", "2.5.0" }, date = "2015-10-15T20:48:55.929+0900")
public class EmployeeDaoImpl extends org.seasar.doma.internal.jdbc.dao.AbstractDao implements com.example.dao.EmployeeDao {

    /**
     * @param config the config
     */
    @javax.inject.Inject()
    public EmployeeDaoImpl(org.seasar.doma.jdbc.Config config) {
        super(config);
    }

@AnnnotateWithで指定した通り、クラスに@Dependentまたは@Statelessコンストラクタ@Injectが付加されています。これで、このEmployeeDaoImplを他のクラスにCDIでインジェクトできるようになります。

また、コンストラクタには@Injectが付加されています。

引数のConfigインターフェイスは、データソースなどを保持するもので、後ほど実装クラスを作成します。

Config実装クラスを作成して@ApplicationScopedなどを付加しておけば、この実装クラスのインスタンスが、コンストラクタインジェクションされます。

4. Config実装クラスの作成

package com.example.dao.config;

import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.dialect.Dialect;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.sql.DataSource;

@ApplicationScoped
public class AppConfig implements Config {

    @Inject
    private DataSource dataSource;

    @Inject
    private Dialect dialect;

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

}

@ApplicationScopedを付加してCDI管理ビーン(このクラスはEJBでなくでOKです)にします。アプリケーションに関する設定情報を保持するクラスなので、スコープはアプリケーションスコープが適切と思われます。

DataSourceDialectについては、別途プロデューサークラスを作成して、@Injectで取得できるようにしています。

package com.example.dao.config;

import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.MysqlDialect;

import javax.annotation.Resource;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.sql.DataSource;
import java.io.Serializable;

@Dependent
public class DataSourceProducer implements Serializable {

    @Resource(lookup = "jdbc/sandbox")
    private DataSource dataSource;

    private Dialect dialect = new MysqlDialect();

    @Produces
    public DataSource getDataSource() {
        return dataSource;
    }

    @Produces
    public Dialect getDialect() {
        return dialect;
    }
}

このクラスもEJBでなくてOKです。

訂正とお詫び:@ResourceJDBCリソースを取得する際、name属性を使っていたのですが、正しくはlookup属性でした。訂正してお詫びします。

Config実装クラスが複数ある場合は?

データソースが複数ある場合など、Config実装クラスが複数になる可能性があると思います。その場合はQualifierを自作して、それをConfig実装クラスと@AnnotatedWithに追加すれば良いはずです。

@AnnotateWith(annotations = {
        @Annotation(target = AnnotationTarget.CLASS, type = Dependent.class)
        , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
        , @Annotation(target = AnnotationTarget.CONSTRUCTOR_PARAMETER, type = 自作Qualifier1.class
})
public @interface InjectConfig1 {
}
@ApplicationScoped
@自作Qualifier1
public class AppConfig1 implements Config {
    // 省略
}
@AnnotateWith(annotations = {
        @Annotation(target = AnnotationTarget.CLASS, type = Dependent.class)
        , @Annotation(target = AnnotationTarget.CONSTRUCTOR, type = Inject.class)
        , @Annotation(target = AnnotationTarget.CONSTRUCTOR_PARAMETER, type = 自作Qualifier2.class
})
public @interface InjectConfig2 {
}
@ApplicationScoped
@自作Qualifier2
public class AppConfig2 implements Config {
    // 省略
}

JTAトランザクション管理する

DataSource@Resourceで取ってきているし、DaoImplクラスはCDI管理にできたので、後は簡単です。

CDIの場合

package com.example.service;

import com.example.dao.EmployeeDao;
import com.example.entity.Employee;
import com.example.resource.dto.EmployeeDto;

import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.io.Serializable;
import java.util.Optional;

@Dependent
public class EmployeeService implements Serializable {
    @Inject
    private EmployeeDao employeeDao;

    @Transactional(Transactional.TxType.REQUIRED)
    public Optional<EmployeeDto> selectById(Integer empId) {
        Optional<Employee> employeeOptional = employeeDao.selectById(empId);
        return employeeOptional.map(this::convertToDto);
    }

EJBの場合

package com.example.service;

import com.example.dao.EmployeeDao;
import com.example.entity.Employee;
import com.example.resource.dto.EmployeeDto;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import java.io.Serializable;
import java.util.Optional;

@Stateless
public class EmployeeService implements Serializable {
    @Inject
    private EmployeeDao employeeDao;

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Optional<EmployeeDto> selectById(Integer empId) {
        Optional<Employee> employeeOptional = employeeDao.selectById(empId);
        return employeeOptional.map(this::convertToDto);
    }

@Injectでインジェクトすると、EmployeeDaoImplインスタンスが注入されます。で、メソッド@Transactionalを付加すればOK!

JAX-RSでRESTを作る

package com.example.resource;

import com.example.resource.dto.EmployeeDto;
import com.example.service.EmployeeService;
import org.hibernate.validator.constraints.NotBlank;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.constraints.Pattern;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("employees")
@RequestScoped
public class EmployeeResource {

    @Inject
    private EmployeeService employeeService;

    @GET
    @Path("{empId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response selectById(@PathParam("empId")
                                   @Pattern(regexp = "[1-9][0-9]*") String empIdStr) {
        Integer empId = Integer.valueOf(empIdStr);
        EmployeeDto employeeDto = employeeService.selectById(empId)
                .orElseThrow(() -> new NotFoundException("該当する社員が見つかりませんでした"));
        return Response.ok(employeeDto).build();
    }
}

感想

これで去年からの疑問点が解決しました。ようやっとスッキリです。

あとはDomaの使い方さえ知れれば、怖いものなし・・・のはず!

併せて読みたい

うらがみさんのブログ。Doma+JAX-RS連携について書かれています。

Thymeleaf + JAX-RS + DomaをGlassFishで試してみる - 裏紙

今回利用した、@siosioさん作のIntelliJ IDEA Doma Support Plugin

IntelliJ IDEA用のDomaプラグイン作ってみた - しおしお

Doma公式ドキュメント

Welcome to Doma — Doma 2.0 ドキュメント

Domaにこの機能が入った経緯のブログ記事です。

DomaのEJB3.1対応 - taediumの日記