2007年 2月初めにリリースされた Struts 2 は、画期的な機能を大量に備えていました。この記事では、Struts 2 フレームワークに追加された最新機能の 1 つである、OGNL (Object-Graph Navigation Language) について詳しく説明します。OGNL は新しい形式の式言語であり、コレクション、オブジェクト・グラフ・トラバーサル、索引付けに関する優れた機能を備えています。
この記事の詳細な内容としては、Struts 2 OGNL フレームワークの最小限のコードを使用して、階層構造を持つ Web ページからデータを抽出する方法や、このフレームワークの構成体を使用することによって、重要なビジネス概念がどのように Web に実装されるのかを説明します。また、従来のフレームワーク (Struts 1.x) では、階層構造を持つ Web ページからどのようにしてデータを抽出していたのか、さらには Struts 2 の OGNL フレームワークに移行することで、どれだけ開発作業を少なくすることができるのかを説明します。それに関連して、OGNL と Struts 2 の基本についても触れ、OGNL と Struts 2 を利用して軽量なコードを作成することで、階層構造のページを処理する方法も紹介します。最後に、Struts 1.x と比較した Struts 2 の利点を挙げ、コード量の少ない環境に移行する上で Struts 2 が役立つことを示します。
ここで、複数レベルの階層構造データを Web ページ上で表示、更新する以下のシナリオを考えてみましょう。投資アドバイザーが顧客用にレポートを準備しています。図 1 のスクリーンショットには、顧客の投資ポートフォリオ用データの更新に関する詳細図が示されています。この投資ポートフォリオの最上位レベルの属性は、投資家に関するデータの入力フィールドです。紫色の行は、ポートフォリオの配下にある第 1 レベルのデータ項目です。「Target investment amt (投資額)」フィールドと「Target profit % (収益率)」フィールドは第 1 レベルの属性です。同様に、緑色の行は第 2 レベルの項目で、そこに含まれるフィールドは第 2 レベルの属性です。最後に、黄色の行は第 3 レベルの項目を示しています。この記事では、すべてのフィールドが更新可能であるとし、ユーザーがページを送信した場合には、それに対応してバックエンドのデータを更新する必要があります。また、第 1、第 2、第 3 レベルの行の数字が動的であることにも注意してください。
図 1. ポートフォリオの階層構造を示す図
リスト 1 は上記の Web ページのデータ・モデルを示しています。
リスト 1. 従来のデータ構造
public class ClientPortfolioDetailsBean
{
//Domain model for the client portfolio details
//Portfolio level attributes
private String investorName = null;
private String emailId = null;
private String contactNumber = null;
//..... Other portfolio level attributes
//List to hold the details of Stocks / Mutual Funds etc investment category
private List <InvestmentCategoryDetailsBean> invCategoryList = null;
}
public class InvestmentCategoryDetailsBean {
//Bean to hold the information about investment category details
//like Stocks, Mutual Funds etc. Represents the first level rows
private String categoryName = null;
private double targetInvstAmt = 0.0;
private double expectedAnnualReturn = 0.0;
//Other attributes follows
//List of stocks and fund details bean to hold the
//second level items
private List <StocksAndFundDetailsBean> itemList = null;
}
public class StocksAndFundDetailsBean {
//Bean to hold the information about investment category details
//like Stocks, Mutual Funds etc. Represents the second level rows
//Attributes at that level
private String stockFundCategoryName = null;
private double targetInvstAmt = 0.0;
private double targetProfit =0.0;
//Other attribute follows
//List to hold the stock details
private List <StocksDetailsBean> stockDetailsList = null;
}
public class StocksDetailsBean {
//Represents the third level item details
//Details of individual stock/fund details
private String stockFundName = null;
private double noOfStocks = 0.0;
private double buyingPrice =0.0;
private Date buyingDate = null;
}
|
Struts 1.x の場合、上記シナリオに対するソリューションでは、フラットな単一のレベルでフォーム Bean を定義する必要があります。ユーザーの入力によって更新された値を取得するためには、リスト 2 に示すフォーム Bean の構造が必要です。
リスト 2. Struts 1.x でのフォーム Bean の構造
public class PortfolioDetailsBean extends ActionForm
{
//Top level attributes
private String investorName = null;
private String emailId = null;
//........... Other Top level attributes
//Fields to capture Attributes of First level rows
private double[] firstLevelTargetAmt = null;
private double[] firstLevelExpectedAnnlReturn = null;
//......... Other First Level row attributes
//Fields to capture Attributes of Second level rows
private double[] secondLevelTargetAmt = null;
private double[] secondLevelTargetInvPeriod = null;
//......... Other Second level row attributes
//Fields to capture Attributes of Second level rows
private double[] thirdLevelNoOfStocks = null;
private double[] thirdLevelByingPrice = null;
//......... Other Third level row attributes
}
|
このように単一レベルのフォーム Bean 要素を複数レベルのドメイン・モデル (リスト 1) に変換するためには、アクション・クラスにリスト 3 のようなコードを追加する必要があります。ここに適用されるロジックはドメイン・モデルの構造に依存し、レベルと属性数が増えるにつれてアクション・クラスでの変換が複雑になることに注意してください。
リスト 3. カスタム・コードにより、単一レベルのフォーム Bean 要素を複数レベルのドメイン・モデルに変換する
public class UpdatePortfolioAction extends Action
{
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
ActionErrors errors = new ActionErrors();
ActionForward forward = new ActionForward(); // return value
PortfolioDetailsBean portfolioDetailsBean = (PortfolioDetailsBean) form;
try {
//Counter to track index of 2nd level index
int counter2ndLevelIndex= 0;
//Converting the form bean to the domain model
ClientPortfolioDetailsBean clientBean = new ClientPortfolioDetailsBean();
//Setting the top level attributes
clientBean.setEmailId(portfolioDetailsBean.getEmailId());
//.........................................
//Similarly other top level fields are created
//Get the size of first level row sizes from session
int sizeInvestmentCategory = getInvestmentCategorySize
(request.getSession());
clientBean.setInvCategoryList(new ArrayList
<InvestmentCategoryDetailsBean>(sizeInvestmentCategory));
for(int firstLevelIndex=0;firstLevelIndex<
sizeInvestmentCategory;firstLevelIndex++)
{
//Create the updated first level bean
InvestmentCategoryDetailsBean invCatBean =
new InvestmentCategoryDetailsBean();
//Set the investment category Bean in the parent bean
clientBean.getInvCategoryList().add(invCatBean);
//set the updated properties
invCatBean.setExpectedAnnualReturn(
portfolioDetailsBean.getFirstLevelExpectedAnnlReturn()[firstLevelIndex]);
//.........................................
//Set the other properties at first level
//Get the size of 2nd level row sizes from session
int sizeStockAndFundDetails = getStockAndFundDetailsSize
(request.getSession(),firstLevelIndex);
invCatBean.setItemList(
new ArrayList<StocksAndFundDetailsBean>(sizeStockAndFundDetails));
//Fill Up the second level objects
for(int secondLevelIndex=0;secondLevelIndex<
sizeStockAndFundDetails;secondLevelIndex++)
{
StocksAndFundDetailsBean stkFundDetails =
new StocksAndFundDetailsBean();
//Set the object to the parent Bean
invCatBean.getItemList().add(stkFundDetails);
//Set the individual attributes
stkFundDetails.setTargetInvstAmt(
portfolioDetailsBean.getSecondLevelTargetAmt()[counter2ndLevelIndex++]);
//............... Other fields follows
// Setting up of 3rd level list follows here
//......................................
}
}
} catch (Exception e) {
// Report the error using the appropriate name and ID.
}
}
|
Web アプリケーションを作成する上で面倒な作業の 1 つに、フォーム Bean からデータ Bean へとデータを移し変える作業があります。文字列から Java の各型への変換、そしてその逆方向の変換により、作業はさらに複雑になります。文字列の値を解析して倍精度型や整数型にしたり、不適切なデータによって発生する例外を解決したりする必要があります。
データの移し変えと型変換はリクエスト処理サイクルの最初と最後で発生します。変換結果が描画される時には、データは Java の型から文字列のフォーマットに再変換されています。このプロセスは Web アプリケーションのほとんどすべてのリクエストで発生し、Web アプリケーションに不可欠な部分でもあります。一連の動的レコードを持つ動的フォームをサポートするためには複雑なコードとロジックが必要です。そうした論理的なコードの作成や保守は、複雑な作業になります。
Struts 2 の最も強力な機能の 1 つがデータの移し変えと型変換の自動化です。Struts 2 フレームワークでは、OGNL によって、データをリスト型やマップ型などの Java の複雑な型へと移し変えることができます。またカスタム・コンバーターを作成することで、型変換メカニズムを拡張し、ユーザー定義の型をはじめとする任意のデータ型を扱えるようにすることもできます。
OGNL は、Struts 2 フレームワークによる文字列ベースの HTTP 入出力と Java ベースの内部処理との間のインターフェースです。開発者は OGNL を十分に理解することで、効率を大幅に高めることや保守の大変さを大きく軽減することができます。
図 2. OGNL スタック
開発者の観点から見ると、OGNL には以下の 2 つのコンポーネントがあります。
- 式言語 — 通常はフォーム入力フィールドの名前や JSP タグに使用されます。OGNL 式を使用することで、Java のデータのプロパティーをテキスト・ベースのビュー・レイヤーの文字列にバインドすることができます。
- 型コンバーター — データ型を変換します。Java 環境との間でデータがやりとりされるたびに、HTML 内に文字列として含まれるデータと、そのデータに対応する Java のデータ型との間でデータ変換を行う必要があります。Struts 2 フレームワークには、これまで求められてきた型変換の機能を遥かに上回る能力を備えたコンバーターが組み込まれているだけでなく、開発者がカスタム・コンバーターを作成することもできます。
Struts 2 フレームワークでは、リクエスト・パラメーターのデータを自動的に移し変えて型変換を行いますが、そのデータはどこに移し変えられ、その場所を OGNL はどのようにして見つけ出すのでしょう?そこで登場するのが Struts 2 の ValueStack です。ValueStack は、各オブジェクトがそれぞれに持つプロパティーを 1 つの仮想オブジェクトのプロパティーとして寄せ集めた構成体です。プロパティーが重複する場合 (つまりスタック内で 2 つのオブジェクトが employeeId プロパティーを持つ場合など) には、スタック内で最上位にあるオブジェクトのプロパティーが使用されます。Struts 2 フレームワークには、HTTP ネイティブの文字列と以下に挙げる Java の型との間の変換機能が組み込まれています。
- 配列型
- boolean/ブール型
- char/文字型
- int/整数型、float/浮動小数点型、long/長整数型、double/倍精度型
- Date/日付型
- String/文字列型
- Map/マップ型
- List/リスト型
Struts 2 の OGNL 式により、それほど苦労することなく、単一レベルのフォーム Bean を複数レベルのドメイン・オブジェクトに変換する複雑なコードを作成することができます。Struts 2 では HTML の入力フィールド名を OGNL 式として生成できるため、アクション・クラスで複雑なコードを作成する必要がなくなります。例えば HTML の入力フィールド名 (テキスト・ボックスなど) が clientPortfolioDetailsBean.investorName に設定されている場合には、アクション・クラスの getClientPortfolioDetailsBean() が呼び出されます。それによって ClientPortfolioDetailsBean が返され、続いてユーザーが画面上で入力した値を引数に取る setInvestorName() メソッドが呼び出されます。ユーザーによって更新される HTML 入力フィールドにはインテリジェントな名前が付けられるため、アクション・クラスで複雑なコードを作成する必要がありません。ドメイン・モデルにはアクション・クラスの中でデータが適切に追加されます。
アクション・クラスの中では、ClientPortfolioDetailsBean という型の属性と、この属性の get メソッドを追加する必要があります。現在のアクション・クラスのインスタンスの中で Bean のインスタンスが設定されていない場合には、その get メソッドの中でセッションから Bean を返す必要があります。ページ・ロードの際、クライアントによって詳細が設定される Bean インスタンスをセッション・オブジェクトの中に保持する必要があります。さらに大きなオブジェクトの場合には、データベースからデータを取得する getClientDetailsFromDB() によって getClientDetailsFromSession() メソッドを置き換えることができます。
リスト 4. OGNL 式を使ったアクション・クラス
public class ClientPortfolioAction extends ActionSupport {
private ClientPortfolioDetailsBean clientDetailsBean = null;
//Accessor for the clientDetailsBean.
public ClientPortfolioDetailsBean getClientDetailsBean() {
if(clientDetailsBean==null)
{
//If the bean is null for this action class instance
//load the data from the session
clientDetailsBean = getClientDetailsFromSession();
}
return clientDetailsBean;
}
}
|
リスト 5. OGNL 式に従って生成される JSP ページ
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:textfield name="clientDetailsBean.investorName"
value="%{clientDetailsBean.investorName}"/%>
<s:textfield name="clientDetailsBean.emailId"
value="%{clientDetailsBean.emailId}"/%>
<s:textfield name="clientDetailsBean.contactNumber"
value="%{clientDetailsBean.contactNumber}"/%>
<s:iterator value="clientDetailsBean.invCategoryList"
var="invCatDetails" status="firstLevelIdx"%>
<s:property value="%{#invCatDetails.categoryName}"/%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.targetInvstAmt"
value="%{invCatDetails.targetInvstAmt}"/%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.expectedAnnualReturn"
value="{invCatDetails.expectedAnnualReturn}"/%>
<s:iterator value="#invCatDetails.itemList"
var="stkFundDetails" status="secondLevelIndx"%>
<s:property value="%{#stkFundDetails.
stockFundCategoryName}"/%>
<s:textfield name="clientDetailsBean
[%{#firstLevelIdx.index}].itemList[%{secondLevelIndx.index}].
targetInvstAmt" value="%{#stkFundDetails.targetInvstAmt}" /%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].
targetProfit" value="%{#stkFundDetails.targetProfit}" /%>
<s:iterator value="#stkFundDetails.stockDetailsList"
var="stkDetails" status="thirdLevelIndx"%>
<s:property value="%{stkDetails.stockFundName}" /%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%{thirdLevelIndx.index}].noOfStocks" value="%{#stkDetails.noOfStocks}" /%>
<s:textfield name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%{thirdLevelIndx.index}].buyingPrice" value="%{#stkDetails.buyingPrice}"/%>
</s:iterator>
</s:iterator>
</s:iterator>
|
ここで、入力テキスト・ボックスの名前が有効な OGNL 式に従って生成される上記 JSP ページについて検討を行い、OGNL 式によってどのように「ABC incorporated (ABC 社)」の「No. of stocks (株数)」フィールドが更新されるのかを見てみます。
<s:textfield
name="clientDetailsBean[%{#firstLevelIdx.index}]
.itemList[%{secondLevelIndx.index}].stockDetailsList
[%thirdLevelIndx.index}].noOfStocks}'/> |
この行は以下の HTML コードに変換されます。
<input type="text"
name="clientDetailsBean[0].itemList[0].stockDetailsList[0].noOfStocks"
value="<somevalue>" /> |
OGNL 式 clientDetailsBean[0].itemList[0].stockDetails[0].noOfStocks は、アクション・クラスのインスタンスの getClientDetailsBean().getItemList().get(0).getStockDetailList(0).get(0).setNoOfStocks(<some value>) というコードを呼び出すのと同じです。同様に他のフィールドもドメイン・モデルの中で更新されます。アクション・メソッドの中で getClientDetailsBean() が呼び出されると、すべてのフィールドが Struts 2 フレームワークによって更新されます。そのため、アクション・クラスのコードを (Struts 1 の例のように) 複雑にすることなく、フォーム Bean をドメイン・モデル・オブジェクトに変換することができます。
Struts 2 フレームワークを使用する利点を以下に記載します。
- OGNL の式言語により、フォーム・フィールドを Java のプロパティーにマッピングすることができます。
- OGNL の型コンバーターにより、リクエスト・パラメーターの (文字列形式の) データが Java のプロパティーの実際の型に自動変換されます。
- ビューが描画される間、式言語と型コンバーターにより、Java の型から文字列の値への自動変換が再度行われます。
- コレクションや配列との間で、柔軟で広範な変換がサポートされています。
- 複雑な Web サイトを容易に作成することができ、バックエンド・コードはそれほど複雑になりません。
- 自動的なデータの移し変えを再利用することで、開発時間を短縮することができます。
- データ変換や例外処理のための面倒なコードが大幅に削減され、中心となるビジネス・ロジックに集中することができます。
この記事では Struts 2 の OGNL に関する概要と、ビジネス・アプリケーションをより効率的に実装する上でいかに OGNL が役立つかを説明しました。今後の記事では、開発の観点から Struts 2 を概説していきます。安定性が増すにつれ、Struts 2 は定着してきており、Struts 2 で強化された機能の利点を最大限に活かす上で、この記事が皆さんの役に立つことを祈っています。
学ぶために
- Struts 2 と Internet Stats について学んでください。
- Struts 2.Net を訪れてください。
- developerWorks podcasts ではソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
- developerWorks の Technical events and webcasts で最新情報を入手してください。
- Twitter で developerWorks をフォローしてください。
- IBM オープンソース開発者にとって関心のある、世界中で今後開催される会議や業界展示会、ウェブキャスト、その他のイベントについて調べてみてください。
- developerWorks の Open source ゾーンをご覧ください。オープンソース技術を使った開発や、IBM 製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報、最も人気のあった記事やチュートリアルの一覧など、豊富な情報が用意されています。
- IBM とオープンソース技術、そして製品機能を調べ、学ぶために、無料の developerWorks On demand demos をご覧ください。
製品や技術を入手するために
- 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
- IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。
議論するために
- developerWorks blogs から developerWorks のコミュニティーに加わってください。
- developerWorks コミュニティーで開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。
Rudranil Dasgupta は IBM の associate IT アーキテクト兼 advisory-accredited IT スペシャリストであり、エレクトロニクス業界担当のプロジェクト・リーダーとして働いています。彼は設計開発、アーキテクチャー設計、技術戦略、クライアント・ソリューションにおけるアセット作成および活用、技術リーダーシップ提供に約 7 年間の経験があります。彼は WebSphere ソリューション、SOA、Web 2.0、コンテンツ管理、Java/J2EE に深い実践経験を積んできました。彼は IBM developerWorks など、いくつかの IBM フォーラムに記事を発表しています。彼は評価の高いフォーラムで Web 2.0 やソーシャル・コラボレーションについて何度か発表を行っています。
Kaushik Dutta はアプリケーション・アーキテクトであり、現在は GBSC Asset Reuse Enablement Initiative の技術アーキテクトとして働いています。彼はソフトウェア・ソリューションの設計、開発、分析、アーキテクチャー設計に 10 年を超える専門経験があります。彼は Web アプリケーションの分析、アーキテクチャー、システム設計と開発に従事してきており、その中で、Java 技術、J2EE、EJB、SOA、Web 2.0、WebSphere Adapter、JMS、Web サービス、JSP、サーブレット、JDBC、RMI、XML、Oracle、HTML、JavaScript を使用してきました。