技術シーンに、フレームワークとして登場した Struts は、最初から革命的でした。Struts は多くの Java™/J2EE プロジェクトに採用され、J2EE ベースのあらゆるアプリケーションにとって最も安定したフレームワークの 1 つとなりました。Struts でサポートされているのはコントローラーのみですが、それは M (モデル) および V (ビュー) 用のカスタム・コンポーネントを作成しようとする開発者にとっては、かえって好都合かもしれません。この記事では、Struts 2.0 の最も重要なコンポーネントの 1 つである OGNL (Object-Graph Navigation Language) とその機能について説明します。

Rudranil Dasgupta, Associate IT Architect, Advisory IT Specialist, GBS, IBM

Rudranil Dasgupta は IBM の associate IT アーキテクト兼 advisory-accredited IT スペシャリストであり、エレクトロニクス業界担当のプロジェクト・リーダーとして働いています。彼は設計開発、アーキテクチャー設計、技術戦略、クライアント・ソリューションにおけるアセット作成および活用、技術リーダーシップ提供に約 7 年間の経験があります。彼は WebSphere ソリューション、SOA、Web 2.0、コンテンツ管理、Java/J2EE に深い実践経験を積んできました。彼は IBM developerWorks など、いくつかの IBM フォーラムに記事を発表しています。彼は評価の高いフォーラムで Web 2.0 やソーシャル・コラボレーションについて何度か発表を行っています。



Kaushik Dutta, IBM Associate IT Architect, IBM

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 を使用してきました。



Sudip Dutta, IBM Senior System Engineer, IBM

Sudip Dutta はエレクトロニクス業界のシニア開発者です。彼は設計開発、プロトタイピング、技術戦略提供、クライアント・ソリューションにおけるアセット作成および活用ソリューション、技術リーダーシップ提供に約 7 年間の経験があります。彼は WebSphere、SOA、Web 2.0、Java/J2EE、.NET、DB2 などのソリューションに深い実戦経験を積んできました。彼はコンピューター・アプリケーションで修士号を、数学で学士号を取得しています。



2011年 3月 08日

はじめに

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. ポートフォリオの階層構造を示す図
データベース内のさまざまなデータ行の関係を示す図

Struts 1.x を使った実装で問題を解決する

リスト 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.

   }
}

Struts 1.x を使った方法の欠点

Web アプリケーションを作成する上で面倒な作業の 1 つに、フォーム Bean からデータ Bean へとデータを移し変える作業があります。文字列から Java の各型への変換、そしてその逆方向の変換により、作業はさらに複雑になります。文字列の値を解析して倍精度型や整数型にしたり、不適切なデータによって発生する例外を解決したりする必要があります。

データの移し変えと型変換はリクエスト処理サイクルの最初と最後で発生します。変換結果が描画される時には、データは Java の型から文字列のフォーマットに再変換されています。このプロセスは Web アプリケーションのほとんどすべてのリクエストで発生し、Web アプリケーションに不可欠な部分でもあります。一連の動的レコードを持つ動的フォームをサポートするためには複雑なコードとロジックが必要です。そうした論理的なコードの作成や保守は、複雑な作業になります。


Struts 2 と OGNL

Struts 2 の最も強力な機能の 1 つがデータの移し変えと型変換の自動化です。Struts 2 フレームワークでは、OGNL によって、データをリスト型やマップ型などの Java の複雑な型へと移し変えることができます。またカスタム・コンバーターを作成することで、型変換メカニズムを拡張し、ユーザー定義の型をはじめとする任意のデータ型を扱えるようにすることもできます。

OGNL は、Struts 2 フレームワークによる文字列ベースの HTTP 入出力と Java ベースの内部処理との間のインターフェースです。開発者は OGNL を十分に理解することで、効率を大幅に高めることや保守の大変さを大きく軽減することができます。

図 2. OGNL スタック
さまざまなコンテキスト要素が 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/リスト型

OGNL を使って変換の難題を解消する

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 フレームワークを使った方法の利点

Struts 2 フレームワークを使用する利点を以下に記載します。

  • OGNL の式言語により、フォーム・フィールドを Java のプロパティーにマッピングすることができます。
  • OGNL の型コンバーターにより、リクエスト・パラメーターの (文字列形式の) データが Java のプロパティーの実際の型に自動変換されます。
  • ビューが描画される間、式言語と型コンバーターにより、Java の型から文字列の値への自動変換が再度行われます。
  • コレクションや配列との間で、柔軟で広範な変換がサポートされています。
  • 複雑な Web サイトを容易に作成することができ、バックエンド・コードはそれほど複雑になりません。
  • 自動的なデータの移し変えを再利用することで、開発時間を短縮することができます。
  • データ変換や例外処理のための面倒なコードが大幅に削減され、中心となるビジネス・ロジックに集中することができます。

まとめ

この記事では Struts 2 の OGNL に関する概要と、ビジネス・アプリケーションをより効率的に実装する上でいかに OGNL が役立つかを説明しました。今後の記事では、開発の観点から Struts 2 を概説していきます。安定性が増すにつれ、Struts 2 は定着してきており、Struts 2 で強化された機能の利点を最大限に活かす上で、この記事が皆さんの役に立つことを祈っています。

参考文献

学ぶために

製品や技術を入手するために

議論するために

  • developerWorks blogs から developerWorks のコミュニティーに加わってください。
  • developerWorks コミュニティーで開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=644894
ArticleTitle=Struts 2.0 と OGNL
publish-date=03082011