BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Struts 2 への移行 (パートII)

Struts 2 への移行 (パートII)

ブックマーク

このシリーズのパートIでは(source) 、(Struts開発者向けに)全体のアーキテクチャ、基本的な要求の流れ、構成の意味、および新しい Struts 2(以前のWebWork)とStruts1のアクションフレームワークの違いを説明しました。知識さえ装備しておけば、どのような規模のアプリケーションでもStrutsからStruts2に簡単に移行することができます。

このシリーズのパート IIでは、アクションの変換を中心に説明を進めます。必要となるコード変更を詳しく説明する前に、まずは状況を設定します。最初に、変換するサンプルのアプリケーション、およびStrutsとStruts2の両バージョンで使用される共通コンポーネントについて説明します。

そこから、Struts2コードに変換するという観点でStrutsアプリケーションコードを再確認します。最後に、構成の変更についても取り上げます。

サンプルアプリケーション

今回は、説明を単純にするために、多くの人がよく知っている「ブログ」をサンプルとします。このようなサンプルは、単純で、よく使用されているので(SunのPet Storeほどではないとしても)、説明は必要ないでしょう。

機能をより詳細に定義するため、次のようなユース ケースを使用します。

  1. 新規のブログエントリを追加する。
  2. ブログエントリを表示する。
  3. ブログエントリを編集する。
  4. ブログエントリを削除する。
  5. すべてのブログエントリを一覧する。

さらに細かく分解してみると、実装する機能は、多くのWebアプリケーションに共通のものとなります。機能は、作成(create)、読み取り(read)、更新(update)、および削除(delete)、つまりよく言われるCRUDで構成されます。これらのステップを簡単にすることは、生産性の大幅な向上につながります。

また、StrutsとStruts2のアプリケーションには共通するコンポーネントもあります。それは、バックエンドビジネスサービスです。次のようなものです。

public class BlogService {
private static List blogs = new ArrayList();
public List list() { ... }
public Blog create(Blog blog) { ... }
public void update(Blog blog) { ... }
public void delete(int id) { ... }
public Blog findById(int id) { ... }
}

このオブジェクトは、ここでのサンプルのユースケースをサポートします。実装を簡略化するため、StrutsおよびStruts2の両方のアクションでこのオブジェクトをインスタンス化します。これは、実際のアプリケーションでは不要な結合を生むことになりますが、Web 層に注目する今回のサンプルとしては問題ありません。

補足:パート Iでは、Struts2 アクションで使用される依存性注入のインターフェイス注入スタイルについて説明しました。これは、サーブレット関連のオブジェクトインスタンス (HttpServletRequest、HttpServletResponse、PrincipalProxyなど)の注入に最もよく使用されるスタイルですが、これが唯一のスタイルではありません。

Struts2 は、デフォルトのコンテナとして Spring Frameworkを使用します。この場合、依存性注入にはsetterメソッドが使用されます。アクションにsetter(以下を参照) を追加することで、Struts2フレームワークは、Spring Frameworkコンテキストから正しいサービスを受け取り、setterを経由してそれをアクションに適用できるようになります。

public void setBlogService(BlogService service) {
this.blogService = service;
}

インターフェイス注入のスタイルの場合と同様に、インターセプタ(ActionAutowiringInterceptor インターセプタ)をアクションのインターセプタの集合の中に含める必要があります。これにより、アクションが呼び出される前に、Spring Frameworkによって管理されるビジネスオブジェクトがStruts2アクションに注入されます。さらに構成パラメータによって、setterとビジネスオブジェクトとのマッチングをどのように行うか(名前や型によって判断するのか、または自動か) を設定することもできます。

Strutsアプリケーションのコード

まずは、Strutsの実装から始めます。ユースケースごとに1つのアクションクラスを実装し、さらに、すべてのアクションが必要に応じて再使用するアクションフォームのクラスも実装します。これは、このアプリケーションにとって最もエレガントな手段とは言えないかもしれません (他の手段としては、動的フォームの使用、または要求ディスパッチアクションなどが考えられます)。しかし、今回の方法は、すべてのStruts開発者にとって馴染みの深いものであるはずです。複雑でない実装の変換を学ぶことで、より高度な実装に移行するために必要となる、Struts2フレームワークに関するスキルと知識を習得しましょう。

