読者です 読者をやめる 読者になる 読者になる

プログラミングメモ

ソフトウェア開発に関する技術メモ。

Horizontal Form の記述例メモ

bootstrap 3 の Horizontal Form。labelとinputを並べるだけのパターンから外れるといつも記述方法がわからなくなるため、しばしば遭遇するレイアウトについてHTML記述例をメモ。

レイアウト例

f:id:teematsu:20170507222247p:plain

 

ソース

 

 

Symfony 2.8とPHP-DIのAuto Wiring

この記事は Symfony Advent Calendar 2015 の16日目です。昨日は@Issei_Mさんの「最近のSymfony Standard Editionのディレクトリ構成」でした。

 

内製フレームワークからの移行先を探してSymfonyを調査中です。その過程でDIのAuto Wiringの使い方を確認したので、ここにまとめます。

概要

Symfony 2.8ではサービスのAuto Wiringが可能になりました (New in Symfony 2.8: Service Auto Wiring (Symfony Blog))。これまでは、services.ymlにサービスの名前とクラス名の定義、依存先のサービスも(たとえばコンストラクタの引数として)指定が必要でした。Auto Wiring有効時は、コンストラクタのタイプヒンティングを利用して、これらの定義が簡略化できるようになっています。

また、Symfonyとは別のフレームワークになりますが、PHP向けのDIコンテナのひとつにPHP-DI があり、これもAuto Wiringをサポートしています。また、アノテーションを利用してプロパティに直接注入できる機能も備え、注入だけのためにコンストラクタを作成する必要がありません。Symfonyとの連携機能も用意されており、状況によっては、Symfonyのサービスコンテナよりさらに記述を簡略化できそうです。

この記事では、以下2点の基本的な使い方をご紹介します。

試した環境は次のとおりです。

Symfony 2.8 の AutoWiring

たとえば、サービスAにサービスBを注入したいとします。従来は、A, Bともにservices.ymlに定義が必要でした。

Auto Wiringを有効にすると、サービスAのargumentsの指定は不要になり、サービスBの定義全体も不要となります。ServiceAクラスのコンストラクタのタイプヒンティングで指定されたクラスが自動的に注入されます。

実際に簡単なコントローラを作成して試してみます。

プロジェクト作成

組み込みサーバの起動確認のため、ブラウザから http://localhost:8000/app_dev.php/ にアクセスしてWelcome画面の表示を確認します。

コントローラとサービスの作成

例として、/sample/hello/{名前} にアクセスすると「こんにちは{名前}さん」というメッセージが表示されるようにします。コントローラを次のように作成します。

  • 名前を受け取って「こんにちは{名前}さん」というメッセージを構築するGreetingServiceを作成。
  • コントローラはGreetingServiceを呼び出してメッセージを作成。
  • コントローラは、サービスとして作成 (How to Define Controllers as Services (The Symfony CookBook)にしたがって作成)

先の例に当てはめると、service_Aがコントローラ、service_BがGreetingServiceにあたります。GreetingService、コントローラ、services.ymlの定義は次のようにしました。

これで http://localhost:8000/sample/hello/JohnSmith にアクセスすると「こんにちは、JohnSmith さん。」と表示されます。

Auto Wiringを利用する

次に、SampleControllerに対してAuto Wiringを有効にしてみます。有効にするには、autowiring: true を指定します。GreetingServiceについてのサービス定義は不要になるため削除してしまいます。services.ymlは次のとおりとなります。

SampleControllerがGreetingServiceを必要としていることはコンストラクタのタイプヒンティングに明記されており、services.ymlには、argumentの指定と、GreetingServiceのサービスとしての定義は不要になりました。SampleControllerクラスとGreetingServiceクラスには変更ありません。

なお、Auto Wiringが可能なのはタイプヒンティングに具象クラスが指定されている場合に限られるようです。サービスのinterfaceをclass (実装)と分けて定義している場合は、コンストラクタのタイプヒンティングにはinterface名を指定するかと思いますが、この場合には以下のとおりエラーになります。実装クラスを特定できないためでしょう。

Unable to autowire argument of type "AppBundle\GreetingServiceInterface" for the service "app.controller.hello". 

依存のネストがある場合

さらに、GreetingServiceが別のサービスを利用するように拡張してみます。氏名を大文字化するCapitalizationServiceを追加し、GreetingServiceから利用するようにします。この場合でも、新しいサービスCapitalizationServiceをservices.ymlに記述する必要はなく、クラスの追加・修正だけで済みます。

SymfonyでのPHP-DIの利用

ここまで作成した処理を、PHP-DIを利用したものに書き換えてみます。

PHP-DIのSymfonyでの利用方法は、PHP-DIのサイトに説明があります。

 PHP-DI in Symfony 2

PHP-DIの導入

まず、PHP-DIをcomposerでインストールします。

なお、私の環境では、インストール中のキャッシュクリアの処理でファイル削除失敗のエラーになってしまったため、app/cache/dev, app/cache/dev_oldを削除してcomposerを実行しなおしました。(php app/console cache:clear でも同様にエラーになります・・・なんとかならないものでしょうか・・・)

インストール後、app/AppKernel.phpに以下の処理を追加する必要があります。(バンドルとして追加されるわけではないようです。)

PHP-DIは次のような仕組みでSymfonyのサービスコンテナと共存するようです。

  • Symfonyのサービスコンテナを、php-di/symfony2-bridgeが提供するもの(DI\Bridge\Symfony\SymfonyContainerBridge)にすり替え。
  • php-di/symfony2-bridgeのサービスコンテナは、Symfonyのサービスコンテナクラスを継承しており、Symfonyのサービスコンテナと同様に動作。
  • ただし、取得しようとしたサービスが見つからない場合に、PHP-DIに委譲。

したがって、PHP-DI導入後も先のプログラムソースのままで動作しますが、Symfonyのサービスコンテナの処理が働いている結果であり、PHP-DIには委譲されていないはずです。

PHP-DI経由でサービスを利用

PHP-DIは、明示的に定義のない場合は、オブジェクトの名前(サービス名)を、そのままクラス名として解釈してオブジェクト(サービス)を取得しようとします。したがって、コントローラクラスの@Routeのservice=にクラス名を指定できるようになります。

このとき、services.ymlにはSampleControllerの定義はもはや必要ありません。

SampleControllerの依存先(GreetingService)についてもSymfony 2.8と同様にタイプヒンティングからクラスが特定されコンストラクタ経由で注入されますので、やはりサービスの定義は不要です。

Symfonyが管理するサービスを利用

PHP-DIが管理するサービスに対して、Symfonyが管理するサービスを注入できます。

How to Define Controllers as Services (The Symfony CookBook によれば、サービスとして作成したコントローラからテンプレートを使うには、"templating" サービス(EngineInterfaceインタフェースの実装クラス)の注入が必要です。これをPHP-DIで管理されたコントローラに注入する方法のひとつは、インタフェースとオブジェクトの対応関係を明示的にPHP-DIに指定してやることです。この対応関係は、PHP-DI導入時にAppKernelに追加したPHP-DIの初期化処理に追加します。

\DI\get('templating') は、「コンテナから名前 templating で取得したオブジェクト」を表します。ここでのオブジェクト取得もまた、Symfony管理→PHP-DI管理の順に探索されるものと思われます。

この指定を追加した上で、SampleControllerのコンストラクタにEngineInterfaceを追加すれば、"templating"サービスが注入されます。

プロパティへの注入

PHP-DIでは@Injectを指定したプロパティにサービスを注入できます。この場合は、コンストラクタのタイプヒンティングの代わりに、プロパティの@varでクラスを指定します。

ただし、アノテーションはデフォルトで無効となっており、有効にするにはPHP-DI初期化時に指定する必要があります。

なお、Symfonyアノテーションと異なり、@Inject の名前空間は考慮されていないようで、Inject の use を記述する必要がありません。

@Injectを使ってSymfonyが管理するサービスを利用

この@Injectを使ってSymfony管理のサービスを注入することもできます。@Injectの引数にサービス名を指定します。

この場合は、EngineInterfaceと"templating"サービスの対応関係を定義する必要はありません。

まとめ

  • Symfony 2.8のAuto Wiringを使うと、サービスに別のサービスを注入する場合に後者のサービスの定義は省略でき、記述が簡素化されます。
  • また、PHP-DIをSymfonyと連携させた場合は、
    • Symfony 2.8のAuto Wiringと同様の効果が得られる上、「サービスとして定義したコントローラ」についてもサービスの定義が省略できます。
    • @Injectアノテーションを指定してプロパティに直接注入でき、コンストラクタ定義が省略できます。
    • Symfonyが管理するサービスでも注入できます。

 明日は tarokamikaze さんです。よろしくお願いします。

AndroidのSQLiteDatabaseを複数のスレッドで利用する

AndroidSQLiteを使うにあたり、複数のスレッドからDBにアクセスする必要がある場合、SQLiteOpenHelperとSQLiteDatabaseのインスタンスをどう扱うべきか。

Androidの次のドキュメントからはマルチスレッドな場合の使い方がはっきりしなかった。

 知りたかったのは、

  • SQLiteOpenHelper / SQLiteDatabase はスレッドセーフか?
  • SQLiteOpenHelper / SQLiteDatabase のインスタンス複数作ってよい?closeはいつすべき?ライフサイクルがどうあるべき?

対象はAndroid 4.2.2。

今のところ次のとおりだと理解した。

  • SQLiteOpenHelperはスレッドセーフ。1データベースにつき1インスタンスとすべき。

  • SQLiteDatabaseもスレッドセーフ。1データベースにつき(closeしていないインスタンスは)1インスタンスとすべき。

  • SQLiteDatabaseをcloseするのは、全スレッドでそのインスタンスの利用を終えた後。

 SQLiteOpenHelperは、Webを検索するとシングルトンにすべきという記事が多く見られる。テーブルの作成などを処理する onCreate, onUpgrade が並列実行されるのは不都合だが、SQLiteOpenHelperのソースを見るとsynchronizedで排他制御されており、同じインスタンスをスレッド間で共有しておけばonCreateなどが並列実行される心配がない。

SQLiteDatabaseは、SQLiteOpenHelper#getWritableDatabase(), #getReadableDatabase()で取得するが、SQLiteOpenHelperは一度作成したインスタンスをキャッシュして、SQLiteDatabaseがcloseされない限りは同じインスタンスを返す(APIリファレンスにも説明あり)ため、SQLiteOpenHelperをシングルトンとした場合はSQLiteDatabaseのインスタンスも1データベースにつき1個となる。

となるとSQLiteOpenHelperはスレッドセーフであってほしいが、ソースを見てもぱっと見は、なにやら複雑でいまひとつ確信が持てない。これについてはSQLiteSessionのjavadocに説明があった。このクラスは内部クラス?であるらしく、APIリファレンスには掲載されていない。ソースは以下を参照した。

この説明によれば

  • SQLiteDatabaseは、ThreadLocalを使い、スレッドごとにSQLiteSessionのインスタンスを1個持つ。
  • SQLiteSessionは、1トランザクションを実行するたびに、コネクションをプールから取得、返却。
  • 1スレッドが同時に使うコネクションは高々1個となるように制御される。

というように、複数スレッドで共有して使うことが前提の仕組みとなっている。ソースを見ると、コネクションプールもSQLiteDatabase 1インスタンスに1個となっており、SQLiteDatabaseをスレッド間で共有しないとプールが効果を発揮しない。

SQLiteDatabase#close()を呼ぶべきタイミングについては、Webを検索すると見解が割れている。

 ソースを見たところ、あるスレッドでSQLiteDatabase#close()を呼ぶと、それを共有別スレッドでは、insertやqueryを呼び出した時点で、close済みという理由で例外が発生する。closeを呼ぶとすれば、SQLiteDatabaseを共有する全スレッドで利用が終了した後とする必要がある。closeの処理が、ThreadLocalな何らかのオブジェクトに転送されるような処理にもなっていなかったため、スレッドごとにcloseを呼ぶ必要はない。

逆にcloseを呼ばない場合のリークの心配については、SQLiteDatabaseのインスタンスが1つに保たれていること、SQLiteSessionがThreadLocalで管理されているためthreadが無くなればガーベージコレクションの対象となること、コネクションはSQLiteDatabaseのインスタンスごとにプールで管理されていることから、アプリ実行中に延々とリソースを食い尽くす心配はないように思われる。不安がある場合は、適当な時期に、どのスレッドもSQLiteDatabaseを使っていないタイミングを見計らってcloseすることになるだろう。close後に再度アクセスが必要になっても、getWritableDatabaseなどを呼び出せば利用可能なSQLiteDatabaseのインスタンスが新たに得られる。

なお、closeをfinalizeに任せると警告がログに出るとの記事もあったが、その件は未調査。

 

 本件がややこしく感じてられ、また、closeの呼び出しタイミングで諸説生じてしまっているのは、SQLiteDatabaseがコネクションを表しているとの誤解が原因ではなかろうか。このAPIがコネクションを隠蔽しており、insertやqueryなどのメソッドがSQLiteDatabaseに用意されているために、SQLiteDatabase=コネクションという想像をしてしまっていた。SQLiteDatabaseを字面どおりデータベースと捕らえれば、それをスレッド間で共有し、すべてが使用を終えてからcloseするというのはすんなり受け入れられる。

 

JJUG CCC 2015 Spring メモ

Timetable / JJUG CCC 2015 Spring(4月11日開催) | 日本Javaユーザーグループ

覚えておきたいところ自分用メモ。

Javaにおけるnull。これまでとこれから (太一氏)

Javaにおけるnull。これまでとこれから - Google スライド

  • 各言語のnullっぽいもの

    • SQL
      • 3値論理である(true, false, NULL)
      • NULLは未知、または、適用不能、をあらわす。値ではない!
  • 言語仕様上nullが許されない場所

    • Integer i; で i の unboxing
    • for (String foo: bar) のbar
    • synchronized(x) の x
    • throw ex のex
  • 何もないこと、をどうあらわすか

    • "空"のオブジェクト

      • 空文字列
      • 空の配列
      • 空のListなど。Collections#emptyList()などで得られる。
    • 型のあるnull

      • NullObjectパターン。A型に対し、abstract Aと、そのサブクラスRealA、NullA を作る。
        • ソースが増えるのがデメリット。
    • Optional

  • 有効でない値、を受け取ったとき/受け取りうるときどうするか

    • Map#getOrDefault 値がないときの値を指定(Java 8)
    • java.util.Objects#toString (Java 7)
    • Optional
      • ifPresetn
      • orElse
  • nullを早期にガード

    • メソッドの引数にnullを渡すのをガード
    • Collectionの要素のnull防止
    • java.util.Objects#requireNonNull
  • The Checker Framework にチェックさせる

※ 自分で調べてみたらこんなもの

  • nullを考慮して処理
    • java.util.Objects
      • Objects#hashCode
      • Objects#equals ( compare もあるが、そちらはNPEが出ることがあるらしい)
      • Objects#nonNull
    • Apache commons lang
      • StringUtils#isEmpty
      • StringUtils#defaultString

大規模な負荷でもドキドキしない為のJava EE (nagaseyasuhito氏)

大規模な負荷でもドキドキしない為のJava EE

  • 負荷テストは Apache JMeter

    • Selenium IDEのシナリオを利用できる。Selenium IDEでユーザー操作をシナリオとして記録できる。
    • Jenkins Performance Plugin : JMeterの結果を可視化。
  • ボトルネックをみつける

    • Resource Monitoring
      • Zabbix, Munin, MRTG, Sensu, Gangliaなど。
    • MissionControl
      • Flight Recorderで取得したJVMの統計情報を可視化
      • -XX:+UnlockCommercialFeatures -XX:+FlightRecorder
      • jcmdコマンドで統計情報を取得
  • JPA のスケールアウト戦略

    • READが頭打ち (SELECTが頭打ち)
      • →Master Slave Replication
        • 更新系クエリはマスターへ発行
        • 検索系はスレーブに発行
        • MySQL ReplicationDriver
          • java.sql.Connection#setReadOnly(true)した場合はスレーブのホストにリクエスト発行
    • WRITEも頭打ち on memory cacheでも間に合わない。
      • →Partitioning
        • クエリを発行するデータベースを分ける。IDの剰余などでわける
        • joinやsortができないのでアプリの設計にも影響
        • EclipseLink Partitioning
        • Hibernate Shards

動的に追加・削除できる入力欄をJSFで作成する

同じ内容を2個以上入力できるフォームで、ボタンクリックにより入力欄の追加や削除をできるようにしたい。Googleで検索してみるとui:repeatを使った解が見つかるが、実際試してみると、場合によっては不自然な挙動になり、実際に使うのには具合が悪かった。色々試した結果、PrimeFacesのp:dataListを使って実現できた。

 

確認環境
作成する画面例

f:id:teematsu:20150403223014j:plain

  •  氏名とe-mailアドレスをペアで入力する
  • このペアの単位で、入力欄を追加・削除できる
Backing Bean
  • 氏名, e-mailをプロパティとして持つMemberクラスを定義し
  • Backing Beanには、List<Member> を持たせる。

Memberクラス

deleteについては後述。

Backing Bean (RegisterPageクラス)

プロパティmembersがList<Member>となっている。

初期表示時に1個だけ入力欄が表示されるように、initメソッドでMemberを1個だけmembersに追加。 

actionのメソッドは後述。

Facelets

p:dataListのコンテントが、氏名・e-mailの入力欄ペアとなっており、これがp:dataListによって繰り返される。

p:dataListはPrimeFacesが提供するタグで、列数1に限定したh:dataTableのようなもの。h:dataTableはHTMLのtable/tr/tdタグを出力するが、p:dataListはul/li、ol/li、dl/ddを出力する。ただし、type="none"を指定することでul/liなどは出力されなくなるため、自分で(ほぼ)自由にHTMLを組み立てられ、ui:repeatに近い使い方もできる。

valueアトリビュート、varアトリビュートはui:repeatと同じで、それぞれ、参照するリストと、リストの要素を代入する変数名を指定する。#{registerPage.members} でList<Member>を参照し、その要素(この例ではMemberクラス)が member に代入されてp:dataListのコンテントが処理される。

p:dataListのコンテントでは、Memberクラスのname, emailプロパティそれぞれをh:inputTextに結びつけて入力できるようにしている。

以上で、List<Member>の要素数と同じだけ入力欄が表示されるようになる。

入力欄の追加

「入力欄を追加する」ボタンをクリックすればRegisterPage#addMember()メソッドによりList<Member>に要素が追加され、p:dataListで表示される入力欄も1個増える。

入力欄追加のh:commandButtonにはimmediate="true"を指定している。これはvalidationをスキップするためだ。Memberクラスのnameプロパティにはvalidationのルール(@NotNull @Size(min = 1, max = 10))を指定してあるが、「入力欄を追加」時にはvalidationをせずにアクションを実行したい。immediate="true"を指定することでvalidationをする前にアクションを呼出せる。

入力欄の削除

追加の逆で、List<Member>から要素を削除すれば良いように思われるし、「削除」ボタンにimmediate="true"を付けない場合はそれでうまく行った。しかし、immediate="true"を付けると、どの「削除」を押しても、最後の入力欄が削除されるという動作になった。

この問題を回避するために、List<Member>から要素を削除するのはあきらめ、代わりにMemberにdeleteプロパティを持たせ、これがtrueなら画面には表示しない、という方法を取った。delete=trueなら表示されないようにするため、p:dataListのコンテントは<ui:fragment rendered="#{not member.deleted}"> で囲んだ。

実用にあたって
  • p:dataListは、内容全体を2つのdivタグで囲み、borderを表示する。このborderを消したい場合は、出力されたHTMLを見ながら、適切にCSSクラスとスタイルを追加する。上記の例では、p:dataList の styleClass="repeated-form" と、h:headタグ内のスタイル定義が該当する。divタグ自体は消せない。全体を<ul>などで囲みたい場合にはdivが間に入るとHTML文法上正しくないので困る。この場合はp:dataListにulやolを出力させるようにtype= を設定して回避する。<table>, <tr>などで囲みたい場合も困るが、この場合はtableを出力するp:dataTableに置き換えると良いかもしれない(未検証)。なお、h:dataTableはimmediate="true"指定時に入力値が維持されない問題がありNG。
  • また、PrimeFacesはその表示にjQuery UI由来のスタイルを適用するため、PrimeFacesベースで画面を作っていない場合はp:dataListの中だけフォントサイズやボタンのスタイルが変わってしまう。これは、冒頭に示した表示例でも顕著だ。これに対処する正式な方法は、jQuery UIの「テーマ」を作成し、PrimeFacesのマニュアルに従い、組み込むことであるが、一筋縄ではいかない。ここもHTMLを観察しながらCSSクラスとスタイルでがんばるのが適当かもしれない。
  • 追加・削除ボタンに <f:ajax ... /> を追加してAJAXによる部分再描画で画面を更新するようにしても、問題なく動作する。f:ajaxのrender アトリビュート(再描画対象)には、p:dataListのidか、さらにその外側のものを指定する。
  • 氏名・email欄のvalueは、上記の例では #{member.name} などとしているが、代わりに、p:dataListのvarStatusと組み合わせて、#{registerPage.members[memberStatus.index].name} というように指定しても、理屈上は問題なさそうだし、Googleで検索して見つかるui:repeatの例では、むしろ後者しか動作しないと説明しているものもある。しかしp:dataListの場合は、後者は動作しなかった。NullPointerExceptionが発生したりする。p:dataListのvarStatusの動作が不完全のような印象を受けた。
  • List<Member>から要素を削除すると画面に正しく反映されない現象は、h:inputText(UIInput)にsubmitted valueが残っている場合に発生するのではないかという気がする。submitted valueが残っていれば、Backing Beanの値ではなく、残っている submitted valueが表示される。h:commandButtonにimmediate="true"を指定すれば、Update Model Valuesフェーズはスキップされるからsubmitted valueが残ったままになる。submitted valueをクリアする手段があれば解決するかもしれない。ただし、List<Member>の、目的のn番目に対応するsubmitted valueだけピンポイントで消す必要がある。
  • 今回の例の追加ボタンは、List<Member>の最後に要素を追加した。要件によっては途中に挿入したいケースもあるかもしれない。試していないが、削除が思ったような動作にならかったのと同じ理由で、途中へ挿入した場合も正しく表示できない気がする。 

 

Mavenで一部のテストの実行/除外を切り替える

やりたいこと

  • 一部のテストは、普段(mvn test で実行したとき)は実行されないようにする。
  • ただし、mvn の引数で何かを指定すれば実行できるようにする。
  • また、そのテストは、NetBeansから「ファイルのテスト」で単独実行されるようにする。

使用した環境

JUnitの@Categoryを使ってテストをグループ分け

「一部のテスト」には、JUnitの@Categoryでカテゴリを指定することで、他のテストと区別する。
Maven側では、特定のカテゴリのみテストを実行したり、テスト対象から除外したりできる。

カテゴリを指定するためには、カテゴリを表すマーカー用のクラスかインタフェースを作り、それをテストクラスに@Categoryを使って指定する。

マーカー用のインタフェースの例。インタフェース名は何でもよい。 (src/test/java 配下に置く)

package sample.maven.test;

public interface ExtraTest {
}

@Categoryを指定したテストクラスの例

package sample.maven;

import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.experimental.categories.Category;
import sample.maven.test.ExtraTest;

@Category(ExtraTest.class)
public class GreeterSlowTest {
    @Test
    public void testSlowSayHello() {
        String actual = new Greeter().sayHello("Suzuki");
        String expected = "Hello Suzuki!";
        assertEquals(expected, actual);
    }
}

普段のテストから指定のカテゴリを除外する

maven-surefire-plugin プラグインを設定して、指定のカテゴリをテスト対象から除外する。
pom.xmlmaven-surefire-pluginの設定がなければ、暗黙の設定がどうなっているかを、mvn help:effective-pom で確認できる。mvn help:effective-pom を実行すると、pom.xmlで記述していないものも含め、現在有効になっている設定が表示される。試した環境では、maven-surefire-plugin の暗黙の設定は次のようになっていた。

 <plugin>
   <artifactId>maven-surefire-plugin</artifactId>
   <version>2.10</version>
   <executions>
     <execution>
       <id>default-test</id>
       <phase>test</phase>
       <goals>
         <goal>test</goal>
       </goals>
     </execution>
   </executions>
 </plugin>

これを自分のpom.xmlに貼り付け、pom.xmlは次のようにした。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>sample</groupId>
    <artifactId>MavenSample</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>MavenSample</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version> <!-- 4.8以上が必要 -->
            <scope>test</scope>
        </dependency>
    </dependencies>
  
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.16</version>  <!-- 2.13以上? が必要  -->
                <executions>
                    <execution>
                        <id>default-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <excludedGroups>sample.maven.test.ExtraTest</excludedGroups>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

ポイントは、

  • surefireの設定にexcludedGroupsを追加して、除外したいカテゴリのマーカー用クラス/インタフェースを指定。

また、

  • surefireプラグインのバージョンは、おそらく、2.13以上 (それより古いバージョンの場合、JUnit47 providerの設定が必要らしい。)
  • JUnitのバージョンは4.8以上

以上の設定で、 mvn test を実行したときには @Category(ExtraTest.class)の付いたテストは対象から除外される。

指定のテストだけ実行するための設定

surefireプラグインは、パラメータgroupsにカテゴリのマーカー用クラス/インタフェースを指定すると、そのカテゴリだけを対象にテストを実行する。これを利用すれば所望の設定ができる。

一方、surefireプラグインは、上記の設定により、ExtraTestは除外するよう設定済みである。しかし、
executionタグを追加し既存とは異なるidを指定することにより、別の設定のsurefireプラグインを追加することが可能となる。

            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.16</version>
                <executions>
                    <execution>
                        <id>default-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <excludedGroups>sample.maven.test.ExtraTest</excludedGroups>
                        </configuration>
                    </execution>
                    <!-- ここから追加 -->
                    <execution>
                        <id>extra-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <groups>sample.maven.test.ExtraTest</groups>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

ここでは、extra-testというidでsurefireの設定を追加した。

なお、以下の指定は

    <phase>test</phase>
    <goals>
        <goal>test</goal>
    </goals>

次のような意味になる。

surefireプラグインが提供しているゴールは"test" 1個だけであり、そのゴールはJUnitなどを使ってテストを実行する。つまり、mvn test と実行すれば、extra-testと名づけた設定でsurefireプラグインによるテストが実行される。

この設定のままだと mvn test したときには、デフォルト(id: default-test)と、追加した id: extra-test の両方が実行されてしまう。extra-testのほうは普段は実行せず、明示的に指定されたときだけ実行されるようにしたい。これは、surefireプラグインのパラメータ skipTests と、mavenのプロパティを利用して実現できる。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <skipExtraTest>true</skipExtraTest> <!-- これを追加 -->
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.16</version>
                <executions>
                    <execution>
                        <id>default-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <excludedGroups>sample.maven.test.ExtraTest</excludedGroups>
                        </configuration>
                    </execution>
                    <execution>
                        <id>extra-test</id>
                        <phase>test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <skipTests>${skipExtraTest}</skipTests> <!-- これを追加 -->
                            <groups>sample.maven.test.ExtraTest</groups>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

surefireプラグインのskipTestsパラメータにtrueを指定すると実行はスキップされる。 skipTestsの値は、独自に追加したskipExtraTestプロパティで与えるようにして、skipExtraTestプロパティのデフォルト値はtrueにしておく(propertiesタグに追加した設定により指定している)。すなわち、デフォルトではスキップされる。

extra-testを実行したい場合は、次のようにskipExtraTestをfalseにして実行すればよい。

mvn -DskipExtraTest=false test

NetBeansからの実行

テストプログラム作成中は「ファイルをテスト」「ファイルのテストをデバッグ」を使って、作成中のテストクラスだけ実行させるのが便利だが、この場合は、@Categoryの有無にかかわらず指定のテストクラスが実行される。

どうやらこのケースの場合は、ここまでで定義したid: default-test, id: extra-testのどちらの設定も適用されていないようだ。「ファイルをテスト」の場合、NetBeansは、mvn test ではなく mvn surefire:test のように、直接ゴール(surefire:test)を指定して実行している。そしてこの場合適用される設定は id: default-test ではなくid: default-cli となるようだ。このことは、mvn surefire:test を実行したときの次の表示から推測できる。

--- maven-surefire-plugin:2.16:test (default-cli) @ MavenSample ---

なお、実行>プロジェクトをテスト の場合は、mvn test で実行されるため、id: extra-testはスキップされる。

備考

プラグインの設定記述例としてよく見かけるのは、次のようにexecutionsタグを使わずにconfigurationを書いている例だが、この場合は、暗黙に定義されているものも含め、すべてのidの設定にこの設定が追加されるようだ。

            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.16</version>
                <configuration>
                    <excludedGroups>sample.maven.test.ExtraTest</excludedGroups>
                </configuration>
            </plugin>

このため、上記の設定であれば、NetBeansから「ファイルをテスト」を実行した場合にもExtraTestは除外される。つまり、ExtraTestをつけたテストクラスは「ファイルをテスト」では実行できなくなってしまう。

@javax.faces.view.ViewScoped を使うとWELDのManagedBeanでNotSerializableException

JSFで (javax.faces.beanのほうではなく)javax.faces.viewの@ViewScopedを使うと、セッション情報がシリアライズされるタイミングで以下の例外が出てしまう。

情報:   Cannot serialize session attribute com.sun.faces.application.view.activeViewContexts for session a4549e255f0feda9b2f59f24a39a
java.io.NotSerializableException: org.jboss.weld.bean.ManagedBean

環境はNetBeans 7.4 + GlassFish 4.0。 Mojarra を2.2.0 から 2.2.5 へ入れ替えても現象は変わらなかった。

セッション情報のシリアライズはたとえば以下のタイミングで発生する。

  • NetBeans上でプログラムを更新して再デプロイされるとき。
  • GlassFishのSession Managerを設定して、"memory"以外のPersistence Typeを指定しているとき。

※ STATE_SAVING_METHOD に clientを指定している場合もシリアライズが発生しそうだが、以下の説明によると、発生しないのかもしれない(未確認)。

セッション永続化によるリカバリ機能や負荷分散を使う予定がなければ、実質的に困ることはないかもしれない。とはいえ、あとからそれらの機能に頼りたい状況にもしなってしまったときに選択肢が減ってしまうし、NetBeansでの開発中にしょっちゅう例外が飛ぶのは気持ち悪い。

直接的な原因は例外メッセージのとおり、WELDのorg.jboss.weld.bean.ManagedBeanがSerializableになっていないことであるが、WELDとしては、そもそもManagedBeanにSerializableを期待するところが誤りとの見解らしい。

MojarraのJIRAには関連しそうな情報は今のところ見当たらない。MyFacesでは・・・WELDの問題としている??


回避策は見つからないので、この現象を避けたければ @javax.faces.view.ViewScoped はあきらめるしかなさそう。CDIの@SessionScopedなら問題ない。また、古い @javax.faces.bean.ViewScoped でも問題は発生しない。@javax.faces.bean.ViewScoped は @javax.faces.view.ViewScoped と同様に使えるようなので、困ることはなさそう。@javax.faces.bean.ViewScoped なBean内の変数にCDIの@Injectで別のBeanの注入するのも問題なくできた。デメリットがあるとすれば:

  • CDIのNamedやXxxScopedと旧JSFのを混在させるのがわかりにくい。
  • @javax.faces.bean.ViewScoped は deprecatedとなることが予定されている。
  • @javax.faces.bean.ViewScoped なBeanは別のBeanに@Injectで注入できない。

備考

HttpSessionにセットされているデータを見てみると、@javax.faces.view.ViewScoped を使ったときには、

  • 名前 com.sun.faces.application.view.activeViewContexts に ConcurrentHashMap がセットされており、
  • その ConcurrentHashMap の先で ConcurrentHashMap がネストして、
  • そのキーで ManagedBean が使われていた。

古いほう(@javax.faces.bean.ViewScoped)を使ったときは、com.sun.faces.application.view.activeViewContexts はHttpSessionにはセットされていなかった。