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

プログラミングメモ

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

Symfony 2.8とPHP-DIのAuto Wiring

Symfony

この記事は 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を複数のスレッドで利用する

Android

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で作成する

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で一部のテストの実行/除外を切り替える

Java

やりたいこと

  • 一部のテストは、普段(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

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にはセットされていなかった。

gitの使用方法メモ

mercurialからの類推で git を使おうとしたが挫折。
gitの説明書を読んで基本的な概念と操作をまとめてみる。

参照した説明書は次の2つ。

動作は git version 1.7.10.4 で確認した。

基礎概念

コミット
ファイルツリーのある時点でのスナップショット。mercurialでのchangeset。コミットは、61c4efc3〜 のような16進数の番号が付けられている。gitコマンドの引数でコミットを指定するときはこの番号を使う。
ブランチ
開発ライン。コミットの連なり。動作上は、コミットを指すポインタと考えてよい。その意味では、ブランチは、一連のコミットの最新を指していると考えることができる。コミットの連なりは途中から分岐できるため、複数のブランチを持つことができる。gitコマンドでコミットを指定する箇所には、基本的にブランチ名も指定できる(ブランチの最新コミットを指定したことになる)。
HEAD
現在のブランチ。作業ディレクトリには、いずれか1つのブランチを選択して、その最新コミットの内容を取り出す。HEADは、作業ディレクトリの取り出し元となったコミットともいえる。mercurialのheadとは異なる。gitコマンドでブランチ名やコミットを指定する箇所には、基本的に"HEAD"を指定できる(作業ディレクトリの取り出し元コミットを指定したことになる)。

ファイルの編集とリポジトリへの登録

  1. ブランチを選択して、ファイル一式を作業ディレクトリに取り出す(checkout)。
  2. ファイルを編集する。または、追加・削除する。
  3. 変更後のファイルを「ステージング領域」に登録する。または、ステージング領域に新規ファイルを登録したり、ファイルの削除を登録する。
  4. 2, 3 を必要なだけ繰り返す。
  5. ステージング領域の内容をcommitしてコミットを作成する。

同じブランチを対象に作業する限り、1は再実行しなくてよい。

git の help に出現する用語 "index" はステージング領域を指す(厳密には別物かもしれないが・・・)。

作業対象のブランチの切り替え (checkout)
git checkout <ローカルブランチ名>

ローカルブランチの代わりにリモートブランチやコミット(の番号)も指定できる。ある時点でのファイル一式を作業ディレクトリに取得してビルドしたり実行確認するのに利用できる。この場合、作業ディレクトリはどのブランチにも属さない状態(detached HEAD)になる。この状態でコミットしても、どのブランチにも影響を与えない。既存のブランチを誤って更新してしまう心配がない。

detached HEADの状態でもコミットは可能。ただし、コミット番号を覚えておかないと、別のブランチに切り替え(checkout)した後に戻ってこられなくなる。detached HEADな状態で今のコミットを新たなブランチとして登録するには、

git checkout -b <新しいブランチ名>

detached HEADな状態でプログラムの修正を試し、うまく行けばコミットし(場合によってはブランチ名を付け)、その後メインのブランチに戻って、そのコミットをマージする、という使い方もできる。

ステージング領域への登録、commit、変更の取り消し

https://cacoo.com/diagrams/fpbnI786Ta8lpOek-960E2.png

備考

  • checkoutコマンドは次の2つの機能を持っているため、混同しないように注意。
    • 引数にパスを指定しない → 作業対象のブランチを選択する。
    • 引数にパスを指定する → 作業対象のブランチは変更せず、指定のコミット、ブランチ、またはステージング領域からファイルを作業ディレクトリ(またはステージング領域)に取得。
  • 「checkout -- <パス>」などの -- は、後続の引数がパスであることを示すマークであって、単独の引数として意味があるわけではない(コミット名やブランチ名を表しているわけではない)。

diff

https://cacoo.com/diagrams/fpbnI786Ta8lpOek-DF6A5.png

マージ作業中のdiffについては、後述する。

リポジトリとの連携、マージ

リポジトリのcloneと、ローカルブランチ・リモートブランチ

cloneによって作られるリポジトリは、次の点で、clone元のリポジトリとは内容が異なる。

  • リポジトリをcloneすると、clone元リポジトリのブランチは、自分のリポジトリには「リモートブランチ」として登録される。
  • commitコマンドにより更新できるのは「ローカルブランチ」である。「リモートブランチ」は直接は更新できない。
  • cloneすると、ローカルブランチも自動的に作成される。

https://cacoo.com/diagrams/fpbnI786Ta8lpOek-80515.png

リポジトリは、複数リポジトリと連携できる。ここで言う連携とは、push/pull(fetch)の相手となることである。複数リポジトリと連携するため、それぞれに名前を付けて管理する。cloneすると、clone元には"origin"という名前が自動的に付く。名前の一覧表示は git remote で、名前の登録は git remote add で行う。

ローカルブランチとリモートブランチの間に「追跡」関係を設定すると、push/pull(merge)の際の相手が自動的に決定できるようになる。cloneしたときは自動作成されたローカルブランチとリモートブランチとの間に追跡関係が自動設定される。

fetch

連携している別リポジトリから内容をコピーするには、git fetch を実行する。

git fetch <リポジトリ>

指定したリポジトリから取得する。

git fetch --all

名前を付けて登録されている(git remoteで登録)別リポジトリすべてから取得する。

git fetch

現在のブランチと追跡関係にあるリポジトリ(と思われる・・・)だけから取得する。

https://cacoo.com/diagrams/fpbnI786Ta8lpOek-0B28B.png

merge

現在のブランチに、指定したブランチの最新コミットの内容をマージする。

git merge <ブランチ名>

https://cacoo.com/diagrams/fpbnI786Ta8lpOek-AE847.png

備考

  • git merge では、ブランチ名の指定は必須。以下の設定をすると、ブランチ名を省略できるようになり、省略した場合は追跡関係にあるリモートブランチ*1が指定されたとみなす。
git config merge.defaultToUpstream true
  • git pull は、fetch と merge をまとめて実行。このときは、ブランチ名の指定は省略できる。
  • fetchの実行後に、分岐が発生しているか(mergeが必要か)を確認するには、git status を実行すればよい。次のような形式で表示される。
# On branch dev
# Your branch and 'origin/dev' have diverged,    ← have diverged = 分岐している
# and have 1 and 1 different commit each, respectively.
#
nothing to commit (working directory clean)
push

リポジトリに自身の内容を送り出す。

https://cacoo.com/diagrams/fpbnI786Ta8lpOek-CE397.png

備考

  • ブランチを更新していなくても、送出先のブランチが更新されていると(自分のほうが古いと)、pushは失敗する。
  • デフォルトでは、ローカルブランチが複数ある場合、それらすべてをpushしようとする。したがって、自分が作業しているのとは別のブランチについて、送出先のブランチが更新されている場合にもpushが失敗する。なお、これは、fetchすれば解消する問題ではなく、mergeが必要である。fetchで更新されるのはリモートブランチであり、push対象となるローカルブランチはfetchしても更新されない。ローカルブランチの内容をリモートブランチと合わせるにはmergeが必要である。
  • push時に、作業中のブランチについてのみpushされるようにするには、次の設定をする。この設定により、前述の問題は回避できる。
git config push.default upstream
マージ作業中のdiffとファイル取得

git merge でマージにコンフリクトが生じた場合は、手作業で解消する必要がある。
このとき、ステージング領域には、commit予定のファイルを置く領域とは別に、stage1, stage2, stage3 という、3つの別の領域が設けられる。これら3つの領域とのdiffを確認することで、マージ対象となるブランチそれぞれとの差分を確認できる。

https://cacoo.com/diagrams/fpbnI786Ta8lpOek-819A2.png

備考

  • マージ中は、マージの相手となるコミットにMERGE_HEADという別名が付く。git diff MERGE_HEAD で、マージ相手と作業ディレクトリの差分を確認できる。
  • git diff MERGE_HEAD と git diff --theirs (stage3との比較) とは異なる。stage3と作業ディレクトリは、自動的にマージできたものは反映済みである。したがって、git diff --theirs はコンフリクトが生じた部分のみ表示される。MERGE_HEADは、マージ相手そのものである。したがって、git diff MERGE_HEADは、自動マージ済みの部分も差分として表示される。
  • 次のコマンドで、statge0〜3のステージング領域のファイルの内容を表示できる。は0〜3の番号。
git show ::<パス>
  • gitがコンフリクトによるマージ中断したファイル(<<< >>> などのマークが入ったもの)を編集後に、編集前の <<< >>>などが入った状態に戻したい場合は、git checkout -- <パス> を実行すればよい。
  • マージがコンフリクトした場合で、マージ作業を中断してマージを取り消すには、 git merge --abort

ブランチの操作

一覧
  • ローカルブランチ一覧、現在のブランチの確認
git branch

現在のブランチは * 記号付きで表示される。

  • ローカルブランチ一覧、最新のコミットコメント付き
git branch -v
  • リモートブランチ一覧
git branch -r
作成
  • ブランチを作成
git branch <名前> <元のコミット>

<元のコミット>省略時は、現在のブランチを元にする。

  • ブランチ作成と同時にcheckout(作業ディレクトリ内容も更新)
git checkout -b <名前> <元のコミット>

branch, checkout -b のどちらの場合も、元のコミットがリモートブランチなら、追跡関係が自動的に設定される。

git push --set-upstream <リポジトリ> <ローカルブランチ>

<リポジトリ>はoriginなど。ローカルブランチと同じ名前で別リポジトリに作成する。
--set-upstreamを付けることで、ローカルブランチと新規追加されるリモートブランチに追跡関係が設定される。
ローカルとは別の名前で別リポジトリに登録したい場合は、

git push --set-upstream <リポジトリ> <ローカルブランチ>:<別リポジトリでのブランチ名>

履歴表示

  • 今のブランチのログを表示
git log

今いるブランチのログだけ表示される。

  • 別のローカルブランチや、リモートブランチのログを表示
git log <ブランチ名>

連携している別リポジトリのログが参照したい場合は、git fetchで内容をコピーして、git log origin/master のようにリモートブランチ名を指定してログを表示する。

表示内容の選択
  • diffも合わせて表示
git log -p
  • 変更されたファイルの一覧と変更量も表示
git log --stat
  • 1コミット1行で表示
git log --oneline  
  • 分岐・マージのわかるグラフ付きで表示
git log --graph
表示する履歴の絞込み
  • master(の最新)の祖先。ただし、v2.5とその祖先は除く。
git log v2.5..master

v2.5の延長にmasterがある場合は、単純にv2.5の次からmasterまで。途中で分岐してv2.5とmasterに分かれた場合は、分岐地点の次からmasterまで。

  • master(の最新)の祖先と、v2.5の祖先。ただし、masterとv2.5の共通の祖先を除く。
git log v2.5...master

途中で分岐してv2.5とmasterに分かれた場合は、分岐地点からv2.5までと分岐地点からmasterまで。

  • コミットコメントにfooが含まれるもの
log --grep foo


hg incoming と同様のこと(リモートブランチにあって、自分のブランチに無いコミットを表示)がしたい場合は、git fetchした後で、たとえば次のようにする。

git log HEAD..origin/master

hg outgoing と同様のこと(pushにより送出されるコミットを表示)がしたい場合は、

git log origin/master..HEAD

HEADは省略できるので、これらは単に、次のように実行してもよい。

git log ..origin/master
git log origin/master..

*1:このブランチは、.git/configの[branch "ブランチ名"]のremote=とmerge= で指定されている