パートIでは、StrutsとStruts2のアクションの違いについて説明しました。ここでは、UMLの視点から違いを説明します。一般的なStrutsアクションのフォームは、次のようになります。


アクションフォームは複数のアクションで使用されるので、まず、これについて説明します。

public class BlogForm extends ActionForm {

private String id;
private String title;
private String entry;
private String created;

// public setters and getters for all properties
}

UMLに示されているように、フォームはActionFormクラスを継承しなければならないという制限があります。また、フィールドはStringクラスでなければならないという制限もあります。このため、getterはStringを戻し、setterはStringを受け入れます。

アクションフォームを使用するアクションは、表示(View)、作成(Create)、および更新 (Update)のアクションです。

Viewアクション:

public class ViewBlogAction extends Action {

public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {

BlogService service = new BlogService();
String id = request.getParameter("id");
request.setAttribute("blog",service.findById(Integer.parseInt(id)));

return (mapping.findForward("success"));
}
}

Createアクション:

public class SaveBlogEntryAction extends Action {

public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {

BlogService service = new BlogService();
BlogForm blogForm = (BlogForm) form;
Blog blog = new Blog();
BeanUtils.copyProperties( blog, blogForm );

service.create( blog );

return (mapping.findForward("success"));
}
}

Updateアクション:

public class UpdateBlogEntryAction extends Action {

public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {

BlogService service = new BlogService();
BlogForm blogForm = (BlogForm) form;

Blog blog = service.findById( Integer.parseInt(blogForm.getId()));
BeanUtils.copyProperties( blog, blogForm );
service.update( blog );
request.setAttribute("blog",blog);

return (mapping.findForward("success"));
}
}

これら3つのアクションはすべて次のような1つのパターンに従っています。

  • ビジネスオブジェクトインスタンスを作成する - 前述のとおり、今回は、アクションからビジネスオブジェクトを使用する手段の中で最も単純なものを採用しています。つまり、各アクションで新しいインスタンスを作成します。
  • データを要求から取得する - これが行われるのは、2つの形式のうちの1つです。View アクションでは、idがHttpServletRequestオブジェクトから直接的に取得されます。Createアクション、およびUpdateアクションでは、ActionFormが使用されます。ActionFormは、HttpServletRequestのメソッドと非常に似ています。唯一の違いは、複数のフィールドが1つのオブジェクトにグループ化されていることです。
  • ビジネスオブジェクトを呼び出す - ここで、ビジネスオブジェクトを使用します。パラメータが単純なオブジェクトの場合(View アクションの場合)は、パラメータは、正しい型に変換(StringからInteger)してから使用します。オブジェクトがより複雑なドメインオブジェクトの場合は、BeanUtilオブジェクトを使用してActionFormを変換する必要があります。
  • 戻りデータを設定する - データを戻してそれをユーザーに表示する必要がある場合、そのデータは HttpServletRequestオブジェクトの属性として設定する必要があります。 
  • ActionForwardが戻る - どのようなStrutsアクションにおいても、最後のステップでは、ActionForwardオブジェクトを検索して返します。

最後の2つのアクション(削除と一覧のアクション)は、少し異なります。Remove(削除)アクションは、BlogFormクラスを使用しません(以下を参照)。このアクションは、idという要求の属性を使用することにより(Viewアクションと同様)、ビジネスオブジェクトを使用して必要な処理を実行することができます。この後の構成の部分で説明しますが、このアクションは、データを一切戻しません。レコードが削除された後のsuccessという結果は、必要な情報を取得するList(一覧)アクションの実行に関連付けられています(アクションの分離が考慮されています)。

public class RemoveBlogEntryAction extends Action {

public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {

BlogService service = new BlogService();
String id = request.getParameter("id");
service.delete(Integer.parseInt(id));

return (mapping.findForward("success"));
}
}

List アクションは、ユーザからの入力がないという点で他のアクションとは異なります。このアクションは、単純に、引数なしのメソッドでビジネスサービスを呼び出し、そのサービスが戻すドメインオブジェクトのリストをユーザに返します。次のようになります。

public class ListBlogsAction extends Action {

public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {

BlogService service = new BlogService();
request.setAttribute("bloglist",service.list());

return (mapping.findForward("success"));
}
}

Struts2への変換

Struts2では、このサンプルのアプリケーションを実装する方法が多数あります。Struts と同様にユースケースごとにクラスを用意する方法や、クラス階層を作成する方法、あるいは、すべてのユースケースを単一のアクションクラスで扱う方法などがあります。ここで説明する方法は、最も適切と思われるもので、CRUD機能を単一のクラスで実装する方法です。

ただし、一覧のユースケースは分離します。一覧を同じクラスに組み込んだ場合は、各ユースケースで使用されないクラス属性が発生します(一覧ユースケースにおけるBlogクラス、および他のすべてのユースケースにおけるBlogクラスのリスト)。これらは混乱の原因となる可能性があります。

Struts2の場合のすべてのアクションのクラスを、単一のUMLモデルで表すと次のようになります。


各ユースケースは、アクションのメソッドとして実現されます。上記のUML図から、BlogActionに、save、update、およびremoveのメソッドがあることがわかります。view メソッドは、executeメソッドを使用して実装されます。これは、ActionSupportクラスから継承されます。同様に、ListBlogActionでは、executeメソッドを使用して一覧ユースケースが実装されます。

簡略化するため、この図では BlogActionの3つのインターフェイスを省略しています。それらは、ServletRequestAwareインターフェイス、Prepareableインターフェイス、およびModelDrivenインターフェイスです。

最初のインターフェイス ServletRequestAwareについては、パートIで詳しく説明しました。このインターセプタを使用することにより、アクションでHttpServletRequestオブジェクトにアクセスすることが可能になります。これにより、オブジェクトを要求に戻して JSPで表示することができます。

次のPreparable インターフェイスは、PrepareInterceptorと連携して機能します。これらの2つを組み合わせて使用することで、execute メソッドの前に呼び出されるprepareメソッドを提供できます。これにより、セットアップ、構成、またはアクションへの事前設定のコードが可能になります。

この例では、prepareメソッドで、blogId フィールドの値をチェックして、新規のブログか既存のブログかを判断します。ゼロ以外の値の場合は、ブログモデルが取得されアクションに設定されます。

次は、ModelDrivenインターフェイスです。前の記事で、最も大きな違いの1つとして、Strutsではアクションがスレッドセーフである必要があり、Struts2ではそのような制限がないと説明しました。アクションは、要求ごとにインスタンス化されて呼び出されます。Struts2でこの制限がなくなったことにより、アクションクラスで、クラスレベルの属性およびメソッドを活用できるようになりました (具体的にはgetterとsetterです)。これとインターセプタの機能を組み合わせることで、HttpServletRequest の属性をアクションから直接的に設定できるようになります。

次のようにします。

  • HTTP要求の属性名のリストを繰り返します。
  • 現在の属性の名前に対応するsetterを現在のアクションで検索します。
  • その属性名の値を、HttpServletRequestから取得します。
  • 値を、アクションのsetterにとって正しい型に変換します。
  • 変換した値を、setterを経由してアクションに適用します。

この機能を提供するインターセプタが、ParametersInterceptorインターセプタです。

ヒント:アクションを開発している際、何らかの理由で値が正常に設定されない場合は、まず、このインターセプタが、アクションに適用されているインターセプタの集合の中に含まれているかを確認することをお勧めします。

実際には、どのような場合にもこうした確認は有効です。問題をデバッグしている際、何かが正常でなかったり、想定したとおりに機能しなかったりする場合は、インターセプタが関連していることがよくあります。どのようなインターセプタが適用されているか、また、どのような順序で適用されているかを確認します。インターセプタは、想定外のところで相互に干渉する場合があります。

ここまでで、文字列ベースのフォームまたは要求の属性をアクションに適用することができるようになりました。次のステップでは、属性を、ドメインオブジェクトや値、転送のオブジェクトのフィールドに適用します。これは、簡単にできます。実際に開発者が行うべきことの中で異なる点は、ModelDriven インターフェイス(単一メソッド getModel() を備えます)を実装し、ModelDrivenInterceptorがアクションに適用されていることを確認するだけです。

アクションのsetterを検索する代わりに、最初にモデルを取得し、そのモデルが属性名に一致するsetterを備えていないかをチェックします。一致するsetterがモデル オブジェクトに存在せず、アクションに存在する場合、値はアクションに設定されます。このような柔軟性は、BlogAction、およびBlogモデルオブジェクトのフィールドで実際に確認することができます。アクションにsetId()メソッドが存在しています。このため、オブジェクトのフィールドの値が、取得されたBlogインスタンスで直接的に設定される前に、ブログの idがアクションに設定され、それがprepareメソッドで使用されて、正しいBlogインスタンスが事前に取得されます。

これらの2つの機能があることにより、アクションで呼び出されるメソッドの実装は簡単になります。特定のビジネスサービスを呼び出し、表示するデータをHttpServletRequest に設定するだけです。

public class BlogAction extends ActionSupport
implements ModelDriven, Preparable, ServletRequestAware {

private int blogId;
private Blog blog;
private BlogService service = new BlogService();
private HttpServletRequest request;

public void setServletRequest(HttpServletRequest httpServletRequest) {
this.request = httpServletRequest;
}

public void setId(int blogId) {
this.blogId = blogId;
}

public void prepare() throws Exception {
if( blogId==0 ) {
blog = new Blog();
} else {
blog = service.findById(blogId);
}
}

public Object getModel() {
return blog;
}

public String save() {
service.create(blog);
return SUCCESS;
}
public String update() {
service.update(blog);
request.setAttribute("blog",blog);
return SUCCESS;
}

public String remove() {
service.delete(blogId);
return SUCCESS;
}

public String execute() {
request.setAttribute("blog",blog);
return SUCCESS;
}


}

最後は、一覧アクションです。これも、表示するデータを提供するためにHttpServletRequestオブジェクトにアクセスする必要があります。つまり、ServletRequestAwareインターフェイスを実装する必要があります。しかし、このアクションはユースケースを実行するために入力を使用しないため、追加のインターフェイスは必要ありません。実装は、次のようになります。:

public class ListBlogsAction extends ActionSupport implements ServletRequestAware {

private BlogService service = new BlogService();
private HttpServletRequest request;

public void setServletRequest(HttpServletRequest httpServletRequest) {
this.request = httpServletRequest;
}

public String execute() {
request.setAttribute("bloglist",service.list());
return SUCCESS;
}

}

これで、アクションコードの実装は完了です。シリーズの最後では、このアクションを新しいStruts2ユーザインターフェイスと連携させて、さらに簡略化します。

アクションの構成

ブラウザからアクションの何かを呼び出すためには、それらを構成しておく必要があります。これは、XML構成ファイルで行います。

Strutsの場合は、WEB-INFディレクトリのstruts-config.xmlという名前のファイルを使用します。このファイルでは、2つの要素(アクションフォームおよびアクション自体)を構成する必要があります。Struts2の構成は少し複雑で、classesディレクトリの struts.xml という名前のファイルを使用します。ここでは、アクションに加えて、インターセプタも構成する必要があります。

Struts構成の form-beansノードは単純です。これには属性として、固有名(name)(開発者によって指定される)と型(type)があります。type には、ActionForm クラスのパッケージと名前を指定します。

<struts-config>

<form-beans>
<form-bean name="blogForm"
type="com.fdar.articles.infoq.conversion.struts.BlogForm"/>
</form-beans>
...

</struts-config>

サンプルのアプリケーションのアクションは、3つの方法で構成されています。

1. リダイレクト構成

この構成では、アクションクラスは使用されません。要求は、バックエンド処理なしでJSPに転送されます。

Strutsの設定では、各マッピングに、アクションを呼び出すURLにマップされるpath要素があります。たとえば、/struts/addというpathは/struts/add.doというURLにマップされます。また、転送先のURLを指定するforward属性もあります。この例では、/struts/add.jspに転送します。

<struts-config>
...

<action-mappings>

<action path="/struts/add" forward="/struts/add.jsp"/>
...

</action-mappings>
</struts-config>

Struts2の設定には、より多くの構造があります。

<struts>
<include file="struts-default.xml"/>

<package name="struts2" extends="struts-default" namespace="/struts2">

<action name="add" >
<result>/struts2/add.jsp</result>
</action>
...

</package>
</struts>

まず、action-mappingsノードの代わりに、includeノード、および packageノードがあります。Struts2では、設定を任意の数のファイルに分割することで設定をモジュール化できます。各ファイルは、名前が違うだけで、構造はまったく同じです。

includeノードは、ファイルの名前を示すfile属性を使用して、外部ファイルの内容を現在のファイルに挿入します。packageノードは、複数のアクションをグループ化します。これのname属性には一意の値を指定する必要があります。

Strutsのアクション設定では、path 属性にURL全体を指定します。Struts2では、URLは、packageノードのnamespace属性、actionノードのname属性、およびアクションの拡張子(デフォルトでは.action)を連結したものになります。つまり、上記のアクションは、/struts2/add.actionで呼び出されます。

packageノードの最後に説明する属性は、extends属性です。パッケージは、名前空間の分離を提供するだけでなく、構造も提供します。別のパッケージを継承(extend)することにより、その設定(アクション、結果、インターセプタ、および例外など)にアクセスすることができます。上記のstruts2パッケージは、struts-defaultパッケージを継承しています。このパッケージは、インクルードされるファイルtruts-default.xmlで定義されています。このインクルードファイルは、マスタインクルードファイルであり、すべての設定で最初の行に置かれる必要があります。これにより、使用できる結果の型、インターセプタ、および共通のインターセプタについてのデフォルトの設定がすべて提供されるため、それぞれを入力する手間が省かれます。

最後に、resultノードがあります。これは、転送先のURLを値に指定します。ここでは、name 属性、およびtype属性は省略しています。これらは、デフォルト値を変更しない限り、設定を簡潔にするために省略できます。デフォルト値は、アクションから戻される successの結果についてJSPを表示します。

2. アクション設定

バックエンド処理を提供するためにアクションクラスが呼び出されます。処理からの結果は設定で定義され、ユーザーは対応のビューにリダイレクトされます。

これは、リダイレクト設定から少し進化した設定となります。actionノードには、2 つの追加の属性があります。type属性は、アクションクラスのパッケージと名前を指定します。scope属性は、フォームbean(使用される場合)が要求スコープ内に置かれるようにします。

forward属性の代わりに、アクション設定は forwardノードを使用します。アクションが戻す可能性のあるすべての結果それぞれについて1つのノードが必要となります。

<struts-config>
...

<action-mappings>

<action path="/struts/list" scope="request"
type="com.fdar.articles.infoq.conversion.struts.ListBlogsAction" >
<forward name="success" path="/struts/list.jsp"/>
</action>
...

</action-mappings>
</struts-config>

アクション設定でも、Struts2 設定のXMLは前の設定とほぼ同じです。唯一の違いは、呼び出されるアクションのパッケージおよび名前がactionノードのclass属性で指定されることです。

<struts>
...

<package name="struts2" extends="struts-default" namespace="/struts2">

<default-interceptor-ref name="defaultStack"/>

<action name="list"
class="com.fdar.articles.infoq.conversion.struts2.ListBlogsAction">
<result>/struts2/list.jsp</result>
<interceptor-ref name="basicStack"/>
</action>
...

</package>
</struts>

executeメソッド以外のメソッドを呼び出す場合(BlogActionクラスを参照する設定のほとんどの場合)は、method属性にメソッドの名前を指定します。以下の例では、updateメソッドが呼び出されます。

    <action name="update" method="update"
class="com.fdar.articles.infoq.conversion.struts2.BlogAction" >
...
</action>

違いは、default-interceptor-refノードと interceptor-refノードに関係しています。パートIで、アクションが呼び出される前に、要求が一連のインターセプタを通過することを説明しましたが、これらのノードは、このインターセプタを設定します。default-interceptor-refノードは、パッケージにデフォルトで使用されるインターセプタの集合の名前を指定します。interceptor-refノードが指定されている場合は、このノードがデフォルトのインターセプタより優先されます(interceptor-ref ノードのname属性は、単一のインターセプタ、または事前に設定されたインターセプタの集合のどちらも参照できます)。さらに、複数の interceptor-refノードを指定することができます。処理は、それらがリストされている順序で実行されます。

3. ポストリダイレクト設定

今回使用した最後の設定は、結果のページの更新によってフォームが再送信されないという条件でフォームを送信するためのものです。これは、「ポストリダイレクトパターン」と言われます。最近は、「フラッシュスコープ」と言われることもあります。

フォームであるため、Strutsが使用するActionFormを指定する必要があります。これは、actionノードのname属性にフォームの名前(上記で設定した)を指定することにより実現します。この他に必要となる変更は、forward ノードでredirect属性をtrueに設定することだけです。

<struts-config>
...

<action-mappings>
<action path="/struts/save"
type="com.fdar.articles.infoq.conversion.struts.SaveBlogEntryAction"
name="blogForm" scope="request">
<forward name="success" redirect="true" path="/struts/list.do"/>
</action>
...

</action-mappings>
</struts-config>

Struts2では、既存の設定を強化するのではなく、結果の種類を増やすことでポストリダイレクトの機能が提供されています。これまでは、デフォルトのdispatchという結果の種類を使用していましたが、さまざまな種類の結果が使用できるようになっています。以下で使用している結果は、redirect です。

<struts>
...

<package name="struts2" extends="struts-default" namespace="/struts2">

<action name="save" method="save"
class="com.fdar.articles.infoq.conversion.struts2.BlogAction" >
<result type="redirect">list.action</result>
<interceptor-ref name="defaultStack"/>
</action>
...

</package>
</struts>

繰り返しですが、今回はデフォルトの結果として success を使用します。

まとめ

この記事では多くのことを説明してきましたが、まだ説明していない部分もあります。フレームワークおよび他の実装オプションについて理解を深めるために、調査する必要のある事項を以下に簡単にリストアップします。:

  • インターセプタ、およびその集合の設定 - たとえば、struts2-coreJARファイル内の struts-default.xml ファイルを確認します。横断的なアプリケーション機能を提供する独自のインターセプタを作成することは、大幅な時間の削減につながります。たとえば、struts-default.xml の中の例では、新規のインターセプタを含めてアプリケーションベースのインターセプタの集合を独自に設定する方法が示されています。
  • 設定ファイルでのワイルドカードパターン - Struts2では、すべてをスペルアウトする方法に加えて、ワイルドカードパターンを使用するというオプションもあります。これは、Struts実装からStruts2に移植されたものです。
  • ParameterAwareインターフェイスを使用したUIプロパティ マップの活用 - モデル、転送、値のオブジェクト、またはクラスの特定の属性を使用する代わりに、Struts2がすべての要求またはフォームの属性をアクションのマップに置くように設定することができます。これは、Strutsの動的フォーム機能をシミュレートします。

ここで、「これが本当に同じUIで機能するのか」という疑問が浮かぶことでしょう。答えはYesです。この記事では、今回のサンプルの完全なソースコード(zip)を提供しています。これを見れば、必要な変更は、呼び出されるURLの拡張子を変える(.doから.actionに)ことだけであることがわかります。さらに、Strutsのtaglib (フォーム向け)は、JSTLの代わりに簡単に使用できます。これは、興味のある読者の皆さんへの課題として残しておきます。

このシリーズの最後となる次回の記事では、ユーザインターフェイスについて説明します。アーキテクチャ、テーマとタグ、バリデーションを組み込む適切な方法、および UI コンポーネントを使用してコードを再使用する方法について説明します。最後には、アプリケーションを完全に変換することができるでしょう。

著者について

Ian Roughley氏は、マサチューセッツ州ボストンを拠点とする独立コンサルタントであり、講演や執筆などの活動も行っています。彼は、フォーチュン10社から創業間もないクライアントまで、さまざまな規模のクライアントに対して、アーキテクチャ、開発、プロセス改善および指導サービスを 10 年間にわたり提供してきました。彼は、実用的で成果重視の手法に注目し、オープンソースや、アジャイル開発技法によるプロセス改善および品質改善をサポートしています。

原文はこちらです:http://www.infoq.com/articles/migrating-struts-2-part2

(このArticleは2006年10月18日にリリースされました)

この記事に星をつける

おすすめ度
スタイル

BT