Posts filed under ‘GlassFish’
Java EE の戦略アップデート (2016/08/09 : JCP Executive Committee)
2016/08/09 JCP の Executive Committee のミーティング (Executive Committee Meeting Minutes for August 9 2016) がありOracle の Java EE/WebLogic 系製品の責任者 (Oracle Group Vice President ) である Anil Gaur さんから、今後の Java EE の戦略に対する説明がされたようです。
※ Anil さんは、元 Sun で Java EE/GlassFish の開発チームをまとめていた方。
要約すると
* エンタープライズのプログラミング・スタイルが変わっている。
* 旧来のアプリケーション・サーバでは、複数のアプリが同一 アプリ上 アプリケーション・サーバで動いていたが、今は単一のサービスやアプリが、それ専用のモジュール化された実行環境でクラウド上にデプロイされている、Java EE をこうした次世代のアプリ開発に対応できるようにしたい。
* 新しいプログラミング・モデルは、大規模にスケールする分散アプリを構築するためにリアクティブ・スタイルのプログラミングを取り入れ疎結合化する必要がある。
* また、HTTP/2, 設定、状態管理、Eventual Consistency(結果整合性)、マルチテナンシー、O-Auth, OpenID Connect 対応にも注目している
* 現在は、Java EE の一部の大規模ベンダーに相談しているが、今後 Java Champion や Java ユーザ・グループにも相談する予定。
以降 EC メンバーとの Q&A
[Q] Werner Keil
Java SE プラットフォームは、今後リリース・サイクルを早めることを計画していると聞いていますが、このリリース・サイクルは Java EE にも当てはまりますか?
[A] Anil
Java SE とは切り離して考えてほしい。いくつかの機能は Java SE 8 をベースにするだろうし、Java SE 9 に依存する部分もあるでしょう。
[Q] Martijn Verburg
Microprofile.io のチームとコラボレートする計画はありますか?
[A] Anil
2つの計画が一緒になる事を期待したいと思います。Oracle は Red Hat にこの件について話し合いを持ちました。しかし現時点では明確な答えはありません。
[Q] Alex Blewitt
クラウド・ベースのプロビジョニングについて質問をします。クレディ・スイスは、非常に機密性の高いデータを持っているため、”クラウドではなくオンプレミスで実行したい
[A] Anil
Java EE 8 は後方互換性を壊すのではなく、オンプレミスでも動作するでしょう。今までと同様に、専門家グループがより広いコミュニティからのフィードバックを元に、リリースに関して最終的な範囲やパッケージングを決定するでしょう。
[Q] Mark Little
私は、Red Hat が Oracle と話し合いを持った事を確認しています。そして Red Hat は Microprofile.io でプロトタイプを実装する際に Oracle と協業したいと言っていました。
その他、Steve Wallin、Bruno Souza、Matt Schuetze などから追加の質問、意見が寄せられてました。詳しくは、下記原文をご参照。
以降、該当部分の原文を抜粋
Java EE strategy
Anil Gaur, Oracle Group Vice President with responsibility for Java EE and WebLogic Server, gave a brief verbal presentation on Oracle’s Java EE strategy. He noted that enterprise programming styles are changing – more and more applications are distributed in nature and get deployed in cloud environments. Rather than traditional appservers that typically run multiple applications, apps are now distributed and deployed in the Cloud via modular runtimes dedicated to a single application or service.We would like the future of Java EE to be viable to next generation of applications. These apps are composed and deployed differently in cloud and require flexibility, reliability and scale. The platform needs a new programming model that’s geared towards reactive style programming for building large-scale distributed applications that are loosely coupled. In addition, we would like to see HTTP/2, Config, State management, Eventual Consistency, Multi-tenancy, O-Auth and OpenID Connect get included in the platform. Oracle is talking to large Java EE vendors, and will soon consult with members of the community such as Java Champions and Java User Groups. He then asked for questions.
Werner Keil said that we had heard that Java SE would transition to faster release cycles. Would this also be reflected in Java EE? Anil replied that they intended to decouple from Java SE as best they could. Some features would be based on Java SE8 while others would depend on Java SE9.Martijn Verburg asked whether there were plans to collaborate with the Microprofile.io team. Anil responded that he would like to see the two efforts come together. Oracle has spoken to RedHat about this but there is no definitive answer yet.
Alex Blewitt asked about cloud-based provisioning, noting that Credit Suissse has some very sensitive data and would therefore want to run on-premise instead of “in the cloud”. Anil responded that Java EE8 will not break backward compatibility and noted that it will be possible to run Java EE8 on-premise. As always, the Expert Group will decide the final scope and packaging of the release with input from broader community.
Mark Little confirmed that RedHat has spoken to Oracle, and said that he hoped that they could collaborate, perhaps by prototyping at Microprofile.io.
Steve Wallin said that IBM has also had discussions with Oracle. He asked whether Oracle was considering a completely new platform. Anil responded that he doesn’t want to pre-judge this; the community must decide. Steve asked what was missing from the current platform, noting that IBM has been able to deliver rapid deployment based on the existing platform. He thanked Anil for coming to speak to the EC.
Bruno Souza said that Java EE has historically been very open and participative – more so than other platforms – and consequently had been very good for the JCP. Going quiet for a year was very unhealthy and had damaged Java. He hoped that this would not happen again.
Anil said he understood. Oracle intends to work with the community and the JCP. They want to deliver something that developers will find useful and exciting.
Werner Keil asked when Java EE8 would be released. Anil said that the planned date will change, but he doesn’t yet have the details. He expects to be able to say at JavaOne.Bruno asked whether Java User Groups and the Adopt-a-JSR program could help. Anil said he would welcome such community involvement.
Matt Schuetze asked what the high-level message is. Anil said that Java EE will continue to evolve. Some features will be more revolutionary, but exactly how things will be packaged has not yet been decided.
Werner Keil noted that java.net is being decommissioned and asked where Java EE projects will be hosted. Anil agreed that they would be impacted by this change and that they are evaluating alternatives.There being no further questions, Patrick thanked Anil for coming to talk to the EC. Anil responded that he expected to reach out to many EC members in the coming weeks.
Java サーバの使用状況のアンケートのお願い
サーバ・サイド Java の開発・運用に携わっていらっしゃる方に質問がございます。現在、サーバ・サイドのJava 開発環境もしくは本番環境で、どのサーバ (Webコンテナ、アプリケーション・サーバ)をご利用いただいているか理解したいと考えております。そこで、大変恐れ入りますが下記のアンケートにご協力いただけないでしょうか。
みなさまもご興味ないでしょうか?
アンケートは終了しました。ご協力いただきまして誠にありがとうございました
複数ご回答いただいてもかまいません。個人情報は取得しませんし、この結果を売買に使うことも決してありません。利用状況について正しく把握することで、現在のサーバ・サイドのトレンドについて理解できるとともに、結果を本ブログで公開することで他の皆様にも有用な情報になるのではないかと想定しております。締め切り後に、再利用可能な結果(順位、投票数、パーセント)として公開します。
本データは、日本全体における利用状況の実データにはならないと思いますし、市場調査会社がだす結果とは異なると思いますが、それでも Java コミュニティ参加者や、本ブログをご覧いただいている皆様の近辺のデータを集めることができるのではないかと想定しております。
※ これは#てらだよしお 個人的なお願い事であり、所属企業や団体からの問い合わせではないことをご理解いただければ誠に幸いです。
たとえ、いただいた結果が 10 票以下でも結果はブログで公開いたしますが、母数(投票数)があまりにも少ない場合は信頼性が大幅にかけ、意味のないデータになってしまいますので、可能な方は恐れ入りますが、どうぞご協力いただけませんでしょうか。どうぞ宜しくお願いします。
Java EE 8 の新機能概要のご紹介
この記事は、「Java EE Advent Calendar 2014」の19日目の記事となります。
昨日は、@nagaseyasuhitoさんの「JPAでマスター/スレーブ構成のMySQLを使うぞ」でした。明日は、@kokuzawa さんのご担当となります。
本エントリでは、今年の JavaOne で発表された、Java EE 8 (JSR-366) の今後の動向についてまとめたいと思います。
本エントリの内容は、JJUG CCC 2014 Fall で発表した内容に追加情報を加えた内容になっています。SlideShare で資料をご覧頂きたい方、もしくは PDF ファイルを入手されたい方は上記スライドをご参照ください。
本エントリの記載内容は、2014年12月時点での内容ですので、下記に記載する内容は今後大幅に変更する可能性もあることをどうぞご理解の上でご覧下さい。
まず、Java EE 8 のリリースに伴い、Java EE のスペックリードは本当に必要とされる技術が何なのかを改めて考えてみました。その際、業界のトレンドを観察し必要な技術は何なのかを検討したり、また Java の技術者の皆様にアンケートをとり、実際に技術者の皆様から求められている機能はどのような内容なのかも確認しました。アンケートの結果、JSON Binding, Security, JCache, Action ベース MVC 等が上位に占められていました。
こうして検討を重ねた結果、次の Java EE 8 のテーマを決めました。Java EE 8 では3つのテーマを元に機能提供を検討しています。
● HTML5
+ JSON Binding : JSR-367
+ JSON-P : JSR-374
+ Server-Sent Events : JSR は未定(JAX-RS,Servlet, WebSocket の何れか)
+ Action Base MVC : JSR-371
+ HTTP/2 のサポート(Servlet 4.0) : JSR-369
+ WebSocket の改善
● かんたん開発
+ CDI の適用範囲の拡大 : JSR-365
+ セキュリティ・インターセプタ
+ 仕様の削減
● クラウド環境への対応
+ Java EE Management 2.0 : JSR-373
+ Java™ EE Security API 1.0 : JSR-375
最新の Web 系の技術トレンドに対応するために、HTTP/2 への対応を Servlet 4.0 内で行うほか、Server-Sent Events の標準化、新しい Action ベースの MVC 等の導入も検討しています。また Java EE 5 以降、6,7 と継続してかんたん開発のテーマに取り組んでいますが、これは Java EE 8 でも継続します。例えば CDI の適用範囲をひろげ Java SE 環境でも CDI を利用できるようにします。さらにクラウド対応という点では、今まで Java EE アプリケーションを監視する際に使用していた、管理・監視用の JMX を改め、JAX-RS による監視を可能にする他、今までアプリケーション・サーバ毎に個別設定していた認証・認可の設定・実装(レルム)を標準化し、より簡単にユーザ管理のアプリケーションを実装できるようにします。これらの各種技術概要を見るだけでも Java EE 8 が待ち遠しくなりますが、それぞれの検討内容を下記にまとめて紹介します。
HTML 5 への対応として、ここに列挙する内容が検討されています。これらを順に説明します。
JSON Binding : JSR-367
JSON Binding は JSON と Java オブジェクトをマッピングし相互変換を簡単に実現するための機能です。
今までも、XML を利用する場合には、JAX-B を利用したり、データベースのテーブルと Java オブジェクトをマッピングするために、JPA を利用してきましたが、JSON-B はこれらと類似の方法で JSON と Java オブジェクトをマッピングできるようになります。またこれを実装する事によって JPA の Entity を使用したデータベース・テーブルから JSONへの相互変換や、JAXB を使用した XML と JSON への相互変換も容易になります。
JSON-B は JSON の Java へのマッピングにおいてできるだけ実装コードを少なくするため、Configuration by Exception のルールに乗っ取り標準でデフォルトのマッピング・ルールが提供されます。またクラスの継承も可能です。このデフォルトのルールに従う場合は、Java を簡単に JSON に変換できます。そしてもちろん、デフォルトのルールをカスタマイズしたい場合には、専用のアノテーションを付加する事でデフォルト・ルールを上書きする事もできます。
また、Java EE 7 で導入された JSON-Processing との連携も容易にできます。
JSON-B の参照実装は、Eclipse link MOXy で開発が進められています。
それでは、実際に Java と JSON のバインディングについて見ていきましょう。ここでは、Employee というクラスに、インスタンス変数、id, firstName, lastName, email が定義されており、Employee クラスのインスタンス (e) を生成した後、それぞれに値を代入しています。この Java オブジェクトを JSON で扱う場合、デフォルトのルールに従うとインスタンス変数名が、JSON の KEY になり、変数に代入された値が JSON の value として代入されます。
また、JAX-B をご利用した事のある方であれば、用語の説明は不要かと想定しますが、Java から JSON への変換を Marshal (マーシャル) といい、逆に JSON から Java への復元の事を Unmarshal (アンマーシャル) といいます。
Marshal (JavaからJSONへの変換), Unmarshal (JSONからJavaへの変換) を行うために、まず JsonContext オブジェクトのインスタンスを生成します。JsonContext のインスタンスを生成するためには、デフォルトで用意されているファクトリ・メソッド JsonContext#newInstance() を利用する事ができます。また下記のように JsonContextBuilder を通じてインスタンスを生成する事もできます。
JsonContext context = new JsonContextBuilder() .setScope (MyClass.class, ...) .excludeField(“field”, MyClass.class) .addFilter(myContextFilterImplementation) .build();
生成した JsonContext のインスタンスから、それぞれ createMarshaller() で Marshaller オブジェクトを、createUnmarshaller() で Unmarshaller のオブジェクトを生成します。
一旦、Marshaller オブジェクトを生成すると、変換したいオブジェクトを marshall() メソッドの引数に渡す事で簡単に JSON を含む String オブジェクトを取得したり、Writer として取り出す事もできます。
同様に、unmarshaller() メソッドの引数に JSON を含む String 、もしくは Reader から Java オブジェクトを渡す事で簡単に Java オブジェクトに復元する事もできます。
今まで JsonContext から Marshaller や Unmarshaller を生成し各処理を行う方法を紹介しましたが、よりかんたんに直接相互変換を行うためのユーティリティ・クラス Jsonb の作成も検討中です。Jsonb のメソッドを利用すると直接、marshal, unmarshal を実行する事もできるようになります。
JSON-B ではプリミティブ型や、参照型に対するデフォルトのマッピングルールを提供します。しかし、デフォルトのルールに従わず独自にカスタマイズしたい場合には、下記のようなアノテーションを付加しカスタマイズする事もできます。
例えば、デフォルトのルールに従うと、Java クラス中のインスタンス変数名は、JSON のキーに同一の名でマッピングされます。しかし、Java クラス中のインスタンス変数名と、キー名を変更したい場合は、@JsonPropertyアノテーションを付加して変更する事ができます。
また、Java のクラス中に含まれる特定のインスタンス変数を JSON にマッピングさせたくない場合、@JsonTransient アノテーションを付加し無効化する事ができます。
さらに、JSON ドキュメント中のキーの記載順に意味があるような場合、デフォルトでは Java クラスのインスタンス変数の記載順にマッピングされますが、その順番を変更するために、@JsonPropertyOrder アノテーションを付加し引数内に順番を指定する事ができます。
JSON-B では継承やポリモーフィズムにも対応予定で、また Marshal の前後、もしくは Unmarshal の前後に処理をインターセプトして何らかの処理を付け加えて実行したい場合に、それらを実現するための API (JsonPre***, JsonPost***) も用意しています。
JSON-Processing は Java EE 7 に JSR-353 として導入されました。JSON-P は Stream を利用して JSON の解析、生成を行うための低レベル API を提供し、XML における StAX や DOM のような機能を提供しています。
Java EE 8 では Java EE 7 で提供されている機能に加え、JSON の標準仕様に準拠した最新機能 (JSON ポインタや JSON パッチ) にも対応します。
JSON ポインタは IETF RFC 6901 : JavaScript Object Notation (JSON) Pointer で定義される仕様で、JSON ドキュメント内に存在する特定の値を参照するための構文です。
たとえば、この例をご覧ください。この例では JSON 配列の中に、2つの JSON オブジェクト(名前が Duke と Jane)があります。そして人に関する情報(名前、性別、電話番号)が JSON オブジェクトとして記載されています。それではこの中から携帯電話の電話番号 (650-234-5678) を取得するにはどうすればよいでしょうか。JSON ポインタを利用すると、”/何番目/phones/mobile” といった形式で一意に値を取得する事ができるようになります。例では “0/phones/mobile” と 0 番目を指定する事で値を取得しています。
それでは、Java で JSON ポインタを扱うためにはどのようにすれば宜しいでしょうか。まず、先ほどの JSON ドキュメント(名前が Duke と Jane)が JsonArray のインスタンス contacts として表されている事とします。 この時、Json クラスのクラスメソッド createPointer() の引数に、参照したい JSON 値への参照パス(“/0/phones/mobile”)を渡し、JsonPointer のインスタンスを生成します。ポインタの getValue() を実行する事で JsonValue を取得する事ができます。またポインタに対して、replace を実行する事で、指定した参照の値を変更した結果の JsonArray を取得する事ができます(※ オリジナルを直接書き換えるわけではない)。
JsonPointer クラスには、ここで示すように様々なメソッドが用意されています。しかし、ここで1点注意をしなければならない事があります、add, replace, remove 等のメソッドは直接オリジナルの JSON ドキュメントに対する変更を行うわけではないという点です。JsonObject や JsonArray はイミュータブル(不変)のデータなので、直接それを変更するわけではなく、変更した後に新しい JsonArray や JsonObject を生成するという点に気を付けてください。オリジナルの JSON ドキュメントを編集したい場合は、JsonPointer ではなく、次に紹介する JsonPatch を利用します。
続いて、JSON パッチについて紹介します。
JSON パッチは、IETF RFC 6902 : JavaScript Object Notation (JSON) Patchで定義される仕様で、特定の JSON ドキュメントに対して行う操作(追加、変更、削除等)内容が記載された JSON のドキュメントのフォーマットです。JSON パッチのドキュメントは、op (add,replace,remove 等) と path (JSON ポインタ)が必須で、それ以外は必要に応じて追加できます。また、MIME タイプ “appliaction/json-patch+json” を指定し、HTTP の PATCH メソッドを利用できます。
※ JsonArray や JsonOBject はイミュータブル(不変)なオブジェクトなので、直接変更を加える事ができません。そこでオリジナルの JSON ドキュメントに対して、変更(追加、変更、削除など)を加えたい場合、新たに処理内容を記載した JSON ドキュメントを作成し、指定したドキュメントに対して操作を行う事ができるようになります。
下記に HTTP の PATCH メソッドの利用例をご紹介します。
PATCH /my/data HTTP/1.1 Host: example.org Content-Length: 326 Content-Type: application/json-patch+json If-Match: “abc123” [ |

それでは、実際に JSON パッチのドキュメント記載方法と利用方法について説明します。まず、左側に示す、”mobile”:”650-234-5678″ の電話番号を (“650-111-2222”) に変更する場合を考えてみましょう。変更するために処理内容を記載した、JSON パッチのドキュメントを生成しますが、先ほど説明したように、op と path は必須です。ここでは既存の値を変更したいので、op には “replace” を指定します。また変更対象の値は、JSON ポインタで “/0/phones/mobile” と表す事ができますので、path にこの値を記載します。最後に value に変更後の値を指定する事で、電話番号を変更するための JSON パッチのドキュメントができあがりました。
また次に既存の JSON ドキュメントから要素を削除する例を確認してみましょう。この例では、0 番目を全部削除する場合を考えます。削除するために、JSON パッチのドキュメントを生成しますが、先ほどと同様 op と path は必須で、それぞれ remove と JSON ポインタで対象を指定します。
さて、基本的な JSON パッチの概念が理解できましたので、Java でどのようにしてパッチを扱うかについてご紹介します。オリジナルの JSON のドキュメントが JsonArray の target に代入されている事とします。また JSON パッチの記載内容が JsonArray の patch に代入されている事とします。Json クラスのクラス・メソッド createPatch(patch) を実行し、JsonPatch のオブジェクトを取得します。そして JsonPatch の apply メソッドをターゲットに対して実行します。このようにとても簡単にパッチを適用できます。
最後に、JSON クエリに対する Lambda & Stream API の適用についてご紹介します。JSON-P で提供される API の javax.json.JsonObject, javax.json.JsonArray はそれぞれ、java.util.Map, java.util.List インタフェースを実装したクラスです。そして、Map や List は Java SE 8 で導入された Lambda 式や Stream API を利用してバルク(一括)操作を行う事ができるようになります。具体的には、JSON ドキュメント中のデータのフィルタリングや変更(マッピング)などのバルク(一括)処理ができるようになります。下記に Java SE 8 の Lambda 式や Stream API を使用して JSON データに含まれるデータの抽出を行う例を紹介します。
Stream を取得するために、JsonObject, JsonArray で getValuesAs(JsonObject.class).stream() を実行します。一旦 Stream のインスタンスを取得した後は、java.util.stream.Stream で提供される filter や map といった中間操作用のメソッド、collect といった終端操作用のメソッドを、通常の Stream API と同様実行する事ができます。この例では、filter メソッド内で、性別が女性の方をフィルタリングし、 JsonObject として抽出しています、次に map メソッドで、抽出した JsonObject 中から、女性の名前を getString(“name”) で取得し、結果として String のストリームを返しています。最後に collect メソッド内で、結果を List として返しています。つまり List<String> femaleNames には女性の名前の一覧が代入されてます。
Lambda 式と Stream API を利用して JSON ドキュメントのデータの抽出等がかんたんにできる事が分かりました。しかし、Stream API の終端操作 Collectors で標準に提供する機能では、配列やリストなどへの変換しかできません。Stream API を利用して任意の中間操作を行った後、最終的に終端操作で直接、JsonObject や JsonArray のオブジェクトを取得できるとさらに有用です。そこで、Java EE 8 では java.util.stream.Collectors クラスを拡張した JsonCollectors を新たに提供する予定です。JsonCollectors では最終的に JsonObject や JsonArray を取得するためのメソッドを追加したり、groupingBy メソッドで結果のグルーピングをおこなうためのメソッドも提供する予定です。
具体的に JsonCollectors の利用例を紹介します。先ほどの例では collect で List を返していましたが、その代わりに collect で JsonCorrectors.toJsonArray() を指定する事で、結果 JsonArray オブジェクトとして結果を取得する事ができます。
最後に、今までご紹介してきた、JSON ポインタ、JSON パッチ、Lambda 式 & Stream API を使用して、オリジナルの JSON ドキュメントに対して一気に変更を加える例を紹介します。ここでは、電話番号のエリアコード(地域識別番号)で 415 を含むデータのみを抽出し、そのエリアコードを 415 から 650 に一括変更するための処理を記載します。まず、Stream を取得した後、filter メソッドで電話番号のエリアコードが 415 を含む JSONObject の Stream を取得します。次に、map メソッドで エリアコードの部分を 415 から 650 に変換するための JSON パッチを動的に生成し、生成した JsonObject の Stream を返します。最後に collect メソッドで、条件に一致する全ての変更用のパッチを含む JsonArray を生成し返します。パッチ用の JSON ドキュメント生成後、オリジナルの JSON ドキュメント(contacts) に対して、Json.createPatch(patch).apply(contacts) でパッチを適用する事で結果を取得しています。
このように、Lambda 式 & Stream API を利用する事で、より簡単にパッチの適用ができるようになります。
JSON-B に関する詳細は下記 JavaOne の発表資料「Java API for JSON Binding
Introduction and update」もご参照ください。
ここでご紹介した API に関して、ご意見、ご要望がある場合は、メーリングリスト、JIRA などでお問い合わせください。
https://java.net/projects/jsonb-spec/
https://java.net/jira/browse/JSONB_SPEC/
続いて、Server-Sent Events (以降 SSE) について説明します。
歴史を少し振り返ると、サーバ側で発生するイベントをリアルタイムに通知するための仕組みは、今まで Ajax を使った Polling, Comet(Long Polling,Streaming), WebSocket が利用できました。Polling や Comet は、パフォーマンスが非常に悪く、大量のリクエストを捌くサービスを提供する事が困難でした。WebSocket はハイ・スケーラブルで双方向通信を実現できますが、専用のプロトコルを利用するため、これをサポートするクライアント、サーバ、さらに HTTP プロキシ等が必要でした。一方で SSE は純粋な HTTP を使用し、サーバ側からクライアントに対する一方向へのメッセージ通知としては利用できます。双方向通信ではなく、サーバからの一方向通信でよい場合、SSE は有用です。
SSE は、現在 W3C で仕様がドラフトとしてまとめられています。
http://www.w3.org/TR/eventsource/
SSE は、クライアントからサーバに接続しコネクションが確立した後は、サーバからクライアントに対して一方向でデータを送信します。この際、サーバ側で発生するイベントに応じて、何度も同一コネクションを利用してデータ転送する事ができます。MIME タイプとして専用のメディア・タイプ “text/event-stream” を利用します。SSE はクライアントがサーバに対してサブスクライブ(購読)要求をだして、サーバがサブスクライブしているクライアントに対してパブリッシュ(配信)するという点で JMS のパブリッシュ・サブスクライブのモデルに似ています。
現在、Java EE 上で SSE の実装を検討中ですが、どのレイヤーで実装するかを今まさに議論中です。具体的には Servlet 上、WebSocket 上、JAX-RS 上、もしくは独自のコンテナ上で実装する案があがっており、これらを検討中です。
検討案の説明資料:
上記をエキスパート・グループメンバーに問い合わせ中ですが、現在、スペックリードとしては JAX-RS が一番適切ではないかと考えています。なぜならば、JAX-RS で既に HTTP のリソースに対するストリーミングをサポートしており小さな変更ですむためです。
● 小さな変更で実現
例えば、JAX-RS でサーバ、クライアントにそれぞれ下記の API を追加します
サーバの API として : EventOutput に対し新しいメディアタイプを追加
クライアントの API として:Server側のイベントに対する新しいハンドラを追加
● 他のHTTPの操作と組み合わせる際に便利:新しいメディアタイプ
● Jersey(JAX-RS RI)は既に SSE をサポート済み
Jersey を利用した実装例はコチラ
JavaOne で発表された、次の JAX-RS で組み込む機能紹介の概要は下記よりご参照ください。
Let’s Talk JAX-RS.next! JSR-370 – What to expect in JAX-RS in Java EE 8
また、JAX-RS の参照実装 Jersey で既に実現している SSE の実装方法については下記をご参照ください。
Chapter 14. Server-Sent Events (SSE) Support
オリジナルの資料では Java EE 7 から導入された Concurrency Utilities for EE を使って説明されていなかったため、Concurrency Utilities for EE を使って書き直したコードを下記に記載します。ここでは、ManagedExecutorService を @Resource アノテーションでインジェクトした後、別スレッドで SSE のイベント通知を行っています。別スレッドの処理内容の例は、StockThread に記載しています。実際に SSE でメッセージ通知は EventOutput#send()メソッドで行っています。
また、SSE ではクライアント用の API も用意しています。これにより、JavaFX 等のスタンドアローン・アプリケーションでも SSE クライアントを実装し、SSE のサーバ・サイドからの通知を受信して何らかの別処理を行う事ができます。
次に、Action ベースの新しい MVC フレームワークについて紹介します。
Java EE 7 まで Web アプリケーションの開発を行う際、JavaServer Faces(以降 JSF), もしくは JAX-RS を利用できました。この中で JSF はコンポーネント・ベースの MVC フレームワークとして Java EE 5 以降標準の Web 開発フレームワークとして導入されています。一方、アクション・ベースの MVC フレームワークも世の中には多く存在します。例えば Struts や SpringMVC 等がこれに当てはまります。
Java EE の開発者にアンケートを実施した所「アクション・ベースの MVC は改めて必要でしょうか?」という質問に対し、約 6 割の方々が ハイと答えました。またその際、アクション・ベースのMVC を実装するために、「参考にすべき技術(デファクト・スタンダードな技術)はありますか?」と質問した所、75% の開発者は分からない、もしくは存在しないと答えました。
当初、JAX-RS のエキスパート・グループ・メンバーを中心に新しいアクション・ベースの MVC について議論を行いましたが、様々な議論を行った結果、JAX-RS とは別途、検討・実装した方がよいという結論になり、グループを再編し、スタンドアローンのAPI として、新しく JSR 371: Model-View-Controller (MVC 1.0) Specification として仕様策定を進めています。
※ 現在まさに仕様を検討中ですので、下記に紹介する内容は今後大きく変更する可能性があります。その点をどうぞご注意ください。
まず、MVC の各構成にどのような技術を適用するかを紹介します。アンケートの結果、参考にすべきアクション・ベース MVC が無い、もしくは分からないというご意見が多数あったため、既存の資産を有効活用するため Java EE で現在提供されている機能を組み合わせて、それらをつなぎ合わせて実装できるような MVC フレームワークを検討中です。Movel には CDI, Bean Validation, JPA 等を View は Facelets, JSP, 任意のテンプレート・エンジン(プラグイン可能なテンプレートエンジンが入れられると望ましい)等が予定されています。
一方で、現時点でまだ Controller 部分の実装が決まっていません。スペックリードが現在(10/29 から)この新しい MVC フレームワークのコントローラ部分をどのような環境で提供すべきかアンケートも実施しています。
https://java.net/projects/mvc-spec/lists/users/archive/2014-10/message/68
(A) Layer MVC on top of the Servlet API: define new set of annotations, new URI mapping algorithm and new data binding layer for controllers. This option implies no MVC + REST controllers, at least not in a elegant manner (see example on e-mail thread).
(B) Layer MVC on top of the JAX-RS API: reuse existing set of JAX-RS annotations, URI mapping algorithm and data binding layer for controllers. This option implies that MVC + REST controllers will be possible, but also that our runtime will depend on the JAX-RS runtime.
メーリングリストでの回答の多くは、(A) の Servlet API 上に Controller 実装を希望している方が多く見られるようです。一方、Oracle (Santiago) としては (B) に投票予定のようです。
(3) Oracle’s Vote: We have been busy writing some prototypes and evaluating
MVC frameworks for the last few weeks. We are not taking this decision
lightly as it obviously has a great impact on future work. Our main concern
has always been familiarity of existing APIs and duplication of work,
especially around matching (routing) and binding. These are the list of
annotations that JAX-RS defines for this:
* Matching: ApplicationPath, Consumes, Produces, GET, PUT, POST, DELETE,
HEAD, OPTIONS, Path, HttpMethod
* Binding: BeanParam, CookieParam, DefaultValue, Encoded, FormParam,
HeaderParam, MatrixParam, PathParam, QueryParam, Cookie, Form
And this is just annotations, no matching and binding semantics, etc.
This fact, together with the existence of several MVC frameworks built on top
of JAX-RS, has led us to the conclusion that JAX-RS is right technology for
MVC to be layered on. Thus, Oracle votes for option (B).
下記に JavaOne 発表時のサンプル・コードを示しますが、上記の投票結果や今後の進捗によって今後大きく変更される可能性があります。
上記では、JSP や Facelets で <form action=”/rough-example/form1a.jsp”> を記載し、ページ内に EL 式を用いてデータ・バインディングができるようになっています。またサーバ側では、クライアントのリクエストに対応する処理を @Path を付加したメソッド内で記述し、メソッドの戻り値として画面遷移先を返しています。
Java EE におけるアクション・ベースの MVC フレームワークのプロジェクトはまだ始まったばかりです。ご興味のある方は是非、java.net に存在するプロジェクトやメーリング・リストにご参加ください。
JSR 371: Model-View-Controller (MVC 1.0) Specification
The MVC specification project : java.net
HTML 5 関連で最後に、HTTP/2 対応についてご紹介します。
HTTP/2 は Internet Engineering Task Force(IETF)の Hypertext Transfer Protocol Bis (HTTPbis)のワーキング・グループで標準化が進められている、次バージョンの HTTP プロトコルです(現在はまだドラフト)。HTTP/2 の特徴として下記のような物があげられます。
* レイテンシを軽減
* Head of Line Blocking 問題の対応
* 並列処理のサポート(複数コネクションは不要)
* HTTP 1.1 の意味は保持
* HTTP 1.x との連携を定義
Java EE で HTTP/2 に対応するために、JSR 369: Java™ Servlet 4.0 Specification で対応を行います。Servlet API は、HTTP/1.x に対応していたため、単一リクエストに対し単一レスポンスを返すアーキテクチャになってました。この問題点として、例えば、同一クライアントからサーバに対して大量の HTTP リクエストを行うような場合、特定のリクエストで処理時間を多く要した場合、後続の処理が待ち状態になり全体としてパフォーマンスが低下する場合がありました。しかし HTTP/2 ではよりデータ転送の効率化をはかるために、単一コネクションで、リクエストとレスポンスを多重化できるようになります。これにより、例え一つのリクエスト処理に時間を要しても後続の処理に影響が発生しにくくなるため、より効率的なデータ転送を行う事ができるようになります。
Servlet 4.0 における変更点の詳細は下記に記載していますので、下記をご覧ください。
また、JSF 2.3 における変更点は下記ごご覧ください。
続いて、「かんたん開発」の分野における Java EE 8 の拡張ポイントについて説明します。まず、CDI の適用範囲が大幅に広がります。
CDI は Java EE 6 で導入され、Java EE 7 まで、Java EE のコンテナ、つまりアプリケーション・サーバ上で利用されてきました。実際、CDI の仕様である JSR 299 は Contexts and Dependency Injection for the Java™ EE platform として記載されていました。しかし Java EE 8 からは、JSR 365 として Contexts and Dependency Injection for Java™ 2.0 に名前を変え、CDI の適用範囲を Java EE 外にもひろげ、Java SE 環境でも利用できるようにします。これにより、EJB の組み込み可能コンテナと同様に、JUnit のテストコード等でも CDI を利用できるようになります。
Java EE 8 に含まれる CDI 2.0 では、大きな機能拡張としてここにあげる3つの機能があります。
また、上記以外にも下記のような機能拡張も予定されています。
● イベント処理の拡張
● インターセプター・デコレータの仕様への対応
● SPI の拡張
● コンテキストの拡張
● Java 8 機能への対応(タイプ・アノテーション、アノテーションのくり返し、Lambda, Stream, デフォルト・メソッド、型推論)
詳しくはWorking method for CDI 2.0をご参照ください。
まず、Java SE 環境で利用可能にするために、CDI を Java SE 環境で起動するための Bootstrap 用 API が提供されます。
CDI 2.0 first Face to face meeting feedback によると、下記のようなクラスの提供を検討しているようです(まだ議論中との事)。上記会議での議論の内容は CDI 2.0 の新機能を理解する上で重要ですのでご興味のある方はどうぞご覧ください。
public class ContainerBoot { /** * Simple boot */ static BeanManager initialize() { ... } /** * Boot with parameters */ static BeanManager intialize(Map<?,?>) { ... } void shutdown() {} }
つづいて、モジュール化について説明します。CDI は Java EE 6 における標準化以降、Java EE において非常に重要な機能になっています。そして、EJB 等で培ってきた経験を元に、EJB が持つ機能も多く CDI に取り込まれて利用できるようになってきており、この方向性は今後も引き続き継続されそうです。この中で CDI が機能を持てば持つ程、CDI 自身が大きく、重量になる事も懸念されてます。そこで、CDI 自身を引き続き継続して軽量に扱う事ができるように、CDI 自身のモジュール化を検討しています。
上記では①、②、③と示しましたが、実際にはより細かく検討されています。
A. 単純な DI の機能
B. Observer パターンを利用した CDI によるイベント管理機能
C. 対象型の発見方法の拡張
デプロイしたアプリケーションの起動時に型検査を行う
D. (A+B+C+AOP:インターセプタ、デコレータ)
E. D+コンテキスト管理
詳細は CDI 2.0 modularity proposal をご覧ください
また、EJB の MDB に変わり、CDI でも非同期メッセージを受信するための新しい API も検討中です。現在の MDB の実装は、たくさんの設定が必要で MessageListener を implements したクラスの実装も必要でした。
今回、JMS をさらに簡単に利用でき、任意の CDI に対して利用ができるようにします。また、MessageListener を implements したクラスの実装も不要で、直接メソッドに @JMSListener のアノテーションを付加し、監視する Queue を destinationLookup で指定し実装できるようになります。
また、Java EE 7 までの MDB と同等の振る舞いを実装するためには、該当の CDI に対して @Singleton アノテーションを付加する事で MDB と同等の振る舞いを実現できます、また @Transactional アノテーションを付加する事でコンテナ管理のトランザクションも正しく動作します。ここに記載したコード例のように既存の MDB の実装に比べより簡単に実装できるようになります。
Java EE 7 まで提供されてきた認可(Authorization)用のアノテーションとして、@RolesAllowed や @RunAs といったアノテーションが存在しました。これらのアノテーションは多くの利用場面に有用でした。しかしより複雑な認可処理が必要な場合、別途、認可用のプログラミングを実装するか、もしくは新しく CDI のインターセプタを実装する必要がありました。また認可用のアノテーションを実装する際に、EL 式が評価できるようになる事でより多くのユースケースにかんたんに対応できるようになります。
今回、ロールに基づく認可の他、Java EE のコンテキスト情報にもアクセスし、コンテキスト情報から認可情報(プリンシパル名、ロールチェック、認可チェック)を取得する事も可能な新しいアノテーションを、CDI のインターセプタとして実装する予定です。
詳細は、JIRA に登録されている JAVAEE_SPEC-29 : EL-Enabled Authorization Annotation をご覧ください。
また、上記以外にも、CDI との連携をより強化するために、様々な場所でクリーン・アップを行います。例えば、WebSocket の実装においても CDI のスコープを利用できるようにしていますが、このように CDI が他の仕様でも幅広く利用されている事がわかります。
WebSocket の拡張に関しては下記の Slide もご参照ください。
また、Java EE 8 では Pruning (剪定:仕様の削減) 候補として EJB 2.x のリモート、およびローカルのクライアント・ビュー(EJBObject, EJBLocalObject, EJBHome, EJBLocalHome インタフェース)があげられています。さらに CORBA : (Prune CORBA interoperability) もあげられています。理由として昨今 SOAP や REST で通信を行う事が多く CORBA の利用場面が大幅に減ってきているためです。現在これらを用いて実装している場合は、アップデートをご検討ください。
最後に、Java EE プラットフォームの近代化(クラウド環境への対応)を行うための機能を紹介します。
まず、Java EE Management & Deployment API について説明します。
JSR 77: J2EE™ Management という JSR において、Java EE のプラットフォームで提供される管理オブジェクトを定義していました。これらの管理オブジェクトの各インスタンスは、構造化された OBJECT_ID で識別され、管理オブジェクトは追加機能を提供するために、コンテナ側で下記インタフェースを実装する事もできました。そして、これらの管理オブジェクトは、JConsole 等のツールを利用して管理、監視を行ったり、独自に管理・監視機能実装してアプリケーション全体の運用管理を行う事ができました。
● EventProvider : 設定イベントの通知 : GlassFish 実装
● StatisticsProvider:統計情報採取 : GlassFish 実装
● StateManageable:状態管理(基本的なライフサイクル) : GlassFish 実装
今回提案する仕様では、現在の実装方法に代わり、REST インタフェースで管理可能な、管理オブジェクトを定義する事を目的としています。これにより、既存の HTTP のツールやライブラリを用いてかんたんに Java EE アプリケーションの管理ができるようになります。またこれにあわせ、エキスパート・グループ・メンバーは、既存の Management EJB の API や JMX API をオプションにするべきかどうかも検討中です。これは仕様をかんたんにするためという理由だけではなく、今回提案する REST インタフェースをより積極的に採用していただくためです。
新しい REST インタフェースは、既存で提供している OBJECT_NAME を URL に変換し、また個々の管理オブジェクトに対する CRUD 操作も可能です。EventProvider のイベント通知に Server-Sent Events もサポートする予定で(WebSocket も検討中だが、対応ツール(HTTPのアップグレードは不要)リアルタイムで監視する事ができるようになります。
また、JSR 88: Java™ EE Application Deployment という JSR において、デプロイ・ツール用のインタフェースが定義されていました。そしてこれに準拠したデプロイ・ツールから、個々のアプリケーション・サーバに対して直接アプリケーションをインストールする事ができました。JSR 88 のサポートは Java EE 7 からオプション化されています。
デプロイ用の API も上記、REST インタフェース内に取り込む予定です。このインタフェースを利用する事で、アプリケーション・サーバのインスタンスに対して、REST インタフェースを通じて直接アプリケーションをインストールする事ができるようになります。その際、依存するリソース定義や管理も REST インタフェースを通じてできるようになります。
次にセキュリティについて説明します。現在、セキュリティの設定・実装に関する多くがアプリケーション・サーバ固有設定になっており、これにより移植性が大きく損なわれていました。今回、これらサーバ独自の設定を排除し標準化する事で、クラウド環境でより柔軟に移植性の高いアプリケーションの構築が可能になります。
これらは、JSR 375: Java™ EE Security API で検討中です。
まず、パスワード・エイリアスを導入します。これまで、アプリケーション・サーバからデータベースや、LDAP 等の外部リソースへの接続するためには、サーバ側の設定でユーザ名、パスワードを設定していました。所がこのユーザ名、パスワード管理はアプリケーション・サーバ固有で、一部のアプリケーション・サーバでは生のパスワードを記載しなければならない場合もありました。データベースや LDAP への接続用のユーザ名やパスワードを生の形で見えるようにしておくのは非常に危険です。そこで、生パスワードを記入しなくてもよい標準の方法を提供します。
@DataSourceDefinition{ name=“java:app/MyDataSource”, className=“com.example.MyDataSource”, … user=“duke”, password=“${ALIAS=dukePassword}”)
上記の構文は、現在 Java EE の参照実装 GlassFish で採用されているパスワード・エイリアスの指定方法で、標準化にあたりこれをベースにしていますが、パスワードの値には、実パスワードを参照するためのエイリアス(仮の名前)が設定されており、実行環境が必要に応じてエイリアスから元の実パスワードを参照するようになっています。
ご参照:Password Aliasing for EE 7
続いてユーザ管理について説明します。Java EE 7 までユーザ管理(認証・認可)を扱うアプリケーションの実装は、各アプリケーション・サーバ・ベンダーに依存していました。つまり統一的なユーザ管理用の API も用意されていなかったため、概念は共有できても実装コードは個別に行う必要がありました。以前、GlassFish v4 で始める Java EE JDBC レルム ハンズオン・ラボ として GlassFish 上で Java EE6/7 利用者向けのハンズオン・ラボを公開しましたが、ここで記載した内容は他のコンテナではそのままでは利用できません。そこで、このようなユーザ管理を行うアプリケーションを作成する際の、標準化を行い移植性の高いアプリケーションを構築するため、ユーザ管理機能の標準化が行われます。
この仕様もまた、JSR 375: Java™ EE Security API 内で行われています。また、JIRA に登録されている JAVAEE_SPEC-9 : Simplify and standardize authentication & role mapping もご参照ください。
ユーザ管理を行うために、3つのコア・クラスを提供します。これらを順に紹介します。
● UserSourceDefinition:データ・ソース(DB, LDAP, ファイルなど)を定義
● UserService:ユーザの管理(追加、変更、削除など)機能を定義
● UserInfo:ユーザ情報の定義(ユーザ名、パスワード、有効期限など)
UserSource(DB, LDAP, ファイル、その他)のリソース中に含まれるユーザ、グループを UserService で処理し、個々のユーザは UserInfo として管理され、ユーザ名、パスワードだけでなく、有効期限や、アカウントのロック状態なども管理ができるようになります。
具体的に、LDAP からユーザ、パスワードを参照するアプリケーションを作成する例をここで紹介します。@LdapUserSourceDefinition というアノテーションを付加し、ユーザ情報が含まれる LDAP ディレクトリをしていします。またこれを利用する為には、@Resource アノテーションを付加し UserService をインジェクトしています。UserService のインスタンスを取得した後は、下記に示すメソッド(現時点での仮案)を利用してユーザ管理、グループ管理を行う事ができるようになります。
+ UserInfo:loadUserByUsername(username)
+ changePassword
+ createUser
+ deleteUser
+ updateUser
+ userExists
+ createGroup
+ addUserToGroup
+ removeUserFromGroup
+ isUserInGroup
また、UserInfo のインスタンスは下記のフィールドを持ち、ユーザ自身の管理も標準で用意にできるようになります。
+ Username
+ Password
+ AccountExpired
+ AccountLocked
+ PasswordExpired
+ Enabled
+ Attributes
続いて、ロール・マッピングについて説明します。ロール・マッピングもユーザ管理同様、アプリケーション・サーバ固有に設定・実装が必要でした。ロールマッピングの実装もまた標準化を行います。
ロール管理を行うために、2つのコア・クラスを提供します。これらを順に紹介します。
● RoleMapper:ロール情報が保存されるデータ・ソース(DB, LDAP, ファイルなど)を定義
● RoleService:ロールの管理(権限の付加、排除、権限の有無チェックなど)機能を定義
RoleMapper(DB, LDAP, ファイル、その他)のリソース中に含まれるロール情報を RoleService で処理します。
ここでは、メモリに存在するロール情報を元に、ロール管理を行うアプリケーションを作成例を紹介します。ここでは、@MemoryRoleMapperDefinition アノテーションを付加し、その中でロール情報を定義しています。ここで定義されたロール情報を元に @Resource アノテーションで RoleService をインジェクトしインスタンスを生成しています。getRolesメソッドの引数で与えられたユーザが持つ全てのロール一覧を取得するために、getRolesForUser(username) を実行しています。RoleService が提供するメソッドの一覧(現時点での仮案)は下記の通りです。
+ grantRoleToUser(username, role)
+ revokeRoleFromUser(username, role)
+ hasRoleForUser(username, role, includeGroups)
+ getRolesForUser(username, includeGroups)
+ getUsersWithRole(role, includeGroups)
+ grantRoleToGroup(group, role)
+ revokeRoleFromGroup(group, role)
+ hasRoleForGroup(group, role)
+ getRolesForGroup(group)
+ getGroupsWithRole(role)
このように、ユーザ管理用の API やロール管理用の API が標準化される事で、よりかんたんにユーザ管理アプリケーションが実装できるようになるだけでなく、移植性の高い認証・認可のアプリケーションを構築できるようになります。
Java EE 8 は 2016 年の秋頃を目処に仕様を FIX し提供する予定です。
また、これに向け Java EE 8 は JSR 366 として登録され投票の結果、満場一致で承認されました。
詳細なロードマップは上記ですが、Java EE 8 の正式リリース時には、今までと同様 GlassFish が参照実装として提供される予定です。
現時点で登録済みの Java EE 8 関連の JSR 一覧を下記に示します。下記 JSR もどうぞご参照ください。
● JSR 366: Java Platform, Enterprise Edition 8 (Java EE 8) Specification
● JSR 107: JCACHE – Java Temporary Caching API
● JSR 365: Contexts and Dependency Injection for Java™ 2.0
● JSR 367: Java™ API for JSON Binding (JSON-B)
● JSR 368: Java™ Message Service 2.1
● JSR 369: Java™ Servlet 4.0 Specification
● JSR 370: Java™ API for RESTful Web Services (JAX-RS 2.1) Specification
● JSR 371: Model-View-Controller (MVC 1.0) Specification
● JSR 372: JavaServer Faces (JSF 2.3) Specification
● JSR 373: Java™ EE Management API 2.0
● JSR 374: Java™ API for JSON Processing 1.1
● JSR 375: Java™ EE Security API
その他、メンテナンス・リリースとしてここに示す各既存 API の改善も検討されています。
Java EE 8 の今後にご興味のある方は Java EE プロジェクトにご参加頂き、メーリング・リスト等でフィードバックをください。
Adopt-A JSR プロジェクトを通じて、日本 Java ユーザ・グループの一員としてフィードバック等をおこなってください。
最後に、Java EE 7 が日本で本格的に導入されるのは 2015 年からですが、Java EE 7 のプロジェクトを開始するにあたり、その先の Java EE 8 の変更点等を理解し意識した上でプロジェクトを進めていく事は、将来の移行、更新において非常に有用です。例えば、Pruning 予定の EJB 2.x の機能を使っているかた、Java で CORBA の利用をご検討中の方は時代の流れをいち早くつかみ、そうしたコードを排除する事がより安全に長く使っていただくための秘訣です。また認証・認可の実装コードも将来標準化される予定のクラスやメソッド・シグネチャを理解しておく事で、移行も用意になるかと想定します。Java EE 8 が市場で投入されるようになるのは、2017 年後半〜2018年辺りになる事が予想されるので随分先の内容ですが、将来の動向もみながらどうぞ今のプロジェクトをお勧めください。2016 年にリリース予定の Java EE 8 をどうぞ楽しみにしてください。
JSF + WebSocket で実装した IMAP Web メール・クライアント
このエントリは Java EE Advent Calendar 2013 の 11 日目のエントリです。昨日はsk44_ さんの 「JSF で日本語ファイル名のファイルダウンロード?」のご紹介 でした。
明日は @nagaseyasuhito さんです。
エントリを始める前に、昨日 12/10 は Java EE 6/GlassFish v3 が正式リリースされて丁度 4 年目にあたる日でした。2009 年のブログを確認すると 昨日の日本時間の夜 11 時頃からダウンロードできていたようです。
Happy Birth Day 4th Anniversary of Java EE 6 & GlassFish v3 !!
今回私は、JavaServer Faces (JSF) + WebSocket + Java Mail API を使用して、IMAP のメールクライアントを作成しました。本アプリケーションは、Ajax を使用していますが、Ajax 部分では一切 JavaScript を使用していません。JSF のデフォルトで用意されている Ajax ライブラリを使用し動的な画面更新を実現しています。また、今回実装したコードはコード量も比較的少なくある程度かんたんに動かす所までの実装で 3-4 日程度で実装できています。是非ご覧ください。
このアプリケーションのデモ動画はコチラ
今回作成した JSF のアプリケーションは並列処理 (Concurrency Utilities for Java EE)も利用しています。例えば長時間処理が必要な処理を実行しなければならない場合、バックエンドの処理をシーケンシャルに処理していては大量の時間を要してしまいます。これを並列処理 (Concurrency Utilities) を利用する事で描画までの時間を短縮する事もできます。
ここでご紹介する JSF(PrimeFaces) で実装したサンプル・アプリケーションを通じて Java EE 7 のテクノロジーを使ってどのような事ができるのか、どのようにして実装できるのかをご理解いただければ幸いです。
特に JSF (PrimeFaces)で ツリーやテーブルを扱う部分、さらには Ajax を実現する部分はご注目ください。
※ このアプリケーションの実装では INBOX を監視し新規メッセージを受信した際、WebSocket で通知を行なう部分も実装しています。しかし WebSocket の実装部分は次回エントリで記載する予定です。
今回作成したアプリの全ソースコードは下記の URL にアップしました。
https://github.com/yoshioterada/JSF-WebSocket-Mailer
本アプリケーションで使用する、Java EE 7 の技術を紹介します。
● JavaServer Faces 2.2 (PrimeFaces 4.0)
● Java Mail 1.5
● Contexts and Dependency Injection 1.1
● Concurrency Utilities 1.0
まず、本アプリケーションの完成予想イメージを示します。
本アプリケーションは、ログイン画面の「IMAP Server 名」で指定した IMAP サーバに対して、「ログイン名」、「パスワード」を入力しIMAPサーバとの認証を行い、認証に成功した場合、下記の画面が表示されます。
上記の画面は、主に3つのコンポーネントから構成されています。
● フォルダ一覧表示部(画面左部)
● フォルダ内のサブジェクト一覧表示部(画面右上部)
● メッセージ表示部(画面右下部)
フォルダ一覧表示部(画面左部)
画面左側に IMAP サーバ上で作成されているフォルダの一覧を表示しています。
フォルダに子のフォルダが存在する場合、「▶」のマークが表示され、展開する事によって子のフォルダ一覧を取得できます。「▶」を押下すると Ajax でサーバに問い合わせを行い、子のフォルダを一覧を取得します。1度子フォルダを取得した後は「▶」を押下しても Ajax 通信を行なわず、開いたり閉じたりできるようになります。
特定のフォルダを選択すると、選択したフォルダ内に存在するメッセージを Ajax で行い、取得後「フォルダ内のサブジェクト一覧表示部」と「メッセージ表示部」を更新します。
また、「受信数:」のフィールドにデフォルトで「10」と表示されています。ここで扱う数字は、画面右部の「フォルダ内のサブジェクト一覧表示部」で扱うメッセージの件数を変更できます。デフォルトでは、テーブル内で表示されているメッセージは5件です。テーブル下部に存在するボタン「2」を押下する事で次の5件を表示できるようになります。
本エントリでは詳細は説明しませんが、「リアルタイム・チェック開始」、「リアルタイム・チェック中止」ボタンを押下する事で、それぞれ WebSocket 通信の開始、中止を行なうことが、INBOX (受信箱)にメッセージが届くと WebSocket でリアルタイムに通知を受け取ることができるようになります。
フォルダ内のサブジェクト一覧表示部(画面右上部)
画面右上部では、アプリケーション起動直後はデフォルトで INBOX(受信箱)に存在するメッセージの内、最新5 件のメッセージの「サブジェクト」、「送信者アドレス」、「送信日付」、「サイズ」を取得し表示しています。
また、デフォルトでフォルダに存在する最新のメッセージが選択された状態になります。また、「サブジェクト」、「日付」、メッセージの「サイズ」に応じてソートができるようになっていますので、各項目でソートをしたい場合、各項目の上下の ↑↓ 部分を押下することでソートができます。
また、サーバ側ではデフォルトで受信数 10 件を管理していますが、1 画面中では、5 件のメッセージを表示できます。テーブル下部に存在する「2」のボタンを押下する事で次の5件を取得できます。このデフォルトの受信数を変更したい場合は、「フォルダ一覧表示部(画面左部)」の下に存在する「受信数」のフィールドの数字を変更し「適用」ボタンを押下する事で受信数を変更でき、参照可能な件数が代わります。例えば、「受信数」を 20 に変更した場合、テーブル下部に「1」、「2」、「3」、「4」のボタンが追加されます。また、テーブル内に存在する、メッセージ(サブジェクト等が表示されている)を選択すると対応するメッセージを Ajax で取得し「メッセージ表示部」に対象のメッセージを表示します。
メッセージ表示部
最後に、右画面の下部を下記に示します。デフォルトで INBOX(受信箱)に存在する最新のメッセージを表示していますが、「フォルダ内のサブジェクト一覧表示部(画面右上部)」で特定のメッセージをマウスでクリックし選択すると、対応するメッセージがここで表示されます。
本アプリケーションの実装方法の詳細
この JSF アプリケーションの主要な機能は View の実装として、JSF のFacelets を使用し「folders-show.xhtml」ファイルに画面デザインを実装しています。また、この画面のバックエンドの処理を行なったり、画面上に存在する各種コンポーネントとのバインディングを行なうために、JSF 管理対象 Bean を「MessageReceiveMgdBean.java」として実装しています。つまりこの2つのファイルを確認する事で、本アプリケーションの詳細な振る舞いを把握する事ができます。
メッセージを表示するために実装した Facelets の全ソースコードを下記に示します。ある程度、複雑な画面構成になっているにも関わらず、記載しているコード量が 88行程度と、とても少ない事をご確認いただけるのではないかと思います。
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:p="http://primefaces.org/ui" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"> <h:head> <title>JSF-WebSocket WebMail</title> <f:event type="preRenderView" listener="#{messageReceiveMgdBean.onPageLoad}"/> <h:outputScript library="javascripts" name="ws-client-endpoint.js"/> </h:head> <h:body> <h:form id="form"> <p:notificationBar position="top" effect="slide" widgetVar="bar" styleClass="top" style="background-color : #F8F8FF ; width: fit-content;"> <h:panelGrid columns="2" columnClasses="column" cellpadding="0"> <h:outputText value="新着メッセージ :" style="color: red;font-size:12px;" /> <p:commandButton value="閉じる" onclick="PF('bar').hide()" type="button" style="font-size:10px;"/> <h:outputText value="Subject :" style="font-size:10px;" /><h:outputText id="wssubject" value="" style="font-size:10px;" /> <h:outputText value="From :" style="font-size:10px;" /><h:outputText id="wsfrom" value="" style="font-size:10px;" /> <h:outputText value="メッセージ・サマリー :" style="font-size:10px;" /><h:outputText id="wssummary" value="" style="font-size:10px;" /> </h:panelGrid> </p:notificationBar> <p:layout fullPage="true"> <p:layoutUnit position="west" size="200" header="フォルダ一覧" resizable="true" closable="true" collapsible="true" style="font-size:14px;"> <p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}"> <p:ajax event="select" listener="#{messageReceiveMgdBean.onNodeSelect}" update=":form:mailheader :form:specifiedMsg"/> <p:treeNode> <h:outputText value="#{doc.name}" style="font-size:14px;"/> </p:treeNode> </p:tree> <p:outputLabel value="受信数:" style="font-size:10px;"/> <p:inputText autocomplete="false" id="numberOfMsg" value="#{messageReceiveMgdBean.numberOfMessages}" style="font-size:10px;"> </p:inputText> <h:commandButton id="upBtn" value="適用" style="font-size:10px;"> <f:ajax event="click" render="mailheader" execute="numberOfMsg" listener="#{messageReceiveMgdBean.updateMessageCount}"/> </h:commandButton> <input id="connect" type="button" value="リアルタイムチェック開始" style="font-size:10px;" onClick="connectServerEndpoint();"/> <input id="close" type="button" value="リアルタイムチェック中止" style="font-size:10px;" onClick="closeServerEndpoint();"/> </p:layoutUnit> <p:layoutUnit position="center"> <p:dataTable id="mailheader" var="mheader" paginator="true" paginatorPosition="bottom" value="#{messageReceiveMgdBean.mailHeaderModel}" rows="5" rowKey=" #{mheader.messageCount}" selection="#{messageReceiveMgdBean.selectedMailHeader}" selectionMode="single" style="width:800px;font-size:10px;" > <p:ajax event="rowSelect" listener="#{messageReceiveMgdBean.onMessageSelect}" update=":form:specifiedMsg" global="false"/> <p:column id="msubject" headerText="サブジェクト" style="font-size:10px;" sortBy="subject" width="50%"> #{mheader.subject} </p:column> <p:column id="maddress" headerText="アドレス" style="font-size:10px;" width="30%"> <ui:repeat value="#{mheader.fromAddress}" var="fromEmail"> #{fromEmail.toUnicodeString()} </ui:repeat> </p:column> <p:column id="mdate" headerText="日付" style="font-size:10px;" sortBy="sendDate" width="10%"> <h:outputLabel value="#{mheader.sendDate}"> <f:convertDateTime pattern="yyyy年MM月dd日 HH:mm:ss"/> </h:outputLabel> </p:column> <p:column id="msize" headerText="サイズ" style="font-size:10px;" sortBy="size" width="10%"> #{mheader.size} </p:column> </p:dataTable> <p:scrollPanel style="width:800px;height:400px" mode="native"> <h:outputText id="specifiedMsg" value="#{messageReceiveMgdBean.specifiedMessage}" escape="false"/> </p:scrollPanel> </p:layoutUnit> </p:layout> </h:form> <p:ajaxStatus onstart="PF('statusDialog').show();" onsuccess="PF('statusDialog').hide();"/> <p:dialog modal="true" widgetVar="statusDialog" header="処理中" draggable="false" closable="false"> <p:graphicImage value="/resources/imgs/ajaxloadingbar.gif" /> </p:dialog> </h:body> </html>
次に上記 Facelets のバックエンド処理を実装する、MessageReceiveMgdBean クラスを下記に示します。
package jp.co.oracle.samples.mgdBean; import java.io.Serializable; import java.util.List; import java.util.Properties; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Resource; import javax.enterprise.concurrent.ManagedExecutorService; import javax.inject.Named; import javax.faces.view.ViewScoped; import javax.inject.Inject; import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.Store; import jp.co.oracle.samples.tasks.AllFolderHandlerTask; import jp.co.oracle.samples.beans.FolderName; import jp.co.oracle.samples.beans.MailHeader; import jp.co.oracle.samples.beans.MailHeaderModel; import jp.co.oracle.samples.tasks.SpecifiedMessageHandlerTask; import jp.co.oracle.samples.tasks.SpecifiedNodeMailHeaderHandleTask; import org.primefaces.event.NodeSelectEvent; import org.primefaces.event.SelectEvent; import org.primefaces.model.TreeNode; /** * * @author Yoshio Terada */ @Named(value = "messageReceiveMgdBean") @ViewScoped public class MessageReceiveMgdBean implements Serializable { private Store store; private TreeNode root; private TreeNode selectedNode; private MailHeader selectedMailHeader; private MailHeaderModel mailHeaderModel; private String folderFullName; private String specifiedMessage; private int numberOfMessages = DEFAULT_NUMBER_OF_MESSAGE; private final static int DEFAULT_NUMBER_OF_MESSAGE = 10; private static final Logger logger = Logger.getLogger(MessageReceiveMgdBean.class.getPackage().getName()); @Inject IndexLoginMgdBean login; @Resource ManagedExecutorService execService; /** * コンストラクタ */ public MessageReceiveMgdBean() { } /** * ページのロード時の処理を実装 * 並列で各タスクを実行し結果表示速度を少し改善 */ public void onPageLoad() { String imapServer = login.getImapServer(); String username = login.getUsername(); String password = login.getPassword(); initStore(imapServer, username, password); if (getRoot() == null) { //全フォルダリストの取得 Future<TreeNode> folderHandlesubmit = execService.submit(new AllFolderHandlerTask(store)); int num = getNumberOfMessages(); if (num == 0) { num = DEFAULT_NUMBER_OF_MESSAGE; } // デフォルトで INBOX のメッセージの取得 folderFullName = "INBOX"; Future<MailHeaderModel> headerHandlerSubmit = execService.submit(new SpecifiedNodeMailHeaderHandleTask(store, folderFullName, num)); Future<String> messageHandlerSubmit = null; try { // デフォルトで INBOX の最新のメッセージ取得 messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, store.getFolder(folderFullName).getMessageCount())); } catch (MessagingException ex) { logger.log(Level.SEVERE, null, ex); } try { //左ペインのツリーの一覧を設定 root = folderHandlesubmit.get(); //右ペインのテーブルの設定 MailHeaderModel mailmodel = headerHandlerSubmit.get(); setMailHeaderModel(mailmodel); List<MailHeader> headers = mailmodel.getAllHeader(); //デフォルトで最新のメッセージを選択された状態に設定 if (headers != null && !headers.isEmpty()) { MailHeader latestMailHeader = headers.get(0); selectedMailHeader = latestMailHeader; } if (messageHandlerSubmit != null) { specifiedMessage = messageHandlerSubmit.get(); } } catch (InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } } } // ツリーが選択された際に呼び出されるイベント public void onNodeSelect(NodeSelectEvent event) { folderFullName = ((FolderName) selectedNode.getData()).getFullName(); int num = getNumberOfMessages(); if (num == 0) { num = DEFAULT_NUMBER_OF_MESSAGE; } // 選択したフォルダのメールヘッダを更新 Future<MailHeaderModel> headerHandlerSubmit = execService.submit(new SpecifiedNodeMailHeaderHandleTask(store, folderFullName, num)); // 選択したフォルダの最新メッセージを取得 try { MailHeaderModel mailmodel = headerHandlerSubmit.get(); //メールヘッダの更新 setMailHeaderModel(mailmodel); // 最新のメッセージ取得 Future<String> messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, store.getFolder(folderFullName).getMessageCount())); specifiedMessage = messageHandlerSubmit.get(); List<MailHeader> headers = mailmodel.getAllHeader(); //デフォルトで最新のメッセージを選択された状態に設定 if (headers != null && !headers.isEmpty()) { MailHeader latestMailHeader = headers.get(0); selectedMailHeader = latestMailHeader; } } catch (MessagingException | InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } } // メッセージが選択された際に呼び出されるイベント public void onMessageSelect(SelectEvent event) { int msgCount = ((MailHeader) event.getObject()).getMessageCount(); try { Future<String> messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, msgCount)); specifiedMessage = messageHandlerSubmit.get(); } catch (InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } } // メッセージのカウンタが更新された際の処理 // 10 よりしたの値が入力された場合、何もしない。 public void updateMessageCount() { String folderName = folderFullName; if (folderName.isEmpty()) { folderName = "INBOX"; } int num = getNumberOfMessages(); if (num > 10) { Future<MailHeaderModel> headerHandlerSubmit = execService.submit(new SpecifiedNodeMailHeaderHandleTask(store, folderName, num)); try { setMailHeaderModel(headerHandlerSubmit.get()); } catch (InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } } } // Store の初期化(ページのロード時) private void initStore(String imapServer, String username, String password) { Properties props = System.getProperties(); props.setProperty("mail.store.protocol", "imaps"); javax.mail.Store initStore; try { Session session = Session.getDefaultInstance(props, null); initStore = session.getStore("imaps"); initStore.connect(imapServer, username, password); this.store = initStore; } catch (MessagingException ex) { logger.log(Level.SEVERE, null, ex); } } /** * @return the selectedNode */ public TreeNode getSelectedNode() { return selectedNode; } /** * @param selectedNode the selectedNode to set */ public void setSelectedNode(TreeNode selectedNode) { this.selectedNode = selectedNode; } /** * @return the selectedMailHeader */ public MailHeader getSelectedMailHeader() { return selectedMailHeader; } /** * @param selectedMailHeader the selectedMailHeader to set */ public void setSelectedMailHeader(MailHeader selectedMailHeader) { this.selectedMailHeader = selectedMailHeader; } /** * @return the mailHeaderModel */ public MailHeaderModel getMailHeaderModel() { return mailHeaderModel; } /** * @param mailHeaderModel the mailHeaderModel to set */ public void setMailHeaderModel(MailHeaderModel mailHeaderModel) { this.mailHeaderModel = mailHeaderModel; } /** * @return the specifiedMessage */ public String getSpecifiedMessage() { return specifiedMessage; } /** * @param specifiedMessage the specifiedMessage to set */ public void setSpecifiedMessage(String specifiedMessage) { this.specifiedMessage = specifiedMessage; } /** * @return the numberOfMessages */ public int getNumberOfMessages() { return numberOfMessages; } /** * @param numberOfMessages the numberOfMessages to set */ public void setNumberOfMessages(int numberOfMessages) { this.numberOfMessages = numberOfMessages; } /** * @return the root */ public TreeNode getRoot() { return root; } }
本アプリケーションの実装において、特筆すべき点として JSF を利用する事で Ajax がとても簡単に実装できる点です。実際、今回のアプリケーションでは WebSocket の実装部以外で一切 JavaScript を記述しておらず、JSF の標準に含まれる <f:ajax> タグを拡張した PrimeFaces の <p:ajax> タグを使用して Ajax 通信を実現しています。
それでは、上記のコードを分割して、各コンポーネントの実装について詳しくご紹介していきます。
画面描画前に実行するコードの実装
この IMAP のメッセージを表示する画面は、画面にリクエストが発生した際に、各種画面のコンポーネントを初期化し、デフォルトで表示する全データを取得した後、描画を行ないます。これを実現するために、JSF では画面の描画前に処理を実行するために <f:event> タグを利用できます。
<h:head> <title>JSF-WebSocket WebMail</title> <f:event type="preRenderView" listener="#{messageReceiveMgdBean.onPageLoad}"/> </h:head>
ここで type=”preRenderView” 属性を追加しレンダリングされる前にイベントを発生する事を指定し、実行したい処理は listener で指定します。ここでは listener で「#{messageReceiveMgdBean.onPageLoad}”」を定義し、CDI (JSF の管理対象 Bean) として実装した MessageReceiveMgdBean クラスの onPageLoad() メソッドを呼び出しています。onPageLoad()メソッドでは最初のアクセスの際にデフォルトで描画する全コンポーネント(ツリーの構築部や、テーブルの構築部、メッセージの表示部)のデータを取得し組み立てます。
この際、「ツリーの構築部」、「テーブルの構築部」、「メッセージの表示部」を構成するための処理を、それぞれ並列処理タスクとして実装しました。 仮に並列処理で実装しない場合、画面を構築するためにかかる所要時間は、「フォルダ一覧表示部(画面左部)」+「フォルダ内のサブジェクト一覧表示部」+「メッセージ表示部」の合計時間になります。この時間を少しでも軽減するために、上記をそれぞれ別のタスクとして実装して並列に取得できるように実装します。
フォルダ一覧表示部(画面左部)の実装
下記に、画面左部の実装を説明します。
画面左部の「フォルダ一覧表示部」の部分のコードは下記です。下記は、大きく3つのコンポーネントから構成されています。
● フォルダの一覧を表示するツリー
● 受信数を変更するテキスト・フィールド
● WebSocket によるリアルタイム監視機能の 開始/中止 ボタン
<p:layoutUnit position="west" size="200" header="フォルダ一覧" resizable="true" closable="true" collapsible="true" style="font-size:14px;"> <p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}"> <p:ajax event="select" listener="#{messageReceiveMgdBean.onNodeSelect}" update=":form:mailheader :form:specifiedMsg"/> <p:treeNode> <h:outputText value="#{doc.name}" style="font-size:14px;"/> </p:treeNode> </p:tree> <p:outputLabel value="受信数:" style="font-size:10px;"/> <p:inputText autocomplete="false" id="numberOfMsg" value="#{messageReceiveMgdBean.numberOfMessages}" style="font-size:10px;"> </p:inputText> <h:commandButton id="upBtn" value="適用" style="font-size:10px;"> <f:ajax event="click" render="mailheader" execute="numberOfMsg" listener="#{messageReceiveMgdBean.updateMessageCount}"/> </h:commandButton> <input id="connect" type="button" value="リアルタイムチェック開始" style="font-size:10px;" onClick="connectServerEndpoint();"/> <input id="close" type="button" value="リアルタイムチェック中止" style="font-size:10px;" onClick="closeServerEndpoint();"/> </p:layoutUnit>
ここで、特に「フォルダ一覧表示部」のツリーは下記のコードで実現しています。
… 前略 <p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}"> <p:ajax event="select" listener="#{messageReceiveMgdBean.onNodeSelect}" update=":form:mailheader :form:specifiedMsg"/> <p:treeNode> <h:outputText value="#{doc.name}" style="font-size:14px;"/> </p:treeNode> </p:tree> … 後略
HTML の中でツリーを実現するために、PrimeFaces の <p:tree> タグを使用します。 また、<p:tree> には下記の属性を指定しています。
● id= コンポーネントの識別子
● value=ツリーを描画するための TreeNode オブジェクト
● var= TreeNode 内に含まれる各ノード・オブジェクト (FolderName) に対する変数
● selectionMode=選択モード
● dynamic=動的モード
● selection=選択されたノードを表すオブジェクト
次に、上記のツリーを描画するために必要な実装コード (MessageReceiveMgdBean クラス) を下記に抜粋します。
@Named(value = "messageReceiveMgdBean") @ViewScoped public class MessageReceiveMgdBean implements Serializable { private TreeNode root; private TreeNode selectedNode; @Resource ManagedExecutorService execService; public void onPageLoad() { // ログイン画面で入力されたデータの取得 String imapServer = login.getImapServer(); String username = login.getUsername(); String password = login.getPassword(); initStore(imapServer, username, password); if (getRoot() == null) { //全フォルダリストの取得 Future<TreeNode> folderHandlesubmit = execService.submit(new AllFolderHandlerTask(store)); try { root = folderHandlesubmit.get(); } catch (InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } } // Store の初期化(ページのロード時) private void initStore(String imapServer, String username, String password) { Properties props = System.getProperties(); props.setProperty("mail.store.protocol", "imaps"); javax.mail.Store initStore; try { Session session = Session.getDefaultInstance(props, null); initStore = session.getStore("imaps"); initStore.connect(imapServer, username, password); this.store = initStore; } catch (MessagingException ex) { logger.log(Level.SEVERE, null, ex); } } }
まず、initStore() メソッドを実行し IMAP サーバに接続します。接続に問題がない場合、18 行目〜26行目で並列タスクを実行し、実行結果よりツリーを取得しています。
ここで、ツリーを構成するための並列処理タスクは下記「AllFolderHandlerTask」クラスです。Callable インタフェースを実装したこのタスクは Runnable インタフェースを実装したタスクと違い、返り値 (TreeNode) を取得することができます。
タスクが ManagedExecutorService のインスタンス execService#submit() によって実行されると、AllFolderHandlerTask クラスの call() メソッドが呼び出されます。このメソッドは IMAP サーバに存在する全フォルダ一覧を再帰的に取得し、TreeNode を構築していきます。
より詳しく説明すると、TreeNode rootのインスタンスを生成し、IMAP サーバに存在するデフォルトのフォルダの一覧を取得します。そしてフォルダの一覧を root に付け加えていきます。次に取得したフォルダの中で子のフォルダを持つフォルダの場合、再帰的に子のフォルダ一覧を取得しツリーのノードに付け加えていきます。全てのフォルダ一覧を取得した後、全フォルダ一覧を含む TreeNode のオブジェクトを返します。
#getAllIMAPFolders()の再帰の実装正直いけてないですが、
#TreeNode に追加する方法上こう実装しました。
AllFolderHandlerTask#call() メソッドの処理が完了後、onPageLoad() メソッドに処理が戻り、root = folderHandlesubmit.get() で返り値を取得できます。そして取得した結果を root に代入しています。
public class AllFolderHandlerTask implements Callable<TreeNode> { private final Store store; private static final Logger logger = Logger.getLogger(AllFolderHandlerTask.class.getPackage().getName()); public AllFolderHandlerTask(Store store) { this.store = store; } @Override public TreeNode call() throws Exception { TreeNode root = new DefaultTreeNode("root", null); Folder[] folders; if (store == null) { return null; } try { folders = store.getDefaultFolder().list(); getAllIMAPFolders(root, folders); } catch (MessagingException ex) { logger.log(Level.SEVERE, null, ex); } return root; } private TreeNode getAllIMAPFolders(TreeNode root, Folder[] folders) { TreeNode child = null; try { for (Folder folder : folders) { String folName = folder.getName(); String folFullName = folder.getFullName(); if (hasChildFolder(folder) == true) { child = new DefaultTreeNode(new FolderName(folName, folFullName), root); getAllIMAPFolders(child, folder.list()); } else { child = new DefaultTreeNode(new FolderName(folName, folFullName), root); } } } catch (MessagingException ex) { logger.log(Level.SEVERE, null, ex); } return child; } //フォルダに子のフォルダがあるか否か private boolean hasChildFolder(Folder folder) throws MessagingException { boolean hasFolder = false; if (folder.list().length > 0) { hasFolder = true; } return hasFolder; } }
onPageLoad() メソッドに処理が戻った後、root に代入する事で、View からフォルダ一覧がツリーとして参照できるようになります。具体的には、Facelets の <p:tree> タグに記述している value=”#{messageReceiveMgdBean.root}” で参照できるようになります。
また、<p:tree> タグに追加した属性 var=”doc” によって、ツリー中に含まれる各フォルダは、変数 doc として EL 式中で扱う事ができるようになります。具体的に doc は FolderName クラスのインスタンスを表します。例えば、#{doc.name} は FolderName インスタンスの getName() メソッドを呼び出し、フォルダ名を取得することができます。
<p:treeNode>タグ中に <h:outputText> タグを記載しています。このタグはテキストを表示するためのコンポーネントですが、属性 value=”#{doc.name}” で各フォルダの名前を出力しています。
<p:tree id="docTree" value="#{messageReceiveMgdBean.root}" var="doc" selectionMode="single" dynamic="true" selection="#{messageReceiveMgdBean.selectedNode}"> <p:ajax event="select" listener="# {messageReceiveMgdBean.onNodeSelect}" update=":form:mailheader :form:specifiedMsg"/> <p:treeNode> <h:outputText value="#{doc.name}" style="font-size:14px;"/> </p:treeNode> </p:tree>
次に、ツリー中のフォルダが選択された際の処理を説明します。ツリー中のフォルダを選択した際、選択されたツリーのノードは、<p:tree> タグで指定した属性、selection (<p:tree selection=”#{messageReceiveMgdBean.selectedNode}”>)によって MessageReceiveMgdBeanクラスの selectedNode に代入されます。
ここで、<p:tree> タグの直下に、<p:ajax>タグを記載していますが、この<p:ajax>タグは、ツリー内で特定のフォルダが選択されると、同タグ中の listener=”#{messageReceiveMgdBean.onNodeSelect}”
を呼び出します。これは、内部的に MessageReceiveMgdBean クラスの onNodeSelect(NodeSelectEvent event)を呼び出します。
onNodeSelect()のメソッドでは、選択されたフォルダに存在する「フォルダ内のサブジェクト一覧表示部」と「メッセージ表示部」を取得する処理が行なわれます。
呼び出した結果、<p:ajax> タグの update 属性の設定に従い、 <p:ajax update=”:form:mailheader :form:specifiedMsg”/>「:form:mailheader」と「:form:specifiedMsg」で指定するコンポーネント、つまり「フォルダ内のサブジェクト一覧表示部」と「メッセージ表示部」を更新します。
「フォルダ内のサブジェクト一覧表示部(画面右上部)の実装
つづいて、「フォルダ内のサブジェクト一覧表示部」の実装を説明します。
テーブルを実現している Facelets の実装コードは下記です。
<p:dataTable id="mailheader" var="mheader" paginator="true" paginatorPosition="bottom" value="#{messageReceiveMgdBean.mailHeaderModel}" rows="5" rowKey=" #{mheader.messageCount}" selection="#{messageReceiveMgdBean.selectedMailHeader}" selectionMode="single" style="width:800px;font-size:10px;" > <p:ajax event="rowSelect" listener="#{messageReceiveMgdBean.onMessageSelect}" update=":form:specifiedMsg" global="false"/> <p:column id="msubject" headerText="サブジェクト" style="font-size:10px;" sortBy="subject" width="50%"> #{mheader.subject} </p:column> <p:column id="maddress" headerText="アドレス" style="font-size:10px;" width="30%"> <ui:repeat value="#{mheader.fromAddress}" var="fromEmail"> <h:outputLabel value="#{fromEmail.toUnicodeString()}"/> </ui:repeat> </p:column> <p:column id="mdate" headerText="日付" style="font-size:10px;" sortBy="sendDate" width="10%"> <h:outputLabel value="#{mheader.sendDate}"> <f:convertDateTime pattern="yyyy年MM月dd日 HH:mm:ss"/> </h:outputLabel> </p:column> <p:column id="msize" headerText="サイズ" style="font-size:10px;" sortBy="size" width="10%"> #{mheader.size} </p:column> </p:dataTable>
ちょっと補足:
web.xml の最後に下記の <context-param>を追加してください。
上記 JSF の Facelets でメッセージ送信日付を、f:convertDateTime で変換し描画しています。この変換の際デフォルトのタイムゾーンをシステムのタイムゾーンに変更します。
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> … 前略 <context-param> <param-name>javax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE</param-name> <param-value>true</param-value> </context-param> </web-app>
画面が描画前に呼び出される処理は onPageLoad() メソッドに実装されている事は上記ですでに紹介しました。onPageLoad() メソッドの中、つまり最初のアクセスの際に、テーブルは「INBOX(受信箱)」に存在するメッセージを並列タスクとして取得し描画します。
HTML のテーブルを扱うために PrimeFaces の <p:dataTable>を使用します。
<p:dataTable id=”mailheader” var=”mheader”
paginator=”true”
paginatorPosition=”bottom”
value=”#{messageReceiveMgdBean.mailHeaderModel}”
rows=”5″ rowKey=” #{mheader.messageCount}”
selection=”#{messageReceiveMgdBean.selectedMailHeader}”
selectionMode=”single” style=”width:800px;font-size:10px;” >
<p:dataTable> タグでは下記の属性を指定しています。
● paginator : テーブル内にボタンを表示しページ移動を可能にする属性
● paginatorPosition : ページ移動用のボタンの配置場所を指定
● value: テーブル内で扱うデータ・モデル
● rows: テーブルで描画する行数
● rowKey: 行を選択した際、他の行と区別するためのキー
● selection: 選択された行のデータ
● selectionMode: 選択モード
この中で、特に重要なのが value で指定するデータ・モデルです。今回、このテーブルを扱うためのモデルとして、MailHeaderModel クラスを実装し、value にはこのクラスのインスタンスを代入します。MailHeaderModel クラスは MailHeader オブジェクト(行を表すオブジェクト)を List で管理しています。これらのクラスのインスタンスは並列処理タスク SpecifiedNodeMailHeaderHandleTask の中で生成されます。下記に MailHeaderModel、MailHeader クラスの実装をそれぞれ示します。
まず、テーブル内に表示するデータを保持する MailHeader クラスを作成します。今回テーブルには「サブジェクト」、「From:」、「送信日」、「メッセージのサイズ」の4項目を表示させますので、4つのフィールド(subject, fromAddress, sendDate, size)を定義しています。
またmessageCount はフォルダ内に存在するメッセージの内、特定のメッセージを識別するためのキーをメッセージのカウントIDとして指定します。実際には、SpecifiedNodeMailHeaderHandleTask の中でMailHeaderのインスタンスを生成する際、Java Mail API の Message#getMessageNumber() をここに代入します。
@ViewScoped public class MailHeader { private String subject; private Address[] fromAddress; private Date sendDate; private int size; private Integer messageCount; public MailHeader(String subject, Address[] fromAddress, Date sendDate, int size, Integer messageCount) { this.subject = subject; this.fromAddress = fromAddress; this.sendDate = sendDate; this.size = size ; this.messageCount = messageCount; } 別途、Setter, Getter メソッドを実装してください。 }
次に、上記 MailHeader クラスを管理するクラスを MailHeaderModel に実装します。このクラスはインスタンスが生成された際に、コンストラクタ内で MailHeader の List を最新日付順(最新のメッセージのカウント ID 順)でソートします。そして各行を識別するためのキーとして、メッセージのカウントID を使用します。
@ViewScoped public class MailHeaderModel extends ListDataModel<MailHeader> implements SelectableDataModel<MailHeader> { public MailHeaderModel() { } public MailHeaderModel(List<MailHeader> header) { super(header); Collections.sort(header, new Comparator<MailHeader>() { @Override public int compare(MailHeader m1, MailHeader m2) { return m2.getMessageCount() - m1.getMessageCount(); } }); } @Override public Object getRowKey(MailHeader header) { return header.getMessageCount(); } @Override public MailHeader getRowData(String rowKey) { List<MailHeader> headers = (List<MailHeader>) getWrappedData(); for (MailHeader header : headers) { if (header.getMessageCount().toString().equals(rowKey)) { return header; } } return null; } }
ここで、画面ロード時に呼び出される MessageReceiveMgdBean#onPageLoad() メソッドの中から、テーブルを初期化する部分のコードを下記に抜粋し示します。画面ロード時にはデフォルトで INBOX のメッセージを取得します。INBOX のフォルダを SpecifiedNodeMailHeaderHandleTask に指定し並列タスクとして実行します。
public void onPageLoad() { // デフォルトで INBOX のメッセージの取得 folderFullName = "INBOX"; Future<MailHeaderModel> headerHandlerSubmit = execService.submit( new SpecifiedNodeMailHeaderHandleTask(store, folderFullName, num)); try { MailHeaderModel mailmodel = headerHandlerSubmit.get(); setMailHeaderModel(mailmodel); // テーブル中の最新のメッセージ(MailHeader)をデフォルトで // 選択 (マウスがクリックされた) 状態にする。 List<MailHeader> headers = mailmodel.getAllHeader(); if (headers != null && !headers.isEmpty()) { MailHeader latestMailHeader = headers.get(0); selectedMailHeader = latestMailHeader; } } catch (InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } }
execService#submit によりSpecifiedNodeMailHeaderHandleTask(store, folderFullName, num) の並列タスクを実行します。このタスクは、指定された IMAP のフォルダの中から、num で指定した数だけ最新メッセージを取得し、取得したメッセージ(MailHeader) を含む MailHeaderModel のインスタンスを返します。
並列処理タスクの処理が完了すると、onPageLoad() に処理が戻り、headerHandlerSubmit.get() でMailHeaderModel を取得できますので、取得した MailHeaderModel を setMailHeaderModel() で置き換えて更新します。更新した後、最新のメッセージを選択状態にするため、最新のメッセージを取得し、selectedMailHeader に代入しています。
public class SpecifiedNodeMailHeaderHandleTask implements Callable<MailHeaderModel> { private final Store store; private final String folderFullName; private final int numberOfMessage; private static final Logger logger = Logger.getLogger(SpecifiedNodeMailHeaderHandleTask.class.getPackage().getName()); public SpecifiedNodeMailHeaderHandleTask(Store store,String folderFullName,int numberOfMessage){ this.store = store; this.folderFullName = folderFullName; this.numberOfMessage = numberOfMessage; } @Override public MailHeaderModel call() throws Exception { MailHeaderModel model = null; if (store != null) { try { Folder folder = store.getFolder(folderFullName); if (!folder.isOpen()) { folder.open(javax.mail.Folder.READ_WRITE); } int endMsgs = folder.getMessageCount(); int startMsgs = endMsgs - (numberOfMessage - 1); Message[] msgs = folder.getMessages(startMsgs, endMsgs); List<MailHeader> data = new ArrayList<>(); for (Message msg : msgs) { MailHeader msgModel = new MailHeader(msg.getSubject(), msg.getFrom(), msg.getSentDate(), msg.getSize(), msg.getMessageNumber()); data.add(msgModel); } model = new MailHeaderModel(data); } catch (MessagingException ex) { logger.log(Level.SEVERE, null, ex); } } return model; } }
次に、テーブル内で行が選択された場合の実装、振る舞いを紹介します。テーブル内で行を選択すると、選択した行を表す MailHeader オブジェクトが、<p:dataTable> タグの selection 属性で指定した、
<p:dataTable selection=”#{messageReceiveMgdBean.selectedMailHeader}” >
に代入されます。また、行を選択した際のキーは MailHeaderModel クラス内の getRowKey() メソッドの返り値、つまり MailHeader の getMessageCount() をキーとなります。
また、<p:dataTable> タグの直後に<p:ajax>タグを記述しています。これにより、実際に行が選択されると <p:ajax> の属性 listener で示すメソッド onMessageSelect() メソッドが呼び出されます。
// メッセージが選択された際に呼び出されるイベント public void onMessageSelect(SelectEvent event) { int msgCount = ((MailHeader) event.getObject()).getMessageCount(); try { Future<String> messageHandlerSubmit = execService.submit(new SpecifiedMessageHandlerTask(store, folderFullName, msgCount)); specifiedMessage = messageHandlerSubmit.get(); } catch (InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } }
onMessageSelect() メソッドは選択された該当のメッセージを、SpecifiedMessageHandlerTask で実装された並列タスクで取得し、取得したメッセージの文字列を specifiedMessage に代入します。
この Ajax リクエストは処理が完了後、メッセージを<p:ajax> タグで設定されている update 属性の内容に従い、”:form:specifiedMsg” つまり「メッセージの表示部」に更新します。
メッセージ表示部(画面右下部)の実装
最後にメッセージ表示部の実装部分について説明します。
![]() |
specifiedMessage は View(Facelets) の中で下記のコードで参照・表示されます。
<p:scrollPanel style="width:800px;height:400px" mode="native"> <h:outputText id="specifiedMsg" value="#{messageReceiveMgdBean.specifiedMessage}" escape="false"/> </p:scrollPanel>
<p:scrollPalnel> タグは、スクロールが可能なパネルで長文のメッセージを表示する際に、スクロールしながら参照が可能なパネルです。このスクロール可能なパネルの中で、IMAP の特定メッセージを <h:outputText> タグ内で表示します。ここで、<h:outputText> タグ内で excape=”false” と指定していますが、これは HTML メールを参照する場合、JSF では
デフォルトで < や > 等をエスケープ&lt;、&gt;しますので、HTMLメールをそのまま表示させるために、この部分だけエスケープしないように設定しています。
実際に、指定されたメッセージのカウントID を持つメッセージを取得するための並列タスク SpecifiedMessageHandlerTask クラスを下記に示します。
public class SpecifiedMessageHandlerTask implements Callable<String> { private final Store store; private final String folderFullName; private final int msgCount; private static final Logger logger = Logger.getLogger(SpecifiedMessageHandlerTask.class.getPackage().getName()); public SpecifiedMessageHandlerTask(Store store, String folderFullName, int msgCount) { this.store = store; this.folderFullName = folderFullName; this.msgCount = msgCount; } @Override public String call() throws Exception { String returnMsg =""; if (store != null) { Folder folder; try { folder = store.getFolder(folderFullName); if (!folder.isOpen()) { folder.open(javax.mail.Folder.READ_WRITE); } Message msg = folder.getMessage(msgCount); MessageDumpUtil dumpUtil = new MessageDumpUtil(); returnMsg = dumpUtil.getText(msg); } catch (MessagingException | IOException e) { logger.log(Level.SEVERE, null, e); } } return returnMsg; } }
Message を取得するためには、Java Mail の API を使用して folder.getMessage(msgCount) で取得します。ただし Message は単純なプレイン・テキストだけではなく、マルチパート、html 形式様々な形の
メッセージが存在します。そこで、メッセージのタイプに応じて表示用の文字列を取得する必要があります。
今回の実装では、Base 64 への未対応や、マルチパートファイルのダウンロードなどは実装しておらず、テキストと HTML 表示のみ対応しています。HTML ならばその文字列をそのまま返し、プレイン・テキストの場合、<:PRE>を付加してメッセージの形式をそのままの形で表示して返しています。Message の解析を行ない表示用の文字列を取得するためのユーティリティ・クラスを MessageDumpUtil として実装しました。MessageDumpUtil クラスを下記に示します。
public class MessageDumpUtil { private boolean textIsHtml = false; public String getText(Part p) throws MessagingException, IOException { if (p.isMimeType("text/*")) { textIsHtml = p.isMimeType("text/html"); if (true == textIsHtml) { return (String) p.getContent(); } else { return getPreString((String) p.getContent()); } } if (p.isMimeType("multipart/alternative")) { // prefer html text over plain text Multipart mp = (Multipart) p.getContent(); String text = null; for (int i = 0; i < mp.getCount(); i++) { Part bp = mp.getBodyPart(i); if (bp.getContent() instanceof BASE64DecoderStream) { return "現在 Base 64 のコンテンツには現在未対応です。"; } else if (bp.isMimeType("text/plain")) { if (text == null) { text = getPreString(getText(bp)); } } else if (bp.isMimeType("text/html")) { String s = getText(bp); if (s != null) { return s; } } else { return getPreString(getText(bp)); } } return text; } else if (p.isMimeType("multipart/*")) { Multipart mp = (Multipart) p.getContent(); for (int i = 0; i < mp.getCount(); i++) { String s = getText(mp.getBodyPart(i)); if (s != null) { return s; } } } return null; } private String getPreString(String data) { StringBuilder sb = new StringBuilder(); sb.append("<PRE style=\"font-size:12px;\">"); sb.append(data); sb.append("</PRE>"); String s = sb.toString(); return s; } }
このユーティリティ・クラスでは getText(Part p) メソッドが実際の解析を行なっています。解析を行なった後、HTML は HTML として、テキストはテキストとして文字列を取りだし、最後に表示用の文字列を String で返しています。ユーティリティ・クラスから文字列が返ってくると、その値をそのまま並列タスクの戻り値として返します。
並列タスクの処理が完了すると <h:outputText value=”#{messageReceiveMgdBean.specifiedMessage}” > でその文字列を描画するために、戻り値を specifiedMessage に代入します。
以上により、大まかな実装の概要説明は終了です。
Ajax 通信の際のダイアログ表示方法
最後に、長時間の Ajax 通信時にステータス・ウィンドウを表示させる方法を紹介します。
Ajax リクエストを行なう際に、上記のステータスを表示するダイアログを表示させています。これは Ajax で長時間処理を要するような処理を行なう際にとても有用です。特に今回は IMAP サーバに直接接続を行いフォルダ一覧を取得したりメッセージを取得しているため、通常の DB アクセスよりもさらに時間を要すような処理を Ajax として実装しました。仮に上記のようなダイアログを使用しない場合、本当に Ajax のリクエストが実行されているのか否かわからなくなります。そこで、このような長時間処理用に、今回 PrimeFaces の下記のタグを使用して実装しました。
<p:ajaxStatus onstart="PF('statusDialog').show();" onsuccess="PF('statusDialog').hide();"/> <p:dialog modal="true" widgetVar="statusDialog" header="処理中" draggable="false" closable="false"> <p:graphicImage value="/resources/imgs/ajaxloadingbar.gif" /> </p:dialog>
基本的には、上記のコードを記述する事で全ての Ajax 通信時にステータス・ウィンドウが表示されるようになります。しかし、全ての処理で上記ダイアログを表示させたくない場合もあります。そのような場合、<p:ajax> タグの属性 global を false に設定する事で、その Ajax リクエストではダイアログを非表示にする事もできます。実際、私の場合は、テーブル中で特定のメッセージを選択した際、その対象メッセージをスクロール・パネルに表示させますが、その Ajax リクエストではダイアログを非表示にしています。
<p:dataTable id="mailheader" var="mheader" paginator="true" paginatorPosition="bottom" value="#{messageReceiveMgdBean.mailHeaderModel}" rows="5" rowKey=" #{mheader.messageCount}" selection="#{messageReceiveMgdBean.selectedMailHeader}" selectionMode="single" style="width:800px;font-size:10px;" > <p:ajax event="rowSelect" listener="#{messageReceiveMgdBean.onMessageSelect}" update=":form:specifiedMsg" global="false"/>
※ご注意:全てのコンポーネントで global=false が利用できるわけではないようです。
このようにして、PrimeFaces のようなリッチな JSF コンポーネントを使用する事で、標準の JSF でもある程度簡単に、シングル・ページ・アプリケーションを構築する事ができます。如何でしょうか?是非この開発生産性の高い技術をお試しください。
最後に、
本アプリケーションは短時間で実装し、あくまでも JSF, WebSocket, JavaMail のサンプル・アプリケーションとして作成しました。そのため、実務レベルで使用するためには細かい部分で実装が足りていません。
例えばログイン時に毎回 IMAP サーバに接続し全情報を取得しますので、アプリケーションの動作としてとても遅いと感じるかもしれません。それはJSF ではなく、毎回 IMAP サーバに接続しに行っているため遅くなっています。また、一般的な IMAP のメールクライアントが実装しているような、ローカル・キャッシュを実装していません。毎回直接 IMAP サーバに参照に行っています。起動時に全画面の描画を高速にするためには、ローカル・キャッシュのデータを参照するなどが必要です。また各処理を並列処理で行なっていますが、タイミングに応じて異なるメッセージが選択される可能性もあります(注意点は GitHub のソースに記述しています)。また、セッション・タイムアウトに対する実装も今回は実装していません。ログイン認証も簡易実装しています。Java EE におけるログイン認証は「たかがレルムされどレルム GlassFish で始める詳細 JDBC レルム」のエントリをご参照ください。
ここで、ご紹介した内容の内、ご参考になる部分があれば幸いです。
はじめての Project Avatar
JavaOne 2013 San Francisco で Project Avatar のオープン・ソース化が発表されました。そこで、本エントリでは Avatar にご興味を持って頂いた方が、どこから Avatar に触ればよいのかを分かりやすくするために、Avatar プログラムの実行方法、Avatar プログラムの作成方法をご紹介します。
※ 昨年、Project Avatar について、下記のプレゼンでアーキテクチャ等をご紹介していますが基本的なコンセプトは変わっていません。しかしこの1年で実装方法が大きく修正されています。昨年の時点では、View の実装部分で Avatar 専用のタグライブラリを使用しなければなりませんでしたが、今回 OSS 化された Avatar の実装を確認すると、標準の HTML 5 + JavaScript + EL 構文で実装できるようになっています。昨年の JavaOne 2012 の BoF で開発者から頂いたフィードバックを受けて、今回の実装方法に修正された事と想定します。
それでは、実際に OSS 化された Avatar の実行方法、プログラムの作成方法を下記にご紹介します。
1. まずはじめに、JDK 8 をダウンロードしてインストールしてください。
(※Avatar の動作のためには、JDK 8 の build 103 以降が必要です。)
Open JDK 8 Early Access のダウンロード
2. 次に Avatar の実行環境をバンドルした GlassFish v4 を入手してインストールしてください。
入手はこちらから
3. 次に AVATAR_HOME (GlassFish のインストールした場所) の環境変数と GlassFish の bin へのパスを設定してください。
csh 系の場合:
setenv AVATAR_HOME /Applications/NetBeans/avatar-gf-1.0-ea
set path=($path;/Applications/NetBeans/avatar-gf-1.0-ea/glassfish4/bin)
4. Avatar プロジェクトの作成
3. でパスを通しておくと、avatar コマンドが実行できるようになってます。
avatar コマンドで Avatar プロジェクトを作成してください。
Avatar プロジェクトを作成するとデフォルトで下記のディレクトリが作成
されます。
# avatar new [project-name]
# ls -F project-name/
WEB-INF ディレクトリ配下には readme.txt が作成されています。
view ディレクトリ配下に src ディレクトリが存在し、hello.html ファイルが
作成されます。
hello.html ファイルの内容
<!DOCTYPE html> <html> <head> <META http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Hello</title> </head> <body> Hello </body> </html>
5. GlassFish v4 の起動
GlassFish を起動します。
# asadmin start-domain
6. Avatar プロジェクトのデプロイ
GlassFish に Avatar プロジェクトをデプロイします。
# asadmin deploy project-name
7. デプロイした Avatar プロジェクトの動作確認
ブラウザで http://localhost:8080/project-name へアクセスしてください。
Hello のみがブラウザ上に表示されます。
8. Avatar アプリケーションの作成の準備
まず、Avatar の基本概念を簡単にご紹介すると、Avatar は View
(クライアント側の実装) と Service(サーバ側の実装)をそれぞれ実装します。
また、Avatar はクライアントとサーバの通信に、RESTful, Server-Sent
Event, WebSocket を利用可能です。
今回は、まず最も簡単なアプリケーションを作成するために、
RESTful 対応のアプリケーションを作成します。
4. で Avatar プロジェクトを下記のコマンドを実行し作成しましたが、
# avatar new project-name
上記コマンドを実行した際には、下記のように view のディレクトリしか
作成されていません。
# ls -F project-name/
サーバ側の実装も行うためには、service ディレクトリとそのディレクトリ
配下にsrc ディレクトリを作成する必要があります。下記のコマンドを
実行して service ディレクトリを作成してください。
# mkdir service
# mkdir service/src
# ls -F
# ls -F service
src/
今回作成する RESTful 対応のサンプル・アプリケーションは、ブラウザ
からボタンを押下すると GET リクエストを送信し、サーバ側の時間を
取得してクライアントに表示させるアプリケーションを作成します。
9. Avatar RESTful 対応のサーバ側 (Service) の実装
まず、サーバ側の実装を行うために、service ディレクトリの下の
src ディレクトリに移動し、main.js ファイルを作成します。
ここでは、下記のコードを実装してください。
# cd service/src
# vi main.js
var avatar = require("org/glassfish/avatar"); var getTime = function(){ var current = new Date(); return{ h: current.getHours(), m: current.getMinutes(), s: current.getSeconds() }; }; avatar.registerRestService({ url:"data/message"}, function(){ this.$onGet = function(request, response){ response.$send(getTime()); }; } );
備考:
Service 側の API は下記 URL に記載されておりますが、REST, Server-Sent
Event(SSE), WebScoket それぞれの実装ができるようになっています。
https://avatar.java.net/jsdoc/service/avatar.html
例:
RESTful : registerRestService(metadata, restService)
SSE : registerPushService(metadata, pushService)
WebSocket : registerSocketService(metadata, socketService)
10. Avatar RESTful 対応のクライアント側(View)の実装
View は下記の内容を実装してください。
<!DOCTYPE html> <html> <head> <META http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Hello</title> </head> <body> <script data-model="rest"> var Message = function(){ this.msg = this.h = this.m = this.s =''; }; </script> <script data-type="Message" data-instance="message" data-url="data/message"></script> <output class="time">#{message.h}:#{message.m}:#{message.s}</output> <button onclick="#{message.$get()}">Update</button> </body> </html>
今回、REST のモデルを使用しますので、<script data-model=”rest”>を
定義しています。
REST モデルで利用可能な API は下記の通りです。
https://avatar.java.net/jsdoc/view/module-RestModelBase.html
今回はボタンを押下した際に GET リクエストでサーバにリクエストを
送信し、サーバ側の時間を表示しますが、Avatar ではモデル・データの
バインディングに Java EE に含まれる Expression Language(EL) 構文
を使用します。
具体的には、#{message.h} , #{message.m} , #{message.s} と記載している
部分が EL 式になります。
11. Avatar プロジェクトのコンパイル
View と Service をそれぞれ実装完了した後、プロジェクトをコンパイル
してください。
# avatar compile project-name
コンパイル後、このアプリケーションを動作させるために必要なファイルが
自動的に追加されます。
12. Avatar プロジェクトを GlassFish にデプロイ
コンパイルが完了すると、アプリケーション・サーバにデプロイします。
# asadmin deploy project-name
13. RESTful アプリケーションの動作確認
ブラウザより http://localhost:8080/project-name にアクセスしてください。
ボタンを押下するとサーバ側の時刻が表示され、ボタンを押す事に
サーバの時刻が更新されるようになります。
14. RESTful アプリケーションから SSE アプリケーションへ移行
RESTful の場合、ボタンを押下しなければサーバ側のデータが取得
できません。
そこで、RESTful から SSE にアプリケーションを変更し、サーバ側から
自動的に時刻を通知するように変更します。
それぞれ、View と Service の実装を下記のように修正してください。
View 側の修正
<!DOCTYPE html> <html> <head> <META http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Hello</title> </head> <body> <script data-model="push"> var Time = function(){ this.msg = this.h = this.m = this.s =''; }; </script> <script data-type="Time" data-instance="time" data-url="data/time"></script> <output class="time">#{time.msg}#{time.h}:#{time.m}:#{time.s}</output> </body> </html>
Service 側の修正
var avatar = require("org/glassfish/avatar"); var message = 'The Server Time is '; var getTime = function(){ var current = new Date(); return{ msg: message, h: current.getHours(), m: current.getMinutes(), s: current.getSeconds() }; }; avatar.registerPushService({ url:"data/time"}, function(){ this.$onOpen = this.$onTimeout = function(context){ context.$setTimeout(1000); return context.$sendMessage(getTime()); }; } );
15. SSE アプリケーションのコンパイル
ソースコードを修正したら、再度コンパイルします。
# avatar compile project-name
16. SSE アプリケーションの動作確認
ブラウザより、http://localhost:8080/project-name にアクセスしてください。
ブラウザでアクセスすると自動的にサーバの時刻が表示されるように
なります。
最後に
今回は、Avatar で簡単な RESTful, SSE のアプリケーションを作成
しましたが、WebSocket なども扱う事ができます。
色々と試してみたい場合、バンドルされている、exapmples をご参考頂く
ことがとても有用です。
example をデプロイして色々なサンプルコードをご覧いただき試して
頂ければ幸いです。また試していただいた後、Avatar プロジェクトでは
フィードバックを求めています。是非、色々なフィードバックを頂ければ
誠に幸いです。
Avatar サンプル・アプリケーションのデプロイ方法
インストールした GlassFish v4 のインストール・ディレクトリ
直下にサンプル・アプリケーションが用意されています。
サンプル・アプリケーションをデプロイして確認してください。
# asadmin deploy avatar-gf-1.0-ea/examples/examples.ear
サンプル・アプリケーションの動作確認
ブラウザより http://localhost:8080/examples にアクセスしてください。
豊富なサンプルが用意されていますので、こちらをご参照頂き
Avatar でどのような事ができるかをご覧ください。
You are the “Make the future Java”
本日、GlassFish ユーザ・グループ・ジャパンによる Java EE 7 リリース記念のナイトセミナーが開催されました。本日ご登壇いただいた、上妻さん、久保田さん、槇さん、蓮沼さんには大変感謝すると共に、ご参加頂いた皆様にも大変感謝いたしております。(下記の写真の通り会場は満員で、ご登録者数は 140 名、実際にご参加頂いた方も 88 名でした。)
Java EE 7 そして GlassFish の最新情報をコミュニティ・ドリブンでお届け頂ける、このような機会はとても貴重だと感じております。GlassFish ユーザ・グループ・ジャパン副会長のの蓮沼さん、そして参加者の皆様ありがとうございます。
GlassFish は元々、日本では元 Sun の3人のメンバーが日本での啓蒙活動を開始しましたが、今や GlassFish はこの3人の手を離れ、Java EE の参照実装として、コミュニティ・ドリブンで情報提供がなされ、さらには多くのユーザが GlassFish の良さに気付きご賛同頂きはじめた事を、日本で活動を始めた3人の内の1人として大変嬉しく思っております。
Java EE 7 は、WebSocket, JOSN, jBatch, Concurrency Utilities for EE といった新機能が含まれ、特に WebSocket, JSON あたりの技術は特に開発者が注目する技術だと思います。また本日、上妻さんに発表して頂いた、jBatch に関しても非常に詳しい内容をご紹介頂いたため、今後多くの開発者にとってとても有用な情報だったのではないかと思います。
その一方でツイート上を除くと「Java EE 7 が流行ればいいな」といった、(言葉が若干悪いかもしれませんがお許しください)他人まかせなご意見も見受けられました。これに対して私が皆様にご期待する内容として、本ブログのタイトルにもございますように、「将来の Java を創っていくのは皆様です!!」
もし、Java EE 7 もしくは GlassFish に関して、ご興味を頂いた方、もしくは試して見ようと思われ方がいらっしゃったら、どんな些細な事でも結構です。実際に試して頂いた内容を、体裁問わず、ブログや Wiki、その他何でも結構です、試された内容を是非公に公開してください。それらの情報が他の開発者にとっても有用な情報になり、それが流行(トレンド)になっていく物と心より信じております。
例えば、今このエントリで Java EE 7 のどの技術にご興味があるかアンケートを実施しています。結果をみると、圧倒的に Java EE 7 のご興味のある技術は WebSocket になっていますが、全ての開発者が WebSocket に興味を持っているわけではなく、JAX-RS 2.0, JSF 2.2, JSON, Concurrency , CDI 等の技術にご興味を持って頂いている方も多く見受けられます。実際、jBatch に関しては現在7番目の人気となっておりますが、本日、上妻さんにご登壇頂いた内容は多くの開発者にとってとても有用だったと思います。
ここで申し上げたい事は、皆様、それぞれ異なる興味分野を持っていらっしゃると思います。1人が全てを一度に試す(把握する)事は困難ですが、自分の興味分野、試した内容を公開する事でかならず、他の Java 開発者の為にもなります。
もちろん、私も今後も継続して情報発信してまいりますが、1人でできる事はとても限られています。スケールも致しません。それを支えていただけるのは皆様です。仮に、試してダメだと思った所は、正直この機能のここがダメだとフィードバックをください。それが将来の Java を創っていく事だと思います。
「将来の Java を創っていくのは皆様」なのです。
是非、お試しいただいた内容を、どのような形でも結構です。是非情報をご共有頂ければ大変嬉しく思います。
最後にくどいようですが、繰り返します「将来の Java を創っていくのは皆様です!!」
祝 Java EE 7 ローンチの日本における反応
日本時間の深夜から午後に掛けて、世界に渡る Java EE 7 ローンチ・イベントがおかげさまで無事終了致しました。日本から深夜、もしくは午後にご参加頂いたエンジニアもいらっしゃいますが、本イベントにご参加頂いた皆様には、Java EE 7 の3つテーマ(HTML 5 対応、開発生産性の向上、エンタープライズ・ニーズへの対応)をご理解頂けたのではないかと思います。
また主要な統合開発環境(Eclipse, NetBeans, Intellij)も Java EE 7 へ対応する事で、今後日本におけるエンタープライズ開発の標準も Java EE 7 へ移行していく物と思われます。今回リリースされた Java EE 7 には様々なテクノロジーが含まれますが、Java EE 7 に含まれる各種機能を試された方は、是非その試された内容をブログや Wiki 等で記載して頂ければ嬉しく思います。
Java EE 7 は EE 6 に対して開発生産性を向上したバージョンであるため、本来であれば Java EE 7 を直接試していただきたいのですが、仮に Java EE にはじめて触れられる方で Java EE の全体像が理解できない方、もしくは英語が大の苦手な方は、既存の Java EE 6 の日本語書籍(Amazon : Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer’s SELECTION) [大型本]) をご参照頂き全体像をご理解頂いた上で、Java EE 6 と EE 7 の差分を抑えて頂く事も可能ですので、いずれかのやりやすい方法で Java EE 7 に触れていただければ幸いです。
最後に、Java EE 7 のローンチに関連して各種メディア、ブログ等に記載された内容を下記にご紹介します。今まで Java EE に実際に触れて来られた方も、この新しい Java EE 7 に期待している内容などが記載されていますので、是非下記の記事もご参照頂ければ幸いです。
(※ 私もブログ書いたよという方がいらっしゃいましたら是非ご連絡ください、下記に追加させて頂きます。)
オンラインメディア
- Publickey:Java EE 7対応のアプリケーションサーバ「GlassFish 4」オープンソース版が公開
- マイナビ ニュース:Oracle、Java EE 7を提供開始
- クラウド Watch : 米OracleがJava EE 7をリリース、HTML5対応アプリの構築を支援する新機能などを追加
- SourceForge.JP:「Java EE 7」が正式発表、米Oracleから初の登場
- @IT : HTML5のサポート強化や開発者の生産性を向上:Oracle、「Java EE 7」を提供開始
個人ブログ
- 武者返し.com:祝! Java EE 7リリース
- きしだのはてな:WebSocketをネタにJava EE 7正式版を試してみる
- しんさんの出張所 はてな編:JavaEE 7 登場
- Challenge Java EE !:NetBeans7.3.1でJava EE7とGlassFish4を少しのぞいてみました チラ| |д・)
- Programming Studio : GlassFish 4.0 正式リリース (GlassFish ユーザ・グループ・ジャパン副会長)
- yoshio3.com:Java EE 7 ローンチ (US 時間: 06/12 午前9時)
Java EE 7 対応関連書籍
Java EE 7 ローンチ (US 時間: 06/12 午前9時)
US 時間で 2013 年 6 月 12 日 午前 9 時より Java EE 7 のローンチ・イベントが開始します (日本では 2 回目のローンチ・イベントで 6 月 13 日 午後 1 時から参加頂けます)。Java EE 7 は、 2009 年にリリースされた Java EE 6 をベースに、4 つの新機能 (WebSocket, JSON, Batch, Concurrency Utilities for EE) と、既存の機能に対する大幅な更新 (JSF 2.2, JAX-RS, EL 3.0, JMS 2.0)を加えた、最新のエンタープライズ向け Java 標準技術です。
Java EE 7 に含まれる各種技術に触れたい場合、Java EE 7 の参照実装である GlassFish v4 を入手した上で、Java EE 7 のチュートリアルをご参照いただきお試しください。また、GlassFish v4.0 をバンドルし、 Java EE 7 の開発に対応した NetBeans 7.3.1 を入手いただく事で余計な設定は不要でいち早く Java EE 7 の開発を体験していただく事ができます。
Java EE 7 の新機能
- Web Socket API
- JSON Processing
- Batch
- Concurrency Utilities for Java EE
今日は、Java EE 7 の正式リリースを記念して、私が今まで様々な所で紹介してきた Java EE 7 のプレゼン資料をまとめ、また過去に発表した内容の中で、正式リリース時点で変更してしまった部分等も改めて見直し、最新情報にアップデートした資料を下記に共有します。全部で P165 ありますので、必要に応じて SlideShare よりダウンロードしてご参照ください。
本ブログをご参照頂いた皆様へ:
もしよろしければ、下記のアンケートにご協力頂けないでしょうか。今後の情報提供の際に役立てたいと思います。(※ アンケートにお応え頂くと他の方の結果をご覧頂けます。)
下記に Java EE 7 の技術のうち新規追加された 4 つの機能について簡単にご紹介します。
* Java API for WebSocket 1.0 (JSR-356):
WebSocket は RFC 6455 で定義された HTTP をアップグレードした TCP ベースのプロトコルで、双方向・全二重の通信が可能なプロトコルです。最新バージョンのブラウザであれば IE 10 を含む、ほぼ全てのブラウザが WebSocket に対応しています。JSR-356 は Java EE に含まれる技術ですのでアプリケーション・サーバ上で動作する WebSocket を実装する事はもちろんできますが、Java SE の環境上でも WebSocket のプログラムを実装するための API を提供しています。サーバ、クライアント共にクラスに対してアノテーション(@ServerEndpoint, @ClientEndpoint) を付加、もしくは javax.websocket.Endpoint クラスを継承したクラスを定義し実装します。クラス内には WebSocket の各ライフサイクルで必要な実装をそれぞれ、OnOpen, OnClose, OnError, OnMessage 内で実装します。
WebSocket はクライアントとサーバ間の通信において、HTTP のように通信時に余分なデータ(HTTP ヘッダ)を互いに送り合う事はなく通信コストを大幅に削減し、リアルタイム通信が必要な環境などで大きな注目を浴びている技術です。今まで、Comet, Reverse Ajax 等の技術を使って実装していたリアルタイムの情報配信システムにとっては、大幅にパフォーマンス向上が可能なWebSocket が標準の Java EE に含まれる事で、エンタープライズ環境でも安心して使う事ができます。
参考資料:
● Java EE 7新機能の目玉「WebSocket対応」、「バッチ処理」をアルン・グプタが解説──Java Day Tokyo 2013レポート
● Java EE 7 チュートリアル : WebSocket API
● DZone : JSR 356, Java API for WebSocket
● java.net WebSocket Spec
* Java API for JSON Processing (JSR-342)
JSON-P は JSON の処理(パース、生成、クエリなど)を Java で実施するための API を提供します。実装は、ストリーミング API もしくは オブジェクト・モデル API の2種類の方法のいずれかを利用することができます。
ストリーミング API は、イベント・ベースのパーサを利用して、発生するイベントに応じて JSON の解析処理、生成処理などのプログラミングを行う事ができます。開発者はコールバックイベントではなく、次に発生するイベントの処理を実装していきます。ストリーミング API を利用するためには、javax.json.stream パッケージ内の JsonParser や JsonGenerator を利用します。
オブジェクト・モデル API はメモリ上に JSON のデータをオブジェクトのツリー構造として表現し、開発者はこのツリーに対して解析や修正を行います。この方法はとても簡単でさらに直感的に扱う事ができますが、その一方で Streaming モデルよりも若干遅く、またメモリもより消費します。オブジェクト・モデル API を利用するためには、javax.json パッケージ内の JsonReader, JSonWriter, ビルダー(JsonObjectBuilder, JsonArrayBuilder)等を利用します。
参考資料:
● Java EE 7 チュートリアル:JSON
● java.net : JSON
● Java EE エキスパートグループ・メンバー(Markus Eisele):Test driving Java API for Processing JSON with GlassFish 4.0
● InfoQ :
JSON用標準JavaAPI
* Batch Application for the Java Platform (JSR-352)
バッチは、ユーザとのインタラクティブな操作が不要な処理を実行します。jBatch ではバッチ・ジョブの定義、実装方法を提供します。ジョブ仕様言語を XML で定義し 、バッチ関連アノテーション、インタフェースから構成されるビジネスロジックを実装します。またバッチ・コンテナでバッチ・ジョブの実行を管理します。
バッチ・ジョブはチャンク形式とタスク(Batchlet)形式のステップを含んでいます。
チャンク形式のステップはデータ・ソースからデータを読み込み、各データに対してビジネスロジックを適用し結果を保存します。
チャンク形式は3つの要素(入力、操作、出力)から構成されます。まず、はじめにデータ入力の部分では、データ・ソースから一度に一つのデータを読み込みます。たとえばデータベースのエントリやファイル、ディレクトリ、ログファイルに含まれるエントリ等がこれに当てはまります。次にビジネス・ロジックの適用部分では、アプリケーションで定義されたビジネス・ロジックを使用して一度に一つのデータの操作を行います。例えばデータのフィルタリング処理、データのフォーマットなどがこれに当てはまります。最後の出力部分では処理が行われたデータの保存を行います。データの入力では、javax.batch.api.chunk.ItemReader インタフェースを実装したクラスを定義します。データの処理には、javax.batch.api.chunk.ItemProcessor インタフェースを実装したクラスを定義します。出力ではjavax.batch.api.chunk.ItemWriter インタフェースを実装したクラスを定義します。
タスク形式のステップは、データ・ソースからデータを取得して処理を行うのではなく、任意のタスクを実行します。例えば、ディレクトリの作成、削除、ファイルの変更、データベース・テーブルの作成・破棄、リソース設定等がこれに当てはまります。タスク・ステップはチャンク形式とは異なり通常短時間で終わる処理を実装します。タスク形式のステップでは、javax.batch.api.chunk.Batchlet インタフェースを実装したクラスを定義します。
参考資料:
● Java EE 7新機能の目玉「WebSocket対応」、「バッチ処理」をアルン・グプタが解説──Java Day Tokyo 2013レポート
● Introducing JSR-352 – Batch Applications for the Java Platform
* Concurrency Utilities for Java EE (JSR-236)
Concurrency Utilities for Java EE はエンタープライズ環境における並列処理タスクの実装方法を提供します。Java EE 6 まではWeb コンテナ(JSP/Servlet)、もしくは EJB コンテナ(EJB)上から新しいスレッドを生成する事は禁止されていました。これは、これらのコンテナ上からスレッドを生成した場合、生成されたスレッドがコンテナ外部で動作しコンテナから生成されたスレッドを管理する事ができなくなるためです。Concurrency Utilities for Java EE ではコンテナ上で安心して新しいスレッドを生成する方法を提供します。
最も簡単に並列タスクを EE 環境で利用するためには、javax.enterprise.concurrent.ManagedExecutorService を利用します。ManagedExecutorService は Java SE の Concurrency Utilities で提供されるjava.util.concurrent.ExecutorService インタフェースを継承していますが ManagedExecutorService は Java EE の実行環境(つまりアプリケーション・サーバ)で実装されています。サーバ側の ManagedExecutorService の実装への参照を、自身のプログラム上で JNDI ルックアップ、もしくは @Resource アノテーションを使用してインジェクトし利用できるようにします。リソースをインジェクトした後は、Runnable, Callable インタフェースを実装したタスクを execute(), submit(), invokeAll(), invokeAny() 等のメソッドを呼び出して実行します。
参考資料:
● JSR 236 スペックリード:Anthony Lai の説明資料 (PDF) : Concurrency Utilities for Java EE
● Concurrency Utilities for EE の説明(私のブログ)
● ZDNet builder : マルチコア時代のCPUリソースを有効活用–Java EE 7で進化した並列処理を理解する
● @IT : エンタープライズ環境におけるマルチスレッド/並列処理の過去・現在・未来
● DZone : Modern concurrency and Java EE
以上が Java EE 7 で提供される新機能です。Java EE 7 の参照実装である GlassFish v4 をダウンロードして頂き、Java EE 7 の新機能を是非お試しください。
※ 下記に JSF 2.2 と EL 3.0 に関して説明していますが、これらは Java EE 6 から更新された技術です。
JavaServer Faces 2.2(JSR-344)
JSF は Java EE 環境におけるデフォルトの Web 開発フレームワークで世界中で幅広く使用されています。Java EE 6 の正式リリース時には JSF 2.0 が標準で提供され、その後、JSF 2.1 がリリースされました。
JSF 2.1 では JSP ベースのページを .jspx ファイルとする事で、Facelets として扱う事ができるようなり、JSF 1.2 で実装されたコードをマイグレーションしやすいような方法を抵抗した他、プラグイン可能な Facelet のキャッシュ機能を実装しています。また ServletContainerInitializer が追加されデフォルトの設定で問題ない場合、web.xml での設定は不要となりました。
そして、Java EE 7 では JSF 2.2 としてさらに大きな変更が加わりました。
* HTML 5 への対応(Pass-througu の属性が追加)
* 画面遷移における新機能の追加
Faces Flow
* ステートレス・モードの追加
* リソース・ハンドリング機能の改善
(マルチ・テンプレート機能は次のバージョンへ持ち越されました)
/contracts, META-INF/contracts にテンプレートのリソースを配置
* その他
この中で、特筆すべき事は JSF にステートレス・モードが追加された事です。JSF は 1.x のリリース当初から JSF における View の全てのステート(状態の情報)を保持していました。これはメモリも大量に消費し、パフォーマンスも良くありませんでした。そこで JSF 2.0 からは、部分的に状態を保存する Partial State Saving の機能が追加され、JSF 1.x の頃に比べパフォーマンスを改善する事ができました。そして JSF 2.2 からはついに状態を持たないステートレス・モードが JSF 2.2 に追加されました。これは現在の流行にそう形ですが、パフォーマンスが大幅に改善しメモリの使用量も減ります。またクラスタ環境で任意のノードに対してリクエストを送信する事が可能になります。今まで JSF をパフォーマンスの観点から見送っていた方々も JSF 2.2 の stateless (<f:view transient=”true”>)モードをご使用ください。
* Expression Language 3.0 (JSR-341)
EL は Java Beans のプロパティやメソッドとのバインディングに使用する式言語で、通常、JSP, JSF のビューから CDI で実装されたバッキング・ビーンのプロパティやメソッド呼び出しなどを結びつけるために使用します。
EL 3.0 では新たな構文が追加されています。新たに Java のコレクションに対して LINQ式を記述できるようになった他、Lambda 式も EL 式内に記述できるようになりました。ただし、EL における LINQ 式はコレクションに対する操作のみが許可されているため、直接 DB を操作する事はできません。また、EL 内で記述する Lambda 式は Java SE 8 で提供される Lambda 式の文法と同一ですが、動作環境は Java SE の環境には依存しません。つまり Java SE 7 の環境上でも EL 式内においてのみ Lambda 式を記載できるようになっています。
また EL は今まで Java EE の環境でしか動作しませんでしたが、EL 3.0 では Java SE 環境上でも動作するようになりました。
最後に、本日正式リリースされた Java EE 7 は Java EE 6 をベースに、プラスαの要素をご理解いただく事で、簡単に Java EE 7 に移行する事ができます。
GlassFish v4.0 正式リリース
Java EE 7 のローンチ・イベントを直前 (日本時間:6/13 午後1時) に控え、オラクル(GlassFish コミュニティ)は本日 2013年6月10日(日本時間:6 月 11 日) Java EE 7 の参照実装で Java EE 7 の仕様に完全準拠した、アプリケーション・サーバ GlassFish v4.0 を、世界で最も早くリリースしました。
glassfish.java.net の Web サイト・デザインも一新され各種項目へのリンクがわかりやすくなっています。是非、glassfish.java.net をご覧いただき、最新の GlassFish を入手してください。
GlassFish v4.0 の管理ガイドはコチラ
GlassFish v4.0 アプリケーション開発ガイドはコチラ
The Java EE 7 Tutorial ドキュメント
1点、下記の点にご注意頂きたいのですが、今回リリースされた製品は開発者がいち早く Java EE 7 の新機能や更新された機能を試すために提供された物で、本番環境で動作させるために必要な下記の機能は、今回のリリース時では十分に動作しない可能性があります。機能としては含まれていますが、Java EE 7 で追加された新機能を利用した場合に正しく動作しない可能性がありますので、まずは、本バージョンをご利用頂き、開発環境で Java EE 7 の新機能を試してください。そして将来提供される本番環境用の製品リリースまで Java EE 7 & GlassFish の API や操作方法について慣れて準備してください。
- クラスタ環境構築
- 高可用性機能
- アップグレード機能
- 組み込みサーバ
追記:誤解が発生する可能性がございますので下記を追記しました。
本日、RIとしてリリースされたバージョンは Java EE 7 で提供される各 API (各 JSR)が正しく動作する事を主に提供されています。つまりクラスタリング・高可用性機能など本番環境で動作させる為に必要な管理機能に対する十分なテストがされていません。そこで本番環境でのご適用はお控え頂き、近い将来十分にテストされたバージョンがリリースされた後に本番環境にご適用ください。ここで申し上げた内容は OSS 版、製品版の違いはございません。OSS 版と製品版の違いは、製品に対するサポートの有無(パッチ提供等を含む)、製品版でのみ利用可能な追加機能の有無などになります。クラスタリング機能、インメモリによる高可用性機能などは OSS 版、製品版共に提供予定です。
リリースノートに記載されている注意事項の原文は下記
Note : The main thrust of the GlassFish Server Open Source Edition 4.0 release is to provide an application server for developers to explore and begin exploiting the new and update technologies in the Java EE 7 platform. Thus, the following features of GlassFish Server were not a focus on this release :
* Clusters and standalone instances
* High availability features
* Upgrade
* Embedded Server
These features are included in the release, but they may not function properly with some of the new features added in support of the Java EE 7 platform.
上記以外にもいくつか、既知の問題があります、詳細はリリース・ノートをご参照ください。
既知の問題の例:
- ubuntu 環境でエラーを発生しインストールに失敗
- 空白を含むパスを使用した場合に package-appclient スクリプトで失敗
- OSGi のクラス・ローダの問題で JDK 8 を使用した場合、ロガー・リソースバンドルのルックアップに失敗
- その他(詳しくはコチラ)
ご注意(NOTE): WebSocket with Concurrency for EE
Java EE 7 のリリースが控えておりますが、先日 Java Day Tokyo で私のセッションの中でデモした WebSocket (JSR-356) と Concurrency Utilities for EE (JSR-236) を組み合わせたデモのコードについてご紹介すると共に、実装時の注意点をご報告いたします。
下記は、実際に私が WebSocket (JSR-356) と Concurrency Utilities for EE (JSR-236) を組み合わせたコードを実装する際にハマった内容をお届けします。
コードは下記のようなコードを記載しています。
package jp.co.oracle.websocket; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Resource; import javax.enterprise.concurrent.ManagedExecutorService; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import jp.co.oracle.tasks.WebSocketAIRSearchTask; import jp.co.oracle.tasks.WebSocketHotelSearchTask; @ServerEndpoint(value = "/asyncResult") public class AsyncResultWebSocketEndpoint { private static final Logger logger = Logger.getLogger(AsyncResultWebSocketEndpoint.class.getPackage().getName()); // Concurrency Utilities for EE の ManagedExecutorService をインジェクト @Resource(name = "concurrent/DefaultManagedExecutorService") ManagedExecutorService managedExecsvc; // WebSocket のコネクションがオープンした際の処理 @OnOpen public void initOpen(Session session) { executeTasks(session); } // WebSocket クライアントからメッセージを受信した際の処理 @OnMessage public void receivedMessage(String message, Session session) { if (!message.equals("re-execute")) { return; } executeTasks(session); } // 実際の処理内容 private void executeTasks(Session session) { // 複数タスクの実行の際に終わった順に処理結果を取り出す ExecutorCompletionService<String> execCompService = new ExecutorCompletionService<>(managedExecsvc); // 複数タスクの登録 List<Future<String>> futures = new ArrayList<>(); for (int i = 0; i < 100; i++) { WebSocketHotelSearchTask task = new WebSocketHotelSearchTask(i); futures.add(execCompService.submit(task)); } try { // 終了したタスクの順番に処理結果を取得し // 処理結果を WebSocket のクライアント・エンドポイント // に対して処理結果を送信 for (Future<String> results : futures) { String resultString = execCompService.take().get(); session.getBasicRemote().sendText(resultString); } } catch (IOException | InterruptedException | ExecutionException ex) { logger.log(Level.SEVERE, null, ex); } } }
ダミーのタスク
package jp.co.oracle.tasks; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; public class WebSocketHotelSearchTask implements Callable<String> { private static final Logger logger = Logger.getLogger(WebSocketHotelSearchTask.class.getPackage().getName()); private int counter; public WebSocketHotelSearchTask(int counter) { this.counter = counter; } // タスクの処理内容によっては時間のかかるタスクもあるため // 半分のタスクをわざと 4 秒待たせ、タスクの登録順にタスクが // 完了しないように作成 @Override public String call() { String result = ""; if (counter % 2 == 1) { Thread.sleep(4000); } result = "ホテル検索タスク完了 : 終了タスクの ID" + counter;; } catch (InterruptedException ex) { logger.log(Level.SEVERE, null, ex); } return result; } }
今回、なぜ下記のようにタスクの一括登録を行った後に、タスクの処理が終わった順に WebSocket のクライアント・エンドポイントに対してメッセージ配信を行うコードを実装したかというと(つまり、タスクの処理コード中から WebSocket のクライアントに対してメッセージ配信をしていない)、WebSocket 側のスレッドの制限があったためです。
// 複数タスクの登録 List<Future<String>> futures = new ArrayList<>(); for (int i = 0; i < 100; i++) { WebSocketHotelSearchTask task = new WebSocketHotelSearchTask(i); futures.add(execCompService.submit(task)); } try { // 終了したタスクの順番に処理結果を取得し // 処理結果を WebSocket のクライアント・エンドポイント // に対して処理結果を送信 for (Future<String> results : futures) { String resultString = execCompService.take().get(); session.getBasicRemote().sendText(resultString); }
当初、下記のように Runnable (Callable の call() 中で実装も可)のインタフェースを実装したタスクを作成し、タスクの処理の中で WebSocket のエンドポイントに対してメッセージを配信するコードを記載しました。例えば、下記のような感じです。
public class SomeMyTask implements Runnable{ Session session; public SomeMyTask(Session session){ this.session = session; } @Override void run(){ // 何らかの処理を実施 // タスクの処理の最後に、WebSocket のクライアント・エンドポイント // に対してメッセージを配信 session.getBasicRemote().sendText(resultString); } }
そして、下記のコードを書いてタスクを実行しました。
for( int i = 0 ; i < 10 ; i++ ){ SomeMyTask task = new SomeMyTask(session); managedExecsvc.submit(task); }
すると下記の例外が発生しました。
例外の出力内容:
java.lang.IllegalStateException: HeapBuffer has already been disposed at org.glassfish.grizzly.memory.HeapBuffer.checkDispose(HeapBuffer.java:802) at org.glassfish.grizzly.memory.HeapBuffer.position(HeapBuffer.java:188) at org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter.fillByteBuffer(TCPNIOAsyncQueueWriter.java:194) at org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter.writeComposite0(TCPNIOAsyncQueueWriter.java:151) at org.glassfish.grizzly.nio.transport.TCPNIOAsyncQueueWriter.write0(TCPNIOAsyncQueueWriter.java:80) at org.glassfish.grizzly.nio.AbstractNIOAsyncQueueWriter.processAsync(AbstractNIOAsyncQueueWriter.java:458) at org.glassfish.grizzly.filterchain.DefaultFilterChain.process(DefaultFilterChain.java:110) at org.glassfish.grizzly.ProcessorExecutor.execute(ProcessorExecutor.java:77) at org.glassfish.grizzly.nio.transport.TCPNIOTransport.fireIOEvent(TCPNIOTransport.java:838) at org.glassfish.grizzly.strategies.AbstractIOStrategy.fireIOEvent(AbstractIOStrategy.java:113) at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.run0(WorkerThreadIOStrategy.java:115) at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.access$100(WorkerThreadIOStrategy.java:55) at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy$WorkerThreadRunnable.run(WorkerThreadIOStrategy.java:135) at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:564) at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:544) at java.lang.Thread.run(Thread.java:722) |
なぜだろうと、Grizzly のソースコードを追ってみると、既にヒープが開放されてしまっているようです。
800 protected final void [More ...] checkDispose() { 801 if (heap == null) { 802 throw new IllegalStateException( 803 "HeapBuffer has already been disposed", 804 disposeStackTrace); 805 } 806 }
当初バグかとも思ったのですが念のため、WebSocket (JSR-356) 仕様のスレッド関連部分をチェックしてみました。すると下記の 5.1 に WebSocket に関するスレッドの注意書きが記載されておりました。
5.1 Threading Considerations Implementations of the WebSocket API may employ a variety of threading strategies in order to provide a scalable implementation. The specification aims to allow a range of strategies. However, the implementation must fulfill certain threading requirements in order to provide the developer a consistent threading environment for their applications. Unless backed by a Java EE component with a different lifecycle (See Chapter 7), the container must use a unique instance of the endpoint per peer. [WSC-5.1-1] In all cases, the implementation must not invoke an endpoint instance with more than one thread per peer at a time. [WSC-5.1-2] The implementation may not invoke the close method on an endpoint until after the open method has completed. [WSC-5.1-3] This guarantees that a websocket endpoint instance is never called by more than one container thread at a time per peer. [WSC-5.1-4] If the implementation decides to process an incoming message in parts, it must ensure that the corresponding onMessage() calls are called sequentially, and do not interleave either with parts of the same message or with other messages [WSC-5.1.5]. |
つまり、タスクの実行自身は複数のスレッドで実行できるのですが、WebSocket のクライアント・エンドポイントへのメッセージ送信は1箇所にまとめて実装しなければならない事に気付き上記のようなコードを書いています。
個人的には、マルチスレッドから WebSocket のクライアント・エンドポイントにメッセージ送信ができるようになるとより便利になるのではないかと思いますが、皆様如何でしょう? もちろん、既に仕様は FIX して Java EE 7 のリリース時点では無理ですし、Grizzly 等サーバ側の実装も今のままだと難しい部分があるかもしれません。しかし、皆様で声を上げていけば、時期 Java EE 8 の WebSocket 1.1 当たりで、マルチスレッドからのメッセージ送信もできるかも?!しれないので、賛同して頂ける方がいらっしゃったら、まとめて報告したいなと思っております。
(仕様上ダメって断られる可能性ももちろんありますが。(^_^;))
でも、昔と違って今の Java はこういった事がスペック・リードやエキスパート・グループメンバーに伝えやすい環境なんですよ!!
JJUG として Adopt-A-JSR プログラムに参加し、日本から改善要望なども出していけるといいですね。