BT

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

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Spring 2.5:Spring MVCの新機能

Spring 2.5:Spring MVCの新機能

ブックマーク

Springフレームワークは当初から、複雑な問題に対して強力だけれども、非侵襲的なソリューションを提供することに焦点をあててきました。Spring 2.0ではXMLベースの設定を削減する方法として、カスタムのネーム空間を導入しました。それ以来カスタムのネーム空間は、Springフレームワークのコア(aop、context、jee、jms、lang、tx、utilの各ネーム空間)、Springポートフォリオプロジェクト(たとえばSpring Security)、Springではないプロジェクト(たとえばCXF)に定着しています。

Spring 2.5では、XMLベースの設定に取って代わるものとして、包括的なアノテーション一式を導入しました。アノテーションの使用法には、Springが管理するオブジェクトの自動発見、依存性注入、ライフサイクルメソッド、Web層の設定、ユニット/統合検査があります。

この記事は、Spring 2.5で導入されたアノテーションを探究する3部作の第2弾です。Web層におけるアノテーションのサポートを扱います。最後の論文では、統合と検査で利用できる追加機能を説明する予定です。

3部作のPart 1(参考記事) では、Springが管理するオブジェクトの設定と依存性注入において、XMLの代わりにどのようにJavaアノテーションを使用可能か説明しました。以下に再度、例を挙げておきます。

@Controller
public class ClinicController {

private final Clinic clinic;

@Autowired
public ClinicController(Clinic clinic) {
this.clinic = clinic;
}
...

@ControllerはClinicControllerがWeb層コンポーネントであることを示しています。@AutowiredはClinicのインスタンスに依存性の注入を要求します。この例では、両アノテーションの承認の有効化とコンポーネント・スキャニング範囲の制限に、わずかな量のXMLだけで済みます。

Web層ではSpring XMLの設定が下の層に比べて冗長になりがちで、おそらくその価値も低い傾向にあるので、わずかな量のXMLで済むというのは素晴らしいニュースです。コントローラは、view名やフォームオブジェクト名、バリデータ型など多数のプロパティを保持しますが、その目的は依存性注入よりも設定です。そうした設定を効率的に管理する方法として、bean定義の継承や、あまり頻繁に変更しないプロパティの設定回避があります。しかし、経験から申し上げると、多数のデベロッパがそうした方法をとらないので、結果として必要以上のXMLとなってしまうのです。ですから、@Controllerと@AutowiredはWeb層の設定に非常に好ましい効果を上げられるのです。

シリーズ第2弾でこの議論を引き継ぎ、Web層向けのSpring 2.5アノテーションを一通り見て回ります。こうしたアノテーションは非公式に@MVCと呼ばれており、Spring MVCとSpring Portlet MVCを意味しています。実際、この論文で説明している機能の大部分が、この2つに当てはまります。

Controllerから@Controllerへ

Part 1で説明したアノテーションとは対照的に、@MVCは単なる代替設定以上のものです。Spring MVC Controllerの以下の有名なシグネチャを考えてみましょう。

public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse
response) throws Exception;
}

すべてのSpring MVCコントローラは、Controllerを直接実装するか、あるいはAbstractController、SimpleFormController、MultiActionController、AbstractWizardFormControllerなど利用可能なベースクラスの実装のうちの1つから拡張します。このインターフェースにより、Spring MVCのDispatcherServletは前述したすべてを「ハンドラ」として扱うことができ、SimpleControllerHandlerAdapterというアダプターの助けを借りて、こうしたハンドラを呼び出すことができるのです。

@MVCはこのプログラミング・モデルを3つの特筆すべき方法で変更します。

  1. インターフェースやベースクラスの必要条件は皆無
  2. リクエスト処理メソッドの数に制限無し
  3. メソッドのシグネチャに高度の柔軟性

この3点を考えると、@MVCはただの代替ではない、と言えるでしょう。Spring MVCのコントローラ技術は進化の次段階に来ているのです。

DispatcherServletはAnnotationMethodHandlerAdapterというアダプターを用いて、アノテーション付きコントローラを呼び出します。これ以降で論じるアノテーションのサポートにおいて、大部分の作業を担当するのがこのアダプターです。さらに同アダプターは、ベースクラスコントローラの必要性を事実上なくします。

@RequestMappingの紹介

従来のSpring MVC Controllerに類似したコントローラから始めましょう。

@Controller
public class AccountsController {

private AccountRepository accountRepository;

@Autowired
public AccountsController(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}

@RequestMapping("/accounts/show")
public ModelAndView show(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
ModelAndView mav = new ModelAndView("/WEB-INF/views/accounts/show.jsp");
mav.addObject("account", accountRepository.findAccount(number));
return mav;
}
}

ここで違っているのは、このコントローラはControllerインタフェースを拡張しないということで、さらに、@RequestMappingアノテーションを用いて、show()がURIパス「/accounts/show」にマップされたリクエスト処理メソッドであることも示しています。残りのコードはSpring MVC コントローラの型どおりのものとなっています。

上記のメソッドを@MVCに完全に変換した後に@RequestMappingに戻る予定ですが、その前に、上記のリクエストマッピングURIが、以下のようなあらゆる拡張子を伴ったURIパスにも一致することに言及しておきましょう。

/accounts/show.htm
/accounts/show.xls
/accounts/show.pdf
...

柔軟なリクエスト処理メソッド・シグネチャ

メソッド・シグネチャが柔軟である、と先ほどお約束しました。先に進めて、入力パラメータからレスポンスオブジェクトを取り除き、ModelAndViewを返す代わりに、モデルを表す入力パラメータとしてMapを追加します。さらに、そのレスポンスのレンダリング時には、Stringを返して使用するview名を示します。

@RequestMapping("/accounts/show")
public String show(HttpServletRequest request, Map model)
throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
model.put("account", accountRepository.findAccount(number));
return "/WEB-INF/views/accounts/show.jsp";
}

Mapの入力パラメータは「暗黙の」モデルと呼ばれており、メソッドが呼び出される前に、都合よく作成されます。暗黙のモデルに対になったキー値を加えると、view(この場合show.jspページ)でのレンダリングにデータが利用可能になります。

@MVCにより、HttpServletRequest/HttpServletResponse、HttpSession、Locale、InputStream、OutputStream、File[]など、多数の型が入力パラメータとして使えるようになります。どのような順番で提供されても大丈夫です。さらに、ModelAndView、Map、String、そして何よりも虚空型といった多数の戻り型も可能になります。サポートされている入力・出力パラメータ型の完全なリストについては、JavaDocの@RequestMapping(リンク)をお調べください。

興味深いのは、メソッドがviewを指定しないと(たとえば戻り型が無効)、どうなるかということです。規則ではそうした場合、DispatcherServletがリクエストURIのパス情報を再利用し、先頭のスラッシュと拡張子を取り除くことになっています。戻り型を虚空に変更しましょう。:

@RequestMapping("/accounts/show")
public void show(HttpServletRequest request, Map model) throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
model.put("account", accountRepository.findAccount(number));
}

このリクエスト処理メソッドと「/accounts/show」リクエストマッピングという条件下では、DispatcherServletが「accounts/show」というデフォルトのview名にフォールバックすると予想でき、このview名が以下のような適切なviewリゾルバと組み合わされると、以前と同じ結果をもたらします。

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>

view名については規則に従うようお勧めします。なぜなら、規則に従っていれば、ハードコードされたview名をコントローラから排除するのに役立つからです。DispatcherServletがデフォルトのview名を導き出す方法をカスタマイズする必要があるなら、サーブレットコンテキスト内で独自のRequestToViewNameTranslator実装を設定し、bean idに「viewNameTranslator」を割り当ててください。

@RequestParamを使って構文解析パラメータを抽出

@MVCのもう1つの特徴として、リクエストパラメータの抽出・解析機能があります。引き続きメソッドのリファクタリングを行い、@RequestParamアノテーションを追加しましょう。

@RequestMapping("/accounts/show")
public void show(@RequestParam("number") String number, Map model) {
model.put("account", accountRepository.findAccount(number));
}

ここでは@RequestParamアノテーションの助けにより「number」という名称のStringパラメータが抽出され、入力パラメータとして渡されます。@RequestParamは型変換も、requiredパラメータ vs. optionalパラメータもサポートします。 型変換については、すべての基本的なJava型がサポートされており、カスタムのPropertyEditorsを使って拡張可能です。以下に、requiredパラメータ、optionalパラメータの例をもう少し挙げておきます。

@RequestParam(value="number", required=false) String number
@RequestParam("id") Long id
@RequestParam("balance") double balance
@RequestParam double amount

最後の例で明示的なパラメータ名を提供していないことにご注意ください。こうすると「amount」というパラメータが抽出されますが、それはコードをデバッグシンボルを使ってコンパイルした場合に限ります。コードのコンパイルにデバッグシンボルを使用しなかった場合はIllegalStateExceptionがスローされることになりますが、その理由は、リクエストからパラメータを抽出するには情報が不十分だからです。このため、パラメータ名の明示的な指定が望ましいのです。

@RequestMappingの続き

選択を狭めるという効果をもたらすために、クラスレベルに置いた@RequestMappingとメソッドレベルの@RequestMappingアノテーションを併用するのは、道理にかなっています。以下に例を挙げます。

クラスレベル:

RequestMapping("/accounts/*")

メソッドレベル:

@RequestMapping(value="delete", method=RequestMethod.POST)
@RequestMapping(value="index", method=RequestMethod.GET, params="type=checking")
@RequestMapping

クラスレベルのマッピングと併用した最初のメソッドレベルのリクエストマッピングは、「/accounts/delete」に一致しますが、この場合のHTTPメソッドはPOSTです。2番目の例では、「type」という名称のリクエストパラメータに要件を追加しますが、そのリクエストの値は「checking」になっています。3番目のメソッドではまったくパスを指定していません。このメソッドは全HTTPメソッドに一致し、必要に応じてそのメソッド名が使われます。次のようなメソッド名解決に依存するよう、メソッドを変更しましょう。

@Controller
@RequestMapping("/accounts/*")
public class AccountsController {

@RequestMapping(method=RequestMethod.GET)
public void show(@RequestParam("number") String number, Map model)
{
model.put("account", accountRepository.findAccount(number));
}
...

このメソッドは、「/accounts/*」のクラスレベル@RequestMappingと「show」というメソッド名に基づき、「/accounts/show」へのリクエストに一致します。

クラスレベルのRequest Mappingの削除

Web層のアノテーションに対してしばしば耳にする反対意見に、URIパスがソースコードに埋め込まれてしまうという事実があります。これについては、コントローラクラスには一致するURIパスというXML設定の戦略を用い、メソッドレベルのマッピングにのみ@RequestMappingアノテーションを使用することで、容易に解決できます。

ControllerClassNameHandlerMappingを設定してみますが、ControllerClassNameHandlerMappingはコントローラのクラス名に依存する規則を使って、コントローラへのURIパスをマッピングします。

さて、「/accounts/*」へのリクエストはAccountsControllerと一致します。これは、メソッドレベルの @RequestMappingアノテーションとの組合せでうまく機能し、上記のマッピングにメソッド名を追加することでマッピングが完了します。さらに、このメソッドではview名が返ってこないため、今度はクラス名、メソッド名、URIパス、view名を一致させる規則を使います。

@Controllerを完全に@MVCに変換すると、以下のようになります。

@Controller
public class AccountsController {

private AccountRepository accountRepository;

@Autowired
public AccountsController(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}

@RequestMapping(method=RequestMethod.GET)
public void show(@RequestParam("number") String number, Map model)
{
model.put("account", accountRepository.findAccount(number));
}
...

補助となるXMLは次のようになります。

<context:component-scan base-package="com.abc.accounts"/>

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>

ご覧のとおり、XMLは最小限で、埋め込みのURIパスや明示的なview名もなく、リクエスト処理メソッドは1行の構成で、メソッドシグネチャは必要とするものと正確に一致しており、追加のリクエスト処理メソッドも容易に付け加えられます。こうした恩恵の全てが、ベースクラスやXML不要で手に入ります -- 少なくとも直接このコントローラに帰因するベースクラスやXMLは皆無です。

このプログラミングモデルの効果が、次第に分かってきたのではないかと思います。

@MVC Formの処理

典型的なフォーム処理のシナリオには、編集するオブジェクトの検索、保持されているデータを編集モードで提示、ユーザによる提出、そして最後に変更の認証と保存が伴います。このすべてを支援するために、リクエストパラメータからオブジェクトを完全投入するためのデータバインディング・メカニズムや、エラー処理および認証のサポート、JSPのフォームタグ・ライブラリ、ベースクラス・コントローラといった機能が、Spring MVCには備わっています。@ModelAttribute、@InitBinder、@SessionAttributesアノテーションのおかげで、ベースクラス・コントローラが不要になったこと以外、@MVCには何の変化もありません。

@ModelAttributeアノテーション

以下のリクエスト処理メソッドのシグネチャをご覧ください。

@RequestMapping(method=RequestMethod.GET)
public Account setupForm() {
...
}

@RequestMapping(method=RequestMethod.POST)
public void onSubmit(Account account) {
...
}

上記は、リクエスト処理メソッドのシグネチャとして完全に有効です。1番目のメソッドでは最初のHTTP GETを処理しています。編集されるデータを準備し、Spring MVCのフォームタグで使用するAccountを返します。2番目のメソッドでは、ユーザによる変更提出の結果生じるHTTP POSTを処理します。Spring MVCのデータバインディング・メカニズムを使って、リクエストパラメータから自動投入されるAccountを受け入れます。非常に単純なプログラミングモデルです。

Accountオブジェクトには編集されるデータが保持されています。Spring MVCの専門用語では、Accountはフォームモデルのオブジェクトとなっています。このオブジェクトは何らかの名称の下、フォームタグ(ならびにデータバインディング・メカニズム)で利用可能になっていなければなりません。以下はJSPページからの抜粋ですが、「Account」という名称のフォームモデル・オブジェクトに触れています。

<form:form modelAttribute="account" method="post">
Account Number: <form:input path="number"/><form:errors path="number"/>
...
</form>

「account」という名称をどこにも指定していませんが、それでもこのJSPスニペットは、前述のメソッドシグネチャとうまく機能するでしょう。なぜかというと、@MVCでは返されたオブジェクト型の名称を使ってデフォルト名を選択するからです。そのため、型がAccountのオブジェクトは、デフォルトで「account」という名称のフォームモデル・オブジェクトとなるのです。デフォルトが適当でない場合は、次のように@ModelAttributeを使って名称を変更できます。

@RequestMapping(method=RequestMethod.GET)
public @ModelAttribute("account") SpecialAccount setupForm() {
...
}
@RequestMapping(method=RequestMethod.POST)
public void update(@ModelAttribute("account") SpecialAccount account) {
...
}

@ ModelAttributeをメソッドレベルに置くと、少々違った効果をもたらします。

@ModelAttribute
public Account setupModelAttribute() {
...
}

ここのsetupModelAttribute()はリクエスト処理メソッドではありません。それどころか、他のいかなるリクエスト処理メソッドの呼び出しよりも前に、フォームモデル・オブジェクトを準備するために使われるメソッドになっています。よくご存知の方々からご覧になると、SimpleFormControllerのformBackingObject()メソッドに酷似しているのではないでしょうか。

@ModelAttributeをメソッド上に置くと、フォーム処理シナリオで役立ちますが、最初のGET中にまず1度、フォームモデル・オブジェクトの検索が行われ、その後、ユーザの変更に伴ってデータバインディングを既存のAccountオブジェクトとオーバーレイさせたいときに発生するPOSTの際に2度目の検索が実行されます。もちろん、オブジェクトの2度検索に代わる手段は、この2度のリクエスト間にオブジェクトをHTTPセッションに格納しておくことでしょう。これについては、次に検討します。

@ SessionAttributesを使って属性を格納

@SessionAttributesアノテーションは、リクエスト間のセッションに保持しておく、フォームモデル・オブジェクトの名称もしくは型の指定に使用可能です。以下に例を2つ挙げます。

@Controller
@SessionAttributes("account")
public class AccountFormController {
...
}

@Controller
@SessionAttributes(types = Account.class)
public class AccountFormController {
...
}

このアノテーションを使い、AccountFormControllerは「account」という名称のフォームモデル・オブジェクト(2番目のケースでは型がAccountのあらゆるフォームモデル・オブジェクト)を最初のGETと次のPOSTの間のHTTP セッションに格納します。変更が持続する場合は、セッションから属性を取り除くべきでしょう。SessionStatusインスタンスの助けを借りて属性を削除できますが、SessionStatusインスタンスはonSubmitメソッドのメソッドシグネチャに追加されると、@MVCによって渡されます。

@RequestMapping(method=RequestMethod.POST)
public void onSubmit(Account account, SessionStatus sessionStatus) {
...
sessionStatus.setComplete(); // Clears @SessionAttributes
}

DataBinderのカスタマイズ

時々、データバインディングにカスタマイズが必要になります。たとえば、必要フィールドを指定したり、日付や通貨金額など向けにカスタムのPropertyEditorsを登録したりする必要があるかもしれません。@MVCを使えば、簡単にできます。

@InitBinder
public void initDataBinder(WebDataBinder binder) {
binder.setRequiredFields(new String[] {"number", "name"});
}

@InitBinderを使ってアノテートしたメソッドは、@MVCがリクエストパラメータのバインディングに利用する DataBinderインスタンスにアクセスできます。各コントローラに必要なカスタマイズが可能になります。

データバインディングの結果と認証

データバインディングは、型変換の失敗やフィールドの欠落といったエラーをもたらす可能性があります。エラーが発生してしまったら、編集フォームに戻り、ユーザが訂正できるようにしたいものです。そのためには、フォームモデル・オブジェクトのすぐ後で、メソッドシグネチャにBindingResultオブジェクトを追加します。以下に例を示します。

@RequestMapping(method=RequestMethod.POST)
public ModelAndView onSubmit(Account account, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
ModelAndView mav = new ModelAndView();
mav.getModel().putAll(bindingResult.getModel());
return mav;
}
// Save the changes and redirect to the next view...
}

エラー発生時には、BindingResultから、モデルに属性を追加した時のviewに戻るので、フィールド特有のエラーに戻ってユーザに表示します。view名を明示的に指定していないことにご留意ください。その代わりに、受け取るURIのパス情報に一致するデフォルトのview名をDispatcherServletが返せるようにしています。

認証に必要なのは追加の1行のみで、ValidatorオブジェクトにBindingResultを渡すために、この1行でValidatorオブジェクトを呼び出します。これにより、バインディングエラーと認証エラーを1箇所に蓄積できます。

@RequestMapping(method=RequestMethod.POST)
public ModelAndView onSubmit(Account account, BindingResult bindingResult) {
accountValidator.validate(account, bindingResult);
if (bindingResult.hasErrors()) {
ModelAndView mav = new ModelAndView();
mav.getModel().putAll(bindingResult.getModel());
return mav;
}
// Save the changes and redirect to the next view...
}

これをもちまして、Spring 2.5のWeb層向けアノテーション、通称@MVCの一通りの紹介は終わりです。

まとめ

Web層におけるアノテーションは非常に有益であることが分かっています。単にXML設定の量を大幅に削減するだけではなく、Spring MVCのコントローラ技術にフルアクセスすることにより、的確かつ柔軟、そして簡潔なプログラミングモデルが可能になります。ソースコードへのURIパスの埋め込みや、view名の明示的な定義を回避するためには、コントローラへリクエストを委譲するハンドラ・マッピング戦略を中央集中化するだけでなく、「設定に勝る規約」の特色を利用することを大いにお勧めします。

最後に、この論文では説明していませんが、Spring MVCの非常に重要な機能拡張は特筆に値します。最近リリースされたSpring Web Flowバージョン2には、Spring MVCベースのJSF view、Spring JavaScriptライブラリ、より高度な編集シナリオをサポートする、ステートおよびナビゲーションの先進管理といった機能が追加されています。

原文はこちらです:http://www.infoq.com/articles/spring-2.5-ii-spring-mvc
(このArticleは2008年8月3日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT