SIerにはコード記述の自動化からビルド・デリバリの自動化へのトレンドの変化を理解してほしい
ちょっと前にTogetterで作成したまとめに対して大きな反響をいただきました。
SIerは自動化する対象が違っているのでは? - Togetter
これは、私が
- 作者: Jez Humble,David Farley
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2010/07/27
- メディア: ハードカバー
- 購入: 3人 クリック: 141回
- この商品を含むブログ (23件) を見る
をきっかけに始まったTL上での議論をまとめたものです。この本は、7月に行われたアジャイルカンファレンス東京にて、マーティン・ファウラーとともに来日したジェズ・ハンブル氏によって著された本で、今年のJolt Awardにも入っており英語圏では話題となっているようです。
この1年の優れたIT系書籍はどれか?「Jolt Awards 2011」が6冊を発表。 - Publickey
以前から、Excelで記述された画面項目定義書やテーブル定義書からソースコードを自動生成するような、SIerの提供するフレームワークにありがちなツールは、個人的にあまり好きではありませんでした。(SIerがExcel→Javaのコード自動生成をPGに押し付けるのは善か悪か? - 達人プログラマーを目指して)ただし、一方でアジャイル開発では自動化を重視するということも知っており、同じ自動化でもなんとなく嫌いな自動化と好きな自動化があるということを感じてきたのですが、今回のTwitter上での議論をきっかけに、どうして同じ自動化でも好き嫌いがあるのだろうということがなんとなくわかった気がします。
つまり、頭をあまり使わなくてもできるくり返しの単純作業は、なるべくツールを使って自動的にできるようにする一方で、複雑なドメインロジックの記述など、プログラマーの能力を発揮すべきところでは、テスト駆動でクリーンコードを保ちながら頭を使ってリファクタリングを続けるべきということですね。ジェズ・ハンブル氏も来日した際の講演の中で「アジャイルは優秀な人の能力を発揮できるようにすべき」というようなことを述べていたと記憶しています。自動化も使いどころを間違えると、善にも悪にもなり得るということですね。ソフトウェア開発では、単に特定の工程の全体を自動化すればよいというものではないということです。
もちろん、たとえば、ビジネスロジックが本当に単純な場合(データベースのCRUDと基本的な項目レベルのチェックのみ)については、EDD*1を使って、すべてのコードを自動生成するということも有効かもしれません。しかし、後から独自のロジックの記述が必要となった場合、自動生成のプロセスが入ることで以下のような点が必ず問題となります。
- 自動生成されたコードと手でメンテナンスするコードとの同期が問題となる。
- 開発者が自動生成されたコードの内容を理解していない場合、障害解析や機能拡張が困難となる。
- 通常、自動生成されたコードは冗長で、無駄な記述を含んでいることが多い。また、実質同じような内容のコピーとなる傾向が高い。
- 自動生成の都合で余分なクラス、インターフェース、設定ファイルの生成が行われる結果、侵略的なフレームワークとなる傾向がある。
- 画面項目から自動生成されたコードは基本的にモジュールやレイヤー構造を持たないSmart UIアンチパターンとなる傾向が高い。
それゆえ、ソースコードの自動生成については、採用すべき箇所や、採用の是非について十分に検討した上で、適切に使い分ける必要があります。間違っても、開発者のスキルが低いからアプリケーションの全体を自動生成させようなどと考えるべきではありません。
さらに、CRUD処理のような簡単なアプリケーションは、そもそも手動で記述しても、コードの記述自体にそれほど時間がかかるわけではなく、自動化による工数削減メリットは限定的です。そういうところが自動化されたとしても、保守の部分まで含めれば、全体のコストが大きく下がるとは思えません。費用対効果からいえば、そのような部分の自動化は、最後の仕上げでやれば十分とも考えられます。
一方で、ドメインモデリングなどのプログラマーの設計スキルが必要な領域であれば、コードをエディタを使って実際に書き下す時間などは、インターフェース設計、リファクタリングやテストの作成工数から考えれば、無視できるレベルのものなのではないでしょうか?
一方で、以下の部分は繰り返し行われるにもかかわらず、毎回手動で実行した場合は基本的に単純作業となります。
- 開発環境の準備と構築
- アプリケーションプログラムのビルド
- 静的解析の実行とチェックレポートの作成
- テストの実行とテスト結果の検証
- テスト環境、ステージング環境、本番環境へのデプロイ
システムは一回作って終わりなのではなく、段階的に機能追加したり、バグを修正するなどのメンテナンスを行うなど運用のコストもかかります。そのような場合には、以上のような単純作業を自動化してくり返し実行できるようになれば、大きなコスト削減につながるはずです。さらに、開発チームと運用チームとの間の距離を小さくすることができ、価値のあるソフトを短期にデリバリできるようになります。*2
特に、最近は開発環境一式を仮想イメージとして提供して、そのままクラウドでも実行可能にするようなツールがいろいろと利用できるようになっているみたいです。たとえば、
Bitnami Application Catalog
Cloud Foundry – Open Source Cloud Application Platform
OpenShift: Container Application Platform by Red Hat, Built on Docker and Kubernetes
http://www.cloudbees.com/
さらに、開発環境はLinuxの方が自動化しやすいと思いますが、会社のWindows PCでも、以下のようなツールを使えばある程度開発環境の構築を自動化することができると思います。
Ninite - Install or Update Multiple Apps at Once
Google Code Archive - Long-term storage for Google Code Project Hosting.
IDE Management by Secure Delivery Center - Genuitec
とにかく、伝統的な慣習にしたがってプログラミングを下流や製造の工程ととらえ、コスト削減の対象としか考えないのは、今では完全に時代遅れになっています。むしろ、(マスターメンテナンス用のCRUD処理などの例外を除いて)、ソースコードはそこから新たな価値を創造する資産なのであり、そういうところこそ、職人プログラマーによってきちんと作成されるべき対象と考えるべきではないのでしょうか。逆に、本来高給取りのSEがやるべきでないビルド、デリバリ、テスト実行などの単純作業は、なるべく自動化されることでコストを下げることができます。
もちろん、こういうアジャイル的な発想はSIerの側からは、ビジネスの収益構造上からも、なかなか出てきにくいかもしれませんが、ユーザー企業、ユーザー系SIerからシステムが業務にもたらす価値という点を意識をするようになれば、日本のSI業界でも少しずつ取り入れていくことができるのではないかと思います。
オラクルさんのWeblogicセミナーでJava EEについてディスカッションしてきました
前回の記事で予告させていただいたとおり、本日、WebLogic & Java EE 活用セミナーの最終セッションの座談会にパネラーとして登壇させていただきました。今回、このような機会を与えていただいたオラクルさんの関係者の皆様、また、セミナーに参加された皆様、どうもありがとうございました。(座談会の内容はオラクルのFusion Middlewareのアカウントである@OracleMiddle_jpでもツイートされていますのでご参照ください。また、今回の勉強会のハッシュタグは#0906wlsjavaeeとのことです。)
今回は私がJava EEの開発者の立場から、斉藤さんがユーザー企業の情報システムの管理者・アーキテクトの立場から、新野さんが技術トレンドのより広い視野からJava EEをみた視点を提供するという立場でそれぞれ意見を交換しました。
今回のセミナーについての正式なレポート記事はまたアップされるとのことなので、ここでは私の話した内容を中心に報告させていただきます。
以下、記事が掲載されました。
OracleがWebLogic Serverの製品戦略とJava EEへの今後の取組みを発表:EnterpriseZine(エンタープライズジン)
http://www.oracle.co.jp/campaign/weblogic/columns/column01/column05/post_24.html
ここ数年のJava EEシステム開発の状況について
まず最初に、日本におけるJava EE市場の現状認識について、モデレーターのオラクル伊藤さんより「ここ2、3年でJava EEシステム開発の状況はどう変化してきているでしょうか?それぞれの開発現場の状況を踏まえて教えてください。」というご質問をいただきました。
まず、斉藤さんの方から「製品や仕様は進化していても、現場の開発手法はあまり変わっていない」という答えがあったのですが、それについてはこのブログでも時々書いているように、私の立場から見ても、実際、そのように感じるところが多かったですね。寺田さんのセッションで「Strutsを使っている方」という質問があって、かなりの人が手を上げていたようですが、最新の技術や開発手法を使って開発を行っているプロジェクトは残念ながらあまり多くない印象です。(Struts1に代わるWebフレームワークの選択 - 達人プログラマーを目指して)
Java EE6の普及に限って言えば、現状では
- Weblogicなど主要な製品がEE6に完全対応するまでもうしばらく時間がかかる(来年にはEE6完全対応のWeblogic 12.1.1がリリースされるとのこと)
- 日本語の書籍がまだほとんど出版されていない*1
ということも大きな原因でないかとは思いますが、それにしても、Java EE5やSpring、Seasar2のような軽量Javaの考え方も思ったよりは現場に浸透していないようですね。
実際の案件において、UIなど目に見える部分の細かい改良についてはユーザーに興味を持ってもらえるところなのですが、xmlの分量が80%削減されるといったようなEoDに関するところや、コードの理解容易性や拡張性、テスト容易性といったアーキテクチャに関する部分の重要性はどうしても理解されにくいところはあると思います。そういうこともあって、EOL*2に従って基盤のバージョンはアップするけれども、開発フレームワークや開発手法については以前のやり方のままといったケースもあるのではないでしょうか。
実際、私も開発効率のよくないフレームワークの利用が社内標準で決まっていた*3にも関わらず、Ajaxなどを駆使した相当高度な画面を作成しなくてはならないという、非常にむずかしいプロジェクトに参加したことがあります。その時は、開発生産性向上のためSpringやJPAを積極的に利用して開発を簡易化するという工夫をしました。その結果、コードや設定ファイルの分量を何分の1以下に削減することができ、かなり長期にわたって機能拡張が可能なシステムに仕上げることができました。そういう経験を通して、EoDは単に開発期間を少なくするということだけでなく、将来のシステムの保守性や発展という意味でも非常に重要な意味があるのだということを体験することができたと考えています。
EoDとシステムの保守性のつながりについては、なかなかユーザーにメリットを説明しにくいところなのですが、私としてはこういう点を強調させていただきたいですね。もちろん、案件によってはアーキテクチャやEoDの観点を重視して成功しているところもあると思いますし、そういう意味で二極化が進んでいるのではというお話をさせていただきました。
日本でJava EEがいまいち盛り上がらない理由
それから、日本でいまいちJava EEが盛り上がっていない理由の一つとして、海外ではユーザ企業が主体でシステムを構築するのに、日本ではSIerにおまかせで開発しているところが多いという違いについても言及させていただきました。つまり、もともと海外市場で主にJava EEがターゲットにしていたのは、文字通りITシステムを活用してビジネスをするユーザー企業が中心であり、それゆえ
- 最新技術の採用よりも、むしろ、既存情報資産の活用
- 比較的長いライフサイクル(互換性)
- 既存部品の再利用による短期開発と短期市場投入
- 部品化によるパッケージ、SaaSビジネスへの展開
- 長期にわたって保守可能なアーキテクチャ(レイヤ化、コンポーネント化)
といったアーキテクチャ的な側面に力点が置かれているのではないかと思います。そういうことで、コスト削減だけでなく、他社に先駆けたビジネスの展開などユーザ企業としてのJava EEのメリットを見出すことが可能なわけです。
一方、通常のSI案件の手法では保守性ということよりも、まず、納期までに確実に開発を完了させるということがどうしても中心になりがちで、Java EEのアーキテクチャは敷居が高く採用されにくいということがあるのではと考えています。もちろん、EJBを使ったシステムもあるわけですが、サービス単位できちんとコンポーネントに分割せずに、とりあえずCommandパターンで機能ごとに実装を行うといったやり方が、実際に日本ではこれまで多かったように思います。(JavaEE標準の進化から最近の業務アプリケーション開発手法の変遷について考える - 達人プログラマーを目指して)
さらに、これもよく言われることですが日本ではカスタム開発が中心でパッケージの市場が盛り上がっておらず、コンポーネント開発などでISVが参加して収益を上げるというモデルがうまくいっていないということも理由として考えられます。いずれにしても、今まで日本でJava EEが盛り上がっていないのは、いろいろな意味で金脈がなかったということですね。
やはり、こういう状況を改善していくためには、ユーザー企業の意識改革や啓蒙といったことが大切になるのではないかと思います。そして、斉藤さんもおっしゃっていましたが、単に納期を短くするという発想だけでなくて、今後は長期にわたった機能拡張などの保守性などの大切さをもっと訴えていく必要があると思います。
Java EEが標準であることの重要性
伊藤さんより「Java EEとコンシューマー系における技術発展とのギャップは?」というご質問がありました。確かに、コンシューマー系(いわゆるWeb系)ではJava EEはそれほど普及していない印象があります。これに対しては、私は正直その辺の技術動向に明るくないというところもあるのですが、Java EEはあくまでも標準なのだから、すべてのレイヤーやコンポーネントを標準で固める必要はなく、必要に応じて他の言語やフレームワークを組み合わせて作ればよいので、あまり技術のギャップという意識はないのではということをお答えしておきました。新野さんからも、今後はJava EEに限らず様々な技術が使えるのではないかというコメントがありました。
ただ、斉藤さんから「やはり銀行は標準を重視する」という発言があり、確かに安心感や導入のしやすさということを考えると、より幅広いエリアが標準化されているとよいとも思いました。
Java EEの存在意義
これについては、NoSQLなどの最新技術の導入というよりも、むしろ、
- 既存の情報資産の活用
- 10年にわたって蓄積されたライブラリーやツール
- 枯れたミドルウェアの存在
などの特徴が大きいのではということで、パネラーの意見が一致しました。あと、開発者の立場からこれに付け加えさせていただくと、Java EE6ではさらに、相当生産性の高いプログラミングモデルが提供されているということが魅力として挙げられますね。SpringやSeamなどのアイデアを取り込んで十分に洗練されたことにより、現在では実際に十分使えるレベルに成熟してきています。最新の仕様をきちんと理解して使いこなすだけで、他のプラットフォームと比較しても遜色のない開発生産性を得られると考えています。(EJBコンテナが分散コンポーネントモデルから軽量なDIコンテナに変化してきた歴史を振り返る - 達人プログラマーを目指して)
EoDは簡単でない?
EoD(Ease of Development)というと、どうしても「素人でも簡単に開発できる」というイメージを持たれることが多いのですが、実際は
- 値の詰替えなどつまらないコードの記述が不要になる
- パターン化された設定ファイルの記述が不要になる
といったことであって、実際にコードの記述が必要になる部分は、逆に「頭を使う必要がある」部分になるというちょっと逆説的な結果になるところがあります。
それで、EoDと生産性について、以下のようなグラフの関係が成り立つのではと私は考えています。
従来は設定ファイルなど開発が面倒な部分があったため、コンポーネント化や再利用などJava EE本来のメリットを活用するために払うべきコストが膨大でしたし、また、テスト自動化の工夫なども相当大変でした。そういう時代であれば、近道をしてコピペで機能ごとに開発*4を行い、テストも自動化しないといったやり方がトータルで現実的だったという面もあったかもしれません。
しかし、Java EE5、Java EE6とEoDを発展させる形で標準に改良が加えられた結果
といった本来ベストプラクティスとされてきた開発手法が、一般的な開発案件でも手の届く範囲に近づいてきました。
これは言い換えれば、Java EEの世界で達人プログラマーとしてのスキルを発揮できるしかけが、標準レベルでも利用できるように整ってきたといえるのではないでしょうか。
Java EE開発者は今後どうあるべきか
まず、考えるべきこととして、インフラ部分はますます抽象化される傾向にあると思います。
それゆえ、今後は顧客視点で業務をモデル化しアーキテクチャとして実現するアーキテクト的な役割が重要になると思います。新野さんも「今後は多能工が重要になる」ということをおっしゃっていました。
あとは、技術者として
- 社外の勉強会に参加しよう
- 積極的に情報発信しよう
- 最新技術にキャッチアップするためにも英語が必須
ということを述べました。私もちょうど1年前からこのブログを始め、今年から勉強会に参加するようになったばかりなので、あまり他人に偉そうなことを言える立場でないのですが、私自身そういう活動をするようになって、多くの技術者と交流することができ、本当に世界が変わりました。私もそうだったように最初はなかなか勇気が持てないかもしれませんが、今後技術者としてはプロジェクトや会社の範囲を超えて情報発信していくことも大切であると思います。
それから、やはり、英語ですね。これは私も苦手意識がなくならないのですが、Java EEの情報量などは日本語と英語では圧倒的に差がありますし、最新の技術を把握するうえでも、英語力は大切だと思いますね。それから、下手な英語でも積極的に情報発信することで、今後はグローバルな領域を相手にできれば、Java EE開発者としてさらに活躍する場が広がるのではないかと思います。
WebLogic & Java EE 活用セミナーのパネルディスカッションに登壇させていただくことになりました
9月6日(火)にオラクル青山センターで開催されるWebLogicとJava EEのセミナーの最後に行われるパネルディスカッションのセッションに登壇させていただくことになりました。
http://www.oracle.com/webapps/events/ns/EventsDetail.jsp?p_eventId=139806&src=7306109&src=7306109&Act=32
PublicKey編集長である新野さんと
マスタリングJavaEE5 第2版 (DVD付) (Programmer’s SELECTION)
- 作者: 三菱UFJインフォメーションテクノロジー株式会社斉藤賢哉
- 出版社/メーカー: 翔泳社
- 発売日: 2009/11/28
- メディア: 大型本
- 購入: 5人 クリック: 29回
- この商品を含むブログ (11件) を見る
WebLogic & Java EE活用セミナーということですが、基本的にパネルディスカッションの内容はWebLogicに限らず、また、新野さんが参加されていることからもわかるように必ずしもJava EEだけでなく、今後のエンタープライズシステム開発について議論することになっています。今回、私はJava EEを使った開発に長いこと関わってきた開発者の立場から議論に参加させていただく予定です。ただし、技術の細かい議論というよりは、今後の方向性といったような大きな話が中心になると思います。参加費用は無料ですので、興味のある方はどうかご参加ください。
日本でパターンが広まらない理由の一つは「ワンパターン」などのネガティブな和製英語のせい?
ソフトウェアアーキテクトの作業の一つに、システム全体の設計思想や開発方針を記述するアーキテクチャ説明書を作成をする仕事があります。そして、そのような設計書を記述する際に私はアーキテクチャパターンやデザインパターンの用語を利用します。例えば、
- システム全体をレイヤーアーキテクチャパターンに従い「プレゼンテーション層」「アプリケーション層」「ドメイン層」「インフラ層」に分割する。
- MVCアーキテクチャパターンにより表示ロジックとビジネスロジックを切り離し独立して画面を変更できるようにする。
- オブザーバーパターンを使ってイベントを監視する機能を容易に追加できるようにする。
といった具合にです。実際に、パターンの用語を適切に使うことで、どうしてそういう設計をするのかという設計判断を簡潔に記述できますし、関連するパターンに言及することでトレードオフや代替手段についても言及でき、情報量の厚みを増すことができます。また、こうしたアーキテクチャ上の設計判断が実際にパッケージ構造やクラス構造に反映され、開発チームの間でもこれらのパターンの用語がユビキタス言語として通用するようになれば、チームでのコミュニケーション力も向上します。
しかしながら、パターンの考え方が浸透していない組織に対しては、露骨にパターンという話をするとあまり良い印象をもたれないことが多いという問題もあります。最初は
- 教科書通りで工夫が足りない
- ワンパターン
といった反応が返ってくることも多くあります。パターンという言葉はあまり印象がよくないので、あえて使わないようにすべきと言われたことすらあります。
実際、最近もちょうどこのような議論があったのでちょっと調べてみたのですが、日本語のパターンには「ワンパターン」「パターン通り」といったネガティブな意味が非常に深く浸透しているにも関わらず、オリジナルのpatternという英語では(近い意味はあるにせよ)そのような使い方はしないようです。つまり、one patternという言い方は和製英語であり、英語を話す外国人には通じない可能性があるということです。
ワンパターン - Wikipedia
一方、英語のpattenという単語には「幾何学的な形や模様の繰り返し」といった意味が強くあるようです。
http://eow.alc.co.jp/pattern/UTF-8/
実際、洋書のパターンの本にはそういったきれいな模様が表紙に書かれていることが多いように思います。また、「パターン認識」という使われ方をするように、無秩序に見えるものから一定の規則を発見するという意味合いもあるようです。
日本においてはなかなかパターンの考え方が浸透しないのは、実はこうした言葉の印象の違いの問題も大きいのではないかと思いました。また、これもパターンに対するよくある誤解ですが、デザインパターンに書かれているいるクラス図の構造を「パターン通り」そのまま当てはめて失敗するということがよくあります。これも本来のパターンの思想とは違った間違った適用の仕方ですが、パターンという和製英語の印象からそのように勘違いして適用しているケースが多いのではないでしょうか。
ただし、実は海外においてもこのようなパターンの誤用による失敗事例は結構あるようですね。以下の本では、最初にそうしたパターンの間違った理解や適用方法について言及した後、400ページにもわたってパターンとは何かということが説明されています。(ちょっとマニアックな本ですが。)
- 作者: Frank Buschmann,Kevin Henney,Douglas C. Schmidt
- 出版社/メーカー: Wiley
- 発売日: 2007/04/13
- メディア: ハードカバー
- 購入: 1人 クリック: 28回
- この商品を含むブログ (4件) を見る
- パターンはそれを適用するコンテキストやきっかけとなる理由(フォース)が大切
- パターンの具体的な実装には様々なバリエーションが考えられる
- パターンは複数のパターンを組み合わることでより効果的になる
- パターンは機械的に設計を作り出すのではなく、考える人を対象にしたものである
といったようなことが書かれています。
確かにその考え方を理解して使いこなすことは容易ではありませんが、システムの設計において、パターンというものの価値を考え直してみるのもよいのではないかと思います。
Java EE6環境でJSF2を使う場合はCDIのBeanを管理Beanとして使う方がよい
先週の勉強会で紹介させていただいたjsf-scrumtoys-refactoredでは、JSFの管理Beanを使用する代わりにCDIのBeanを利用しています。この点説明が不十分だったので、ここで簡単に補足させていただきます。
JSFと管理Bean
勉強会の中で、JSFはコンポーネントベースのWebアプリケーションフレームワークであると説明させていただきました。(この点については以下のエントリーもご参照ください。Struts1に代わるWebフレームワークの選択 - 達人プログラマーを目指して)
コンポーネントベースのフレームワークの場合、VBやSwingといった伝統的な(Webでない)GUIアプリケーションのように、フォームや入力フィールド、ボタンといった画面コンポーネントのツリーが構築されます。そして、画面部品の入力やクリックなどのイベントに従って、管理Beanと呼ばれるPOJOに対してデータのやり取りやメソッドの呼び出しが実行されます。
JSF1.2までのxmlを使った管理Beanの定義
従来のJSF1.2までは、この管理Beanをfaces-config.xmlと呼ばれるxmlファイルにて登録する必要がありました。*1たとえば、今回のSkinActionに相当する管理Beanを登録するためには、以下のような設定が必要でした。
<managed-bean> <managed-bean-class>jsf2.demo.scrum.web.controller.SkinAction</managed-bean-class> <managed-bean-name>skinAction</managed-bean-name> <managed-bean-scope>session</managed-bean-scope> <managed-property>#{skinValuesAction}</managed-property> </managed-bean>
つまり、JSFの世界だけで簡単なDIコンテナのようになっておりsetterメソッドを使ってインジェクションが可能でした。
JSF2のアノテーションによる管理Beanの定義
JSF2.0では従来のJSF1.2のxmlを使った定義をそのまま拡張する形で様々なアノテーションが使えるようになっています。上記のような管理Beanはアノテーションを使うと以下のように書けます。
package jsf2.demo.scrum.web.controller; import java.io.Serializable; import javax.annotation.PostConstruct; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import javax.faces.bean.ManagedProperty; //@ManagedBean(name = "skinAction")と同様 @ManagedBean @SessionScoped public class SkinAction extends AbstractAction implements Serializable { private String selectedSkin; @ManagedProperty("#{skinValuesAction}") private SkinValuesAction skinValuesAction; ... }
これで、xmlの登録は不要になったのですがこの場合、以下のイマイチな点があります。
まず、前者の問題はDIがクラスなどの型情報でなくて、EL式で記述した文字列の情報に依存しているため、リファクタリングなどの変更に弱く、また、(IDEによるサポートが不可能ではないとはいえ)一般に間違いの検出が容易ではありません。さらに、xmlで明示的に管理Beanを宣言していないので、インジェクションする相手のソースコードを読んでBean名を調査しないといけないといったこともあります。
後者の問題は、もともとJSF1.2の頃からあった問題ですが、DIできる対象がJSFの管理Bean同士に限られるということですね。通常は管理BeanからEJBなどサービス層のBeanを呼び出すことが多いのですが、その場合にプログラミングのやり方が統一されていませんでした。さらに、EJBそのものをJSFの管理Beanとして直接利用するということもできません。どんなに単純なケースであっても、必ず管理Beanを作成し、そこからEJBを呼び出すといったことが必要になります。
CDIのBeanをJSFの管理Beanとして使うメリット
リファクタリング版のScrumToysではJSFの管理Beanの使用をやめ、代わりにCDIを導入しています。CDIの仕様に従うとwarモジュールのWEB-INFフォルダ中にbaens.xmlが格納されていれば、そのwar全体でCDIが有効になりますが、CDIが有効になっているwarモジュールでは
の条件を満たす任意のクラスは自動的にBeanとしてコンテナに登録されることになっています。*2また、EJBは一般のクラスとは違い特別な扱いを受けますが、やはり、CDIのBeanとして登録されます。
CDIのプログラミングモデルの基本は非常に簡単で、このように自動的に登録されたBean同士は@InjectによってDIすることが可能というものです。この時、基本的には型情報をもとにDI対象が決定されます。*3この場合、CDIのBeanの定義は以下のようになります。
package jsf2.demo.scrum.web.controller.skin; import jsf2.demo.scrum.infra.web.controller.AbstractAction; import java.io.Serializable; import javax.annotation.PostConstruct; import javax.enterprise.context.SessionScoped; import javax.enterprise.inject.Model; import javax.inject.Inject; @SessionScoped @Model // @SessionScoped @Namedと同義、また@SessionScoped @Named("skinAction")と同義 public class SkinAction extends AbstractAction implements Serializable { private String selectedSkin; @Inject SkinValuesAction skinValuesAction;
ここで、@SessionScopedのアノテーションのパッケージがJSF2の管理Beanのパッケージとはimport元が違っている点に注意してください。また、DIは文字列ではなく型に基づいて行われていることに注意してください。なお、@Namedで文字列の名前をBeanに与えているのはxhtml画面定義中のEL式から名前で参照するためで、インジェクションのために定義しているのではありません。
このようにCDIのBeanを使うことで
といったメリットが得られます。
CDIの更なるメリットと注意点
それに加えて、CDIではJava EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。 - 達人プログラマーを目指してでも説明したように、従来のJSFでは利用できなかった、会話スコープを利用することが可能になっています。つまり、@ConversationScopedを利用することで、Beanを丸ごとセッションスコープに格納する代わりに特定の画面遷移ごとに会話スコープを定義することができます。会話スコープは複数同時進行させることもできるため、タブブラウザやマルチウィンドウのアプリケーションでは特に有用です。
しかし、CDIをJSFと組み合わせる場合には以下のような点に注意する必要があります。
利用可能なスコープの違い
JSF2で定義されているスコープは
- @javax.faces.bean.ApplicationScoped
- @javax.faces.bean.SessionScoped
- @javax.faces.bean.RequestScoped
- @javax.faces.bean.ViewScoped
- @javax.faces.bean.NoneScoped
- @javax.faces.bean.CustomScoped
があります。一方、CDIで利用可能なスコープは
- @javax.enterprise.context.ApplicationScoped
- @javax.enterprise.context.SessionScoped
- @javax.enterprise.context.RequestScoped
- @javax.enterprise.context.ConversationScoped
- @javax.inject.Singleton疑似スコープ
- @javax.enterprise.context.Depenent疑似スコープ
となっていて、ほとんど共通しているのですが完全には対応していません。(大混乱に陥っているJavaEE 6のアノテーションに関する使い分けについて - 達人プログラマーを目指して)特に、JSFで有用なViewScopedは直接は対応するスコープがCDIでは利用できません。このようなViewスコープをCDIでも利用する方法については、Java EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。 - 達人プログラマーを目指してで書いた通りです。
JSFのコンバーターやバリデーターがCDIのBeanとして利用できない
管理Bean以外にも、コンバーターやバリデーターといったJSF固有のBeanが存在します。これらも従来JSF1.2まではxmlで宣言していましたが、JSF2からはアノテーションで登録できるようになりました。ただし、この場合これらのBeanはJSFの側で生成、管理されてしまい、CDIのコンテナと統合されていないといった制約が現状はあるようです。
したがって、これらのBeanに対して@Injectを使ってEJBや他のBeanを普通にインジェクションすることができません。さらに、コンバーターなどは管理Beanでもないため、@EJBによるインジェクションもできないようです。よって、以下のように面倒でもJNDIルックアップが必要になってしまいます。
@FacesConverter("projectConverter") public class ProjectConverter implements Converter { // @Injectも@EJBも動作しないため、JNDIルックアップが必要 private ProjectRepository getProjectRepository() { Context ctx; try { ctx = new InitialContext(); return (ProjectRepository) ctx.lookup("java:module/ProjectRepository"); } catch (NamingException ex) { Logger.getLogger(ProjectConverter.class.getName()).log(Level.SEVERE, null, ex); throw new RuntimeException(ex); } } @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { if (value == null || value.equals("0")) { return null; } try { return getProjectRepository().findById(Long.parseLong(value)); } catch (NumberFormatException e) { throw new ConverterException("Invalid value: " + value, e); } } @Override public String getAsString(FacesContext context, UIComponent component, Object object) { if (object == null) return ""; Project project = (Project) object; Long id = project.getId(); if (id != null) { return String.valueOf(id.longValue()); } else { return "0"; } } }
Java EE6で単体テストや結合テストを自動化する方法について
今週水曜日に、オラクル青山センターで行われたGlassfish Japanユーザーグループの勉強会でJava EE6のお話をさせていただきました。勉強会のスライドとビデオは以下のリンク先にあります。
http://www.ustream.tv/recorded/16552906
今回は基本的に私がこのブログで書いてきたJava EE6関連の情報について紹介させていただきました。欲張って少し内容を詰め込み過ぎたところがあったかもしれませんが、Java EE6を使った単体試験や結合試験の自動化については、説明をスキップしてしまい、ちょっとわかりにくくなってしまいました。ここで、あらためてJava EE6上のアプリケーションのテスト自動化について簡単に補足させていただきたいと思います。
Java EE6アプリケーションの単体試験を行う際のクラスパスについての注意点
基本的にJSFのアクションやEJB3.1などはすべてPOJOなので、普通にJUnitとモックフレームワークを使って単体試験を自動化することが可能です。たとえば、以前に次世代のモックフレームワークであるJMockitの基本的な使い方で紹介したJMockitを利用すると、以下のような感じでアクションクラスの単体試験が書けます。
package jsf2.demo.scrum.web.controller.scrum; import javax.faces.validator.ValidatorException; import mockit.Deencapsulation; import java.util.List; import java.util.Arrays; import jsf2.demo.scrum.domain.project.ProjectRepository; import mockit.Expectations; import jsf2.demo.scrum.application.scrum_management.ScrumManager; import mockit.Mocked; import jsf2.demo.scrum.domain.project.Project; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; public class ProjectActionTest { ProjectAction target = new ProjectAction(); @Mocked ScrumManager scrumManager; @Mocked ProjectRepository projectRepository; Project project = new Project(); @Before public void setUp() { target.scrumManager = scrumManager; target.projectRepository = projectRepository; } @Test public void getCurrentProject() { new Expectations() {{ scrumManager.getCurrentProject(); result = project; }}; Project result = target.getCurrentProject(); assertThat(result, is(project)); } @Test public void setCurrentProject() { new Expectations() {{ scrumManager.setCurrentProject(project); }}; target.setCurrentProject(project); } @Test public void getProjects() { new Expectations() {{ projectRepository.findByNamedQuery("project.getAll"); result = Arrays.asList(project); }}; List<Project> projects = target.getProjects(); assertThat(projects.size(), is(1)); assertThat(projects.contains(project), is(true)); } @Test public void showSprint() { new Expectations() {{ scrumManager.setCurrentProject(project); }}; String view = target.showSprints(project); assertThat(view, is("/sprint/show?faces-redirect=true")); } @Test public void reset() { new Expectations() {{ scrumManager.reset(); }}; target.reset(); } @Test public void checkUniqueProjectName_valid() { new Expectations(target) {{ scrumManager.getCurrentProject(); result = project; projectRepository.countOtherProjectsWithName(project, "test"); result = 0L; }}; target.checkUniqueProjectName(null, null, "test"); } @Test(expected=ValidatorException.class) public void checkUniqueProjectName_invalid() { new Expectations(target) {{ scrumManager.getCurrentProject(); result = project; projectRepository.countOtherProjectsWithName(project, "test"); result = 1L; Deencapsulation.invoke(target, "getMessageForKey", "project.form.label.name.unique"); result = "test value"; }}; target.checkUniqueProjectName(null, null, "test"); } }
ただし、Mavenを使ったプロジェクトにおいて、クラスパスの設定に注意する必要があります。通常、Java EE6のアプリケーションを開発する場合、以下の依存関係のみ追加しておけば、JavaEE 6のAPIを利用して開発が行えます。
<dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency>
しかし、この状態でJUnitを実行すると、以下のような例外となって実行できません。
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/faces/validator/ValidatorException at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141) at java.net.URLClassLoader.defineClass(URLClassLoader.java:283) at java.net.URLClassLoader.access$000(URLClassLoader.java:58) at java.net.URLClassLoader$1.run(URLClassLoader.java:197) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:307) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:248) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:247) at sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:95) at sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:107) at sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:31) at sun.reflect.annotation.AnnotationParser.parseSig(AnnotationParser.java:370) at sun.reflect.annotation.AnnotationParser.parseClassValue(AnnotationParser.java:351) at sun.reflect.annotation.AnnotationParser.parseMemberValue(AnnotationParser.java:280) at sun.reflect.annotation.AnnotationParser.parseAnnotation(AnnotationParser.java:222) at sun.reflect.annotation.AnnotationParser.parseAnnotations2(AnnotationParser.java:69) at sun.reflect.annotation.AnnotationParser.parseAnnotations(AnnotationParser.java:52) at java.lang.reflect.Method.declaredAnnotations(Method.java:693) at java.lang.reflect.Method.getDeclaredAnnotations(Method.java:686) at java.lang.reflect.AccessibleObject.getAnnotations(AccessibleObject.java:175) at org.junit.runners.model.FrameworkMethod.getAnnotations(FrameworkMethod.java:135) at org.junit.runners.model.TestClass.addToAnnotationLists(TestClass.java:50) at org.junit.runners.model.TestClass.<init>(TestClass.java:40) at org.junit.runners.ParentRunner.<init>(ParentRunner.java:65) at org.junit.runners.BlockJUnit4ClassRunner.<init>(BlockJUnit4ClassRunner.java:58) at org.junit.internal.builders.JUnit4Builder.runnerForClass(JUnit4Builder.java:13) at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:57) at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:29) at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:57) at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:24) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.<init>(JUnit4TestReference.java:33) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestClassReference.<init>(JUnit4TestClassReference.java:25) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createTest(JUnit4TestLoader.java:48) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.loadTests(JUnit4TestLoader.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:452) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
この例外が発生する原因は、javaee-web-apiの中にはAPIのインターフェースの部分のみが格納されていて、実装クラスが入っていないことが原因のようです。これを解消するには、例えばGlassfishであれば、以下のように組み込みGlassfishのjarファイルをクラスパスに追加してやるのが簡単です。
<dependency> <groupId>org.glassfish.extras</groupId> <artifactId>glassfish-embedded-web</artifactId> <version>${glassfish.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency>
これで、単体試験が実行可能になります。
結合試験の自動化について
データベースやEJBコンテナ上で実際に複数のクラスを結合した状態で試験を行う方法については、すでにJava EEサーバーが重くてテスト不能というイメージはもう過去の話かもしれない - 達人プログラマーを目指してでも紹介しています。Glassfishを使って結合試験を自動化するためには、jeeunitというライブラリーを使うと便利です。
jeeunitを使うことで、@Injectを使ってテスト対象のオブジェクトをテストクラスのフィールドにインジェクションできます。なお、バージョン0.8からは@Transactionalというアノテーションがサポートされています。実際、これが非常に便利で、このアノテーションをテストクラスのメソッドかクラスにつけることで、各テストメソッドの実行が自動的にトランザクション内で実行され、最後に自動的にロールバックされます。原則としてテストの実行後には副作用が残らないようにしないと、テストの実行順序に依存したりして、トラブルのもとになるため、ロールバックしてデータベースの状態を復元できるということは重要です。
たとえば、以下のように結合試験用のクラスを記述できます。
package jsf2.demo.scrum.domain.project; import org.junit.Before; import com.googlecode.jeeunit.Transactional; import com.googlecode.jeeunit.JeeunitRunner; import java.util.List; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import jsf2.demo.scrum.infra.util.Dates; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; @RunWith(JeeunitRunner.class) @Transactional public class ProjectRepositoryIT { @PersistenceContext EntityManager em; @Inject ProjectRepository target; Project project1; Project project2; Project project3; @Before public void setUp() { project1 = new Project("test1", Dates.create(2011, 8, 6)); project2 = new Project("test2", Dates.create(2011, 8, 7)); project3 = new Project("test3", Dates.create(2011, 8, 8)); em.persist(project1); em.persist(project2); em.persist(project3); } @Test public void crud() throws Exception { Project project = new Project("new project", Dates.create(2011, 8, 6)); target.persist(project); em.flush(); project.setName("renamed"); project.setEndDate(Dates.create(2012, 8, 6)); em.flush(); target.remove(project); em.flush(); } @Test public void getAll() throws Exception { List<Project> projectList = target.findByNamedQuery("project.getAll"); assertThat(projectList.size(), is(3)); } @Test public void countOtherProjectsWithName() throws Exception { assertThat(target.countOtherProjectsWithName(project1, "test2"), is(1L)); assertThat(target.countOtherProjectsWithName(project1, "test*"), is(0L)); } }
以上のテストクラスを実行すると、以下のようなログが得られて、正しくSQLの実行が行われていることが確認できます。
[#|2011-08-13T00:43:43.806+0900|INFO|glassfish3.1|javax.enterprise.system.core.transaction.com.sun.jts.CosTransactions|_ThreadID=84;_ThreadName=http-thread-pool-8088(1);|JTS5014: Recoverable JTS instance, serverId = [100]|#] [#|2011-08-13T00:43:44.026+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.131+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:44.156+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.186+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:44.186+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.286+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:44.286+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.386+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:44.391+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|UPDATE projects SET end_date = ?, NAME = ? WHERE (ID = ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.546+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=17;_ThreadName=http-thread-pool-8088(1);ClassName=null;MethodName=null;|DELETE FROM projects WHERE (ID = ?) bind => [1 parameter bound]|#] [#|2011-08-13T00:43:44.984+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=18;_ThreadName=http-thread-pool-8088(5);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.986+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=18;_ThreadName=http-thread-pool-8088(5);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:44.988+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=18;_ThreadName=http-thread-pool-8088(5);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.990+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=18;_ThreadName=http-thread-pool-8088(5);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:44.990+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=18;_ThreadName=http-thread-pool-8088(5);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:44.991+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=18;_ThreadName=http-thread-pool-8088(5);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:44.993+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=18;_ThreadName=http-thread-pool-8088(5);ClassName=null;MethodName=null;|SELECT ID, end_date, NAME, start_date FROM projects|#] [#|2011-08-13T00:43:45.119+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:45.122+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:45.193+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:45.196+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:45.197+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound]|#] [#|2011-08-13T00:43:45.202+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|values IDENTITY_VAL_LOCAL()|#] [#|2011-08-13T00:43:45.204+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|SELECT COUNT(ID) FROM projects WHERE (NAME = ?) bind => [1 parameter bound]|#] [#|2011-08-13T00:43:45.237+0900|FINE|glassfish3.1|org.eclipse.persistence.session.file:/C:/Users/Ryo/AppData/Local/Temp/gfembed7880109940612296692tmp/applications/jeeunit/WEB-INF/classes/_scrumtoysPU.sql|_ThreadID=19;_ThreadName=http-thread-pool-8088(3);ClassName=null;MethodName=null;|SELECT COUNT(ID) FROM projects WHERE ((NAME = ?) AND NOT ((? = ID))) bind => [2 parameters bound]|#]
なお、GitHub - ryoasai/jsf-scrumtoys-refactored: A sample web application using Java EE6 stack.にあるサンプルではMavenのmaven-failsafe-pluginを使うことで普通の単体試験と結合試験の実行を分離する設定となっています。コマンドラインから
mvn verify
とすると結合テストを含めて全テストが実行され、
mvn test
とすると単体試験のみが実行されます。JavaEEに限らず、結合テストの実行はどうしても時間がかかるため、このようにテストの実行を分離しておくと便利です。
Integration tests with Maven (Part 1): Failsafe Plugin
Integration Tests with Maven (Part 2): Test Coverage Reports
Integration Tests with Maven (Part 3): Case study - Flexmojos
(追記)
jsf-scrumtoys-refactoredの実行手順に関してid:megascusさんに説明を書いていただきました。どうもありがとうございます。
jsf-scrumtoys-refactored を動かすまで - 水まんじゅう2
なお、このアプリケーションはmavenのプロジェクトになっており、構成がIDEに依存しないためEclipseでも開くことが可能です。ただし、Eclipseで実行する場合は別途Glassfishのプラグインを追加してください。
Eclipse3.6 GlassFishプラグインの導入 - Diary of absj31
なお、本文中で単体試験、結合試験という用語を用いていますが、このような用語については定義が大切ですね。ここでは、それぞれ一つ一つのクラスに対してロジックを確認するテストを単体試験、複数のクラスを結合してコンテナ上で動かすテストを結合テストと呼んでいます。
もちろん、画面まで結合して各機能を確認するようなテストや他のシステムまで結合して行うようなテストも別途行う必要がありますが、ここではスコープ外としています。
JPAを使ったデータアクセスでポイントとなる永続コンテキストについて
先週書いたエントリJava EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。 - 達人プログラマーを目指してで、Java EE6の標準仕様を使うだけで、かなりシンプルにデータのCRUD処理を行うアプリケーションが作成できることを紹介しました。ただし、前回は全体のアプリケーションを紹介しただけなので、細かい仕掛けについては解説しきれませんでした。今回は、前回に引き続き特にJPAを使ったデータベースアクセスの部分がどうなっているのかをもう少し掘り下げて解説してみたいと思います。
なお、この場で宣伝ですが、8月10日(水)にGlassfishユーザーグループの勉強会にてお話をさせていただくことになりました。
GlassFish Japan Users Group 勉強会 2011 Summer : ATND
私はJava EE6を使った開発について説明させていただく予定ですが、JavaEE開発や最近の軽量なアプリケーションサーバーについて興味のある方は是非ご参加ください。
従来のDAOを使ったデータアクセスとJPAとの根本的な違いを理解する
以前に、O/Rマッピングで緩和されるインピーダンスミスマッチには静的と動的の側面がある - 達人プログラマーを目指してでO/Rマッピングフレームワークの分類について説明しました。この分類の中では、JPAは完全O/Rマッピングというものに属します。
しかし、現状日本においてはJPAは普及しておらず、多くの場合はSQL中心か、静的O/Rマッピングに分類されるフレームワーク(ここではDAOフレームワークと呼ぶことにします。)を使ってデータアクセスすることが一般的です。このようなDAOフレームワークを使ってデータアクセスを行う場合、基本的にはデータベースのSELECT、INSERT、UPDATE、DELETEの呼び出しをカプセル化するAPIが提供されます。言い換えれば、こうしたDAOフレームワークが行ってくれるのは個々のDML文の呼び出しを簡易化し、場合によってはオブジェクトに値を詰め替えてくれるというところまでです。したがって、たとえば、アプリケーションがデータの更新を行いたい場合は、どこかで必ず明示的にUPDATE文の実行を行うDAOのメソッドを呼び出す必要があります。
一方、JPAの場合アプリケーションから明示的にデータベースのDML文の発行をコントロールすることはしません。その代り、JPAには永続コンテキストというエンティティの状態を管理するメモリ上の入れ物が存在しています。JPAの動作を正しく理解するためには、まず、この永続コンテキストというものを理解することが大切です。
以下の図に示すように、概念的には永続コンテキストはエンティティのIDをキーとして個々のエンティティのインスタンスが保持されていると考えることができます。このように永続コンテキスト中で管理された(Managed)状態のエンティティ(図では赤い色で示してあります。)に対しては、エンティティに対する状態変更や削除指令が自動的記録されます。そして、トランザクションのコミット前など、適切なタイミングで自動的にデータベースに対してDML文が発行されることでデータの同期が行われます。
ですから、JPAの場合は明示的にCRUDに相当する指令を行うAPIを呼び出すのではなく、エンティティと永続コンテキストとの関連付けをコントロールするAPIが提供されているのですが、このAPIは大部分EntityManagerというインターフェースとして提供されています。EntityManagerの持つ代表的なメソッドの意味は次の通りです。
メソッド | 意味 | よくある誤解 |
---|---|---|
persist() | newされた直後のエンティティを新たに永続コンテキスト中で管理された状態にする。 | DBに対してINSERT文を直ちに発効する指令ではない。ただし、コミットまでに結果的にINSERTになる。 |
find() | 永続コンテキスト中のエンティティをIDで取得する。エンティティが永続コンテキスト中になければ、SELECTされ、その後エンティティは管理された状態になる。 | DBに対してSELECT文を単に発効する指令ではない。 |
remove() | 永続コンテキスト中のエンティティを削除予定としてマークする。 | DBに対して直ちにDELETE文を単に発効する指令ではない。ただし、コミットまでに結果的にDELETEになる。 |
merge() | Detachedな状態のエンティティを永続コンテキスト中のエンティティの状態に設定する。 | DBに対して直ちにUPDATE文を単に発効する指令ではない。ただし、コミットまでに結果的にUPDATEになる場合がある。 |
このAPIの意味を理解するには、まず、エンティティには以下の4つの状態があるという点を知っておく必要があります。
- New(新規)状態 → 単にエンティティのインスタンスをnewしただけの状態。永続コンテキスト中で管理されていないため、状態を変更してもDBには一切反映されない。
- Managed(管理された)状態 → 永続コンテキスト中で状態が管理された状態。適切なタイミングでDML文の犯行によりDBと同期がとられる。
- Removed(削除された)状態 → 永続コンテキスト中で削除が予約された状態。
- Detached(切り離された)状態 → 一旦Managedな状態にあったエンティティが永続コンテキストから切り離された状態。永続コンテキストがclose()により終了したり、clear()により明示的にコンテキストから除外されたり*1した場合にこの状態となる。
そして、EntityManagerの各メソッドを呼び出すことで、このエンティティの状態がどのように遷移するのかを理解する必要があります。
ScrumToy(リファクタリング版)のCRUD動作を理解する
以上の点が分かれば、先週紹介したScrumToyのCRUD動作がどのように実現されているのかを理解することができます。
エンティティのINSERSTがいつ行われるか
まず、わかりやすい例として、関連のもっともルートになっているProjectエンティティを新規に作成するケースを考えてみます。この場合、ScrumManagerImplクラスの以下のメソッド中で、新規作成処理が実行されています。
@TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void saveCurrentProject() { assertThatEntityIsNotNull(currentProject); if (!currentProject.isNew()) return; projectRepository.persist(currentProject); }
ここで、projectRespositoryのpersistメソッドは実際にはEntityManagerのpersist()を呼び出しているだけです。Projectを新規に作成した場合、主キーはまだ割り当てられていないため、以下のisNew()の判定がtrueとなってpersist()の呼び出しが実行されます。
public boolean isNew() { return getId() == null; }
この場合、currentProjectにはJSFの画面から入力されたデータが自動的に格納された状態になっています。したがって、
- persist()の呼び出しでNew状態からManaged状態に遷移
- saveCurrentProject()の呼び出しがトランザクション境界となっているため、このメソッド終了後に自動的にINSERTが発行される。
という動作になります。実際、INSERTが発行されたことを以下のログで確認できます。
詳細レベル (低): INSERT INTO projects (end_date, NAME, start_date) VALUES (?, ?, ?) bind => [3 parameters bound] 詳細レベル (低): values IDENTITY_VAL_LOCAL()
これはpersist()とINSERT文の呼び出しが対応しているため、わかりやすいのですが、両者が常に対応していると誤解を招きやすいところでもあります。今度はProjectに含まれるSprintを新規に挿入するメソッドを見ています。
@TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void saveCurrentSprint() { assertThatEntityIsNotNull(currentProject); assertThatEntityIsNotNull(currentSprint); if (!currentSprint.isNew()) return; currentProject.addSprint(currentSprint); }
ここで、ProjectエンティティのaddSprint()メソッドは、以下のように単にProjectとSprintの関連付けを行っているだけで、データベースアクセスのことは一切関係ない点に注意してください。
public boolean addSprint(Sprint sprint) { if (sprint != null && !sprints.contains(sprint)) { sprints.add(sprint); sprint.setProject(this); return true; } return false; }
しかし、この場合もログを確認すると結果としてSprintに対してINSERTが発行され永続化されていることがわかります。
詳細レベル (低): INSERT INTO sprints (daily_meeting_time, end_date, gained_story_points, GOALS, iteration_scope, NAME, start_date, project_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) bind => [8 parameters bound] 詳細レベル (低): values IDENTITY_VAL_LOCAL()
この場合にINSERTが発行されるのは次のような理屈になっています。
- 最初からcurrentProjectはManagedな状態になっている。
- ManagedなcurrentProjectに対してNew状態のSprintが関連付けされる。
- ProjectとSprintとの間の関連はCASCADE指定されているため、自動的にSprintがManagedな状態になる。
- トランザクションコミット時に新規にManagedな状態になったSprintのインスタンスが永続化され、INSERTが発行される。
JPAの永続コンテキストの動作を理解していないと魔法のようなのですが、Managedな状態のエンティティは自動的にDBと同期がとられるという点に注意してください。
実は何もしていなくてもUPDATEが行われている
以上を理解した上で、次にUPDATEが行われるケースについて見てみます。どのエンティティでも同じなので再度Projectエンティティを保存する以下のメソッドを見てください。
@TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void saveCurrentProject() { assertThatEntityIsNotNull(currentProject); if (!currentProject.isNew()) return; projectRepository.persist(currentProject); }
勘の良い方はお気づきかと思いますが、編集画面経由でエンティティを更新する場合にはIDが既に割り当てられており、isNew()の判定がfalseとなるため、このメソッドでは何も行わずに終了してしまいます。ただし、このメソッドがトランザクション内で実行されているということがポイントです。このため、もしcurrentProjectの状態がDBから読み込んだ時点と変更があると判断された場合には自動的にUPDATEが発行され同期がとられます。
詳細レベル (低): UPDATE projects SET NAME = ? WHERE (ID = ?) bind => [2 parameters bound]
なお、ここでは編集画面で名前フィールドのみ変更を加えたため、NAMEフィールドのみが更新の対象となっています。*2
DELETEの動きについて
DELETEの実行の仕方もINSERTの場合と同様にルートのエンティティであるProjectとその他のエンティティでは少し異なります。Projectの場合は、削除ボタンクリック時に以下のメソッドを実行することで、remove()メソッドが呼び出されることで、エンティティがManaged状態からRemoved状態となり、DELETEが行われます。
@TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void removeProject(Project project) { projectRepository.remove(projectRepository.findById(project.getId())); }
なお、Projectに含まれる各エンティティとの関連はすべてCASCADE指定されているため、Projectを削除すると連動してそれに紐づくSprint、Story、Taskはすべて自動的に削除されます。こうした動作を普通のDAOで実装するのはかなり骨の折れる仕事ですが、JPAの場合は非常に簡単に処理できるのです。
詳細レベル (低): DELETE FROM tasks WHERE (ID = ?) bind => [1 parameter bound] 詳細レベル (低): DELETE FROM tasks WHERE (ID = ?) bind => [1 parameter bound] 詳細レベル (低): DELETE FROM tasks WHERE (story_id = ?) bind => [1 parameter bound] 詳細レベル (低): DELETE FROM stories WHERE (ID = ?) bind => [1 parameter bound] 詳細レベル (低): DELETE FROM stories WHERE (ID = ?) bind => [1 parameter bound] 詳細レベル (低): DELETE FROM sprints WHERE (ID = ?) bind => [1 parameter bound] 詳細レベル (低): DELETE FROM projects WHERE (ID = ?) bind => [1 parameter bound]
一方、入れ子のエンティティを削除する場合はJPA2の新機能であるorphanRemovalの機能を使うことで単に親のコレクションから削除するだけで、関連付けが削除されると同時に子供エンティティ自身もDELETEされます。
@TransactionAttribute(TransactionAttributeType.REQUIRED) @Override public void removeTask(Task task) { assertThatEntityIsNotNull(currentStory); currentStory.removeTask(task); }
たとえば、StoryとTaskの関連は以下のようにアノテーションが指定されています。
@OneToMany(mappedBy = "story", cascade = CascadeType.ALL, orphanRemoval = true) private List<Task> tasks = new ArrayList<Task>();
orphanRemoval = trueの指定に注意してください。(これがfalseの場合は単に関連付けのみが削除されTask自身はremoveされません。)
2種類の永続コンテキストの持続期間について
ここまでの説明で、
- エンティティが永続コンテキストの中で管理される
- Managedなエンティティは何もしなくてもコミット前などに同期がとられる
- JPAのEntityManagerは直接DML文の発行を制御するのではなくエンティティと永続コンテキストとの関連付けを制御する
という点をご理解いただけたと思います。そうすると、永続コンテキストのインスタンスはいつ生成されていつまで保持されるかということが疑問として浮かびます。
コンテナによって管理される永続コンテキストには以下の2種類があることを次に説明します。
- TRANSACTION永続コンテキスト トランザクションごとに別々の永続コンテキストが生成される。トランザクション終了時に自動的にclose()される。
- EXTENDED永続コンテキスト ステートフルセッションBeanのインスタンスとともに生成される。ステートフルセッションBeanが破棄されるまでずっと保持される。
前者は@PersistenceContextあるいは、@PersistenceContext(type = PersistenceContextType.TRANSACTION)という指定によりEntityManagerがインジェクションされた場合に使われます。これがデフォルトであり、任意のEJBに対してインジェクションすることができます。一方、後者は@PersistenceContext(type = PersistenceContextType.EXTENDED)指定されたフィールドを持つステートフルBeanでのみ利用することができます。
両者の動作を図示すると以下のようになります。
まず、TRANSACTIONスコープの永続コンテキストを利用する場合は以下のような動作となります。
一方、EXTENDEDな永続コンテキストを利用する場合は以下のような動作となります。
両者を比べてみると、直感的には後者の方が状態管理が明らかにシンプルなことがわかります。データベースのトランザクションの持続期間によらずにエンティティを編集する会話スコープの期間ずっと永続コンテキストが保持されていれば、先にみたように単にエンティティ自身の状態をビジネスロジック中で操作してやるだけで、透過的にデータアクセスを行うことができます。一方、TRANSACTIONスコープの永続コンテキストの場合、個々のトランザクション終了時にコンテキストが終了してしまします。このため、もともとManagedな状態にあったエンティティは自動的にDetachedな状態のエンティティになってしまいます。この状態になると、文字通り永続コンテキストと切り離された状態にあるため、
- 状態を変更してもそのままではDBに反映されない。
- Lazyな関連により初期化されていない関連にアクセスすると例外となる。
といった状態となります。そして、たとえばフォームを入力後DBの更新に反映させるためには明示的にmerge()を呼び出すことで再度別の永続コンテキストと関連づける必要があります。
このようにEXTENDEDには本来優れた特性があるのですが、Java EE6以前はほとんど活用されることがありませんでした*3。それは、ステートフルBeanの扱いがきわめて面倒だったということが第一の理由として挙げられます。
しかし、Java EE6でCDIの会話スコープと組み合わせることでステートフルBeanをかなり簡単に利用することができるようになりました。今回紹介したScrumToyのリファクタリング版では、ScrumManagerImplというステートフルBeanを利用しており、この中でEXTENDED永続コンテキストを利用しています。*4
@Stateful @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) @ConversationScoped public class ScrumManagerImpl implements ScrumManager, Serializable { ... @PersistenceContext(type= PersistenceContextType.EXTENDED) protected EntityManager em;
そして、このステートフルBeanをプレゼンテーション層のアクションBeanに対してインジェクションすることで利用しています。
@Model public class DashboardAction extends AbstractAction implements Serializable { private static final long serialVersionUID = 1L; //========================================================================= // Fields. //========================================================================= @Inject TaskAction taskAction; @Inject StoryAction storyAction; @Inject ScrumManager scrumManager;
ここで見逃してはならないポイントは
- CDIではスコープの異なるBean同士をインジェクションできる。
- ステートフルBeanを会話スコープで動作させることでスコープ終了時に自動的に解放される
といったことです。こうした動作はCDIとEJBを組み合わせることで初めて可能になったことであり、Java EE6で初めてEXTENDED永続コンテキストを活用する道が開かれたと言えるのではないでしょうか。
EXTENDED永続コンテキストの伝搬に対する制約
このように、Java EE6でかなり実用的に使えるようになったEXTENDED永続コンテキストですが、現時点では以下の制約に注意する必要があります。
- @PersistenceContext(type = PersistenceContextType.EXTENDED)の指定ができるのはステートフルBeanのみ。
- 別々のステートフルBean間では異なるEXTENDED永続コンテキストが作成されて維持されるためコンテキストを共有できない。*5
- EXTENDED永続コンテキストを生成するステートフルBeanの呼び出しがトランザクションの境界となる必要がある。(すでに別の永続コンテキストがトランザクション中に存在するとエラーとなる。)
- 一つのトランザクションに関連付けされるEXTENDED永続コンテキストは一つだけ(複数のコンテキストが存在すると例外となる。)
一方で、TRANSACTIONスコープの永続コンテキストは非常に簡単でトランザクションごとに生成され、EJBの種類によらず任意のBeanに対して共通のインスタンスがインジェクションされるため、トランザクション内では簡単に共有できます。この違いを図示すると以下のようになります。
このような制約からScrumToyの例では同一の会話スコープに参加するステートフルBeanを分割せず、ScrumManagerImplという一つのクラスに集約しています。EXTENDED永続コンテキストを利用する場合はこのようにステートフルBeanをファサードとして、会話スコープ単位にある程度大きく分割するのがコツのようです。*6
Seam3では以上の制約を克服するために、EJBコンテナの管理する永続コンテキストを利用する代わりにSeam独自で管理する永続コンテキストを生成して、複数のPojo(EJBでなくてもよい)から共有可能にする仕掛けも提供されています。
http://seamframework.org/Seam3/PersistenceModule
*1:JPA2からエンティティのインスタンスを個別に指定して永続コンテキストからclear()するメソッドが追加されています。
*2:この動作はJPAのプロバイダによって異なり、JBossに標準で入っているHibernateの場合は実際の更新の有無によらずデフォルトでは全カラムが更新の対象となります。
*3:Seamを使った場合を除く。ただし、EXTENDED永続コンテキストはステートフルBeanを使う必要がるなどの制約があるため、Seamでは別途Seam管理の永続コンテキストを作成する機能も持っている。
*4:実はインジェクションしているemフィールドは直接は利用していないのですが、直接呼び出さなくても永続コンテキストが自動的にトランザクションと紐づけられて裏で同期がとられます。
*5:あるステートフルBeanから別のステートBeanのインスタンスをルックアップや@EJBにより直接生成する場合のみ例外的に永続コンテキストが共有される。
*6:ここではEXTENDED永続コンテキストの動きを理解するために、全エンティティの更新を一つの会話スコープで行っていますが、本来の意味的には各エンティティの更新タイミングは異なるはずなので、スコープを分けた方がよいでしょう。