Spring MVCでセッション属性のキーをコントローラーごとに別々にするには
Spring MVCで、フォームBean*1をセッションスコープに格納して処理をしたい場合、以下のように@SessionAttributesアノテーションをコントローラークラスに付加することで行うことができます。また、処理の完了時には SessionStatusをパラメーターで受け取り、setComplete()メソッドを呼び出すことでセッション中のフォームを削除します。
@Controller @RequestMapping(value="/account") @SessionAttributes("account") public class AccountController { private Map<Long, Account> accounts = new ConcurrentHashMap<Long, Account>(); @RequestMapping(method=RequestMethod.GET) public String getCreateForm(Model model) { model.addAttribute(new Account()); return "account/createForm"; } @RequestMapping(method=RequestMethod.POST) public String create(@Valid Account account, BindingResult result, SessionStatus sessionStatus) { if (result.hasErrors()) { return "account/createForm"; } this.accounts.put(account.assignId(), account); sessionStatus.setComplete(); // セッションクリア return "redirect:/account/" + account.getId(); } @RequestMapping(value="{id}", method=RequestMethod.GET) public String getView(@PathVariable Long id, Model model) { Account account = this.accounts.get(id); if (account == null) { throw new ResourceNotFoundException(id); } model.addAttribute(account); return "account/view"; } }
この場合のポイントはセッション属性のキー名がフォームBeanのモデルマップ内のキーと同一であるべきということです。Spring MVCのアーキテクチャーではビューが直接Session中の属性を見ることは通常せず、リクエストスコープのモデルマップを通してのみアクセスします。モデルマップ中のキーはCoC的にはBeanクラスの単純名の先頭を小文字にしたものになるので、上記の例ではaccountとなっています。
複雑なアプリケーションだと、上記のAccountのようなBeanを複数のコントローラーで使いまわすことがありますが、この場合、セッションのキー名が常にaccountになってしまうと、複数の処理で上書きされてしまい予期せぬ不具合の原因となることがあります。ここではコントローラークラスごとにセッション属性のキー名を別々にするための方法を紹介します。
まず、以下のようなHandlerInterceptorの実装クラスを作成します。
package sample.mvc; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; public class HandlerExposingHandlerInterceptor extends HandlerInterceptorAdapter { public static final String HANDLER_KEY = "_HANDLER_KEY"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // ハンドラーオブジェクトをリクエスト属性に登録しておく request.setAttribute(HANDLER_KEY, handler); return super.preHandle(request, response, handler); } }
このインターセプターを登録することで、コントローラーのメソッド起動が行われる前にコントローラー(ハンドラー)オブジェクトを後からアクセス可能なようにリクエストスコープに格納しておきます。
次に、以下のようにSessionAttributeStoreの実装を作成します。
package sample.mvc; import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.context.request.WebRequest; public class ClassNamePrefixingSessionAttributeStore extends DefaultSessionAttributeStore { @Override protected String getAttributeNameInSession(WebRequest request, String attributeName) { Object handler = request.getAttribute(HandlerExposingHandlerInterceptor.HANDLER_KEY, WebRequest.SCOPE_REQUEST); if (handler == null) return super.getAttributeNameInSession(request, attributeName); return handler.getClass().getName() + "." + super.getAttributeNameInSession(request, attributeName); } }
なお、このようにDefaultSessionAttributeStoreを継承するのが簡単ですが、親クラスとの結合度が高くていやな人は、ステップ数は長くなりますがDecoratorパターンを使った以下のような実装も代替案として考えられます。
package sample.mvc; import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.context.request.WebRequest; public class ClassNamePrefixingSessionAttributeStore implements SessionAttributeStore { private SessionAttributeStore delegate = new DefaultSessionAttributeStore(); public SessionAttributeStore getSessionAttributeStore() { return delegate; } public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { this.delegate = sessionAttributeStore; } public void storeAttribute(WebRequest request, String attributeName, Object attributeValue) { delegate.storeAttribute(request, getPrefixForSessionAttributeKey(request) + attributeName, attributeValue); } public Object retrieveAttribute(WebRequest request, String attributeName) { return delegate.retrieveAttribute(request, getPrefixForSessionAttributeKey(request) + attributeName); } public void cleanupAttribute(WebRequest request, String attributeName) { delegate.cleanupAttribute(request, getPrefixForSessionAttributeKey(request) + attributeName); } protected String getPrefixForSessionAttributeKey(WebRequest request) { Object handler = request.getAttribute(HandlerExposingHandlerInterceptor.HANDLER_KEY, WebRequest.SCOPE_REQUEST); if (handler == null) return ""; return handler.getClass().getName() + "."; } }
最後にBean定義xmlファイルの
<bean id="annotationMethodHandlerAdapter" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="sessionAttributeStore"> <bean class="sample.mvc.ClassNamePrefixingSessionAttributeStore" /> </property> </bean> <!-- Configures the @Controller programming model --> <mvc:annotation-driven /> <!-- Configures Handler Interceptors --> <mvc:interceptors> <bean class="sample.mvc.HandlerExposingHandlerInterceptor" /> </mvc:interceptors>
以上で、セッション属性の属性名は「accont」ではなく「コントローラークラス名.account」のようになり、コントローラーごとに別々のキーで保存されるようになります。もちろん、ビューは今までどおりモデルマップ経由でしかデータにアクセスしないため変更は一切不要です。
さらに、コントローラーに独自のアノテーションをつけることで、追加の情報を利用したり、コントローラーごとにプレフィックスをつけるかつけないかといったことを制御することも可能です。また、この考え方を応用することで、ブラウザのウィンドウやタブごとに属性名を分離したり、会話スコープを実装することもできるでしょう。このようにSpring MVCはさまざまなところに拡張ポイントが提供されているため、意外に簡単なコードで機能をカスタマイズすることができます。*2
なお、Spring MVCについては、以下に記事も参考にしてください。Spring MVCのススメ
AJDTを使って規約違反のコードを検出する方法
AspectJというと、メソッドなどに処理を織り込むAOPのイメージが強いと思いますが、AJDTというeclipseのプラグインを使うと強力なコード検証ツールとして利用できることは意外と知られていないようです。(AJDTはSpring Tool Suiteには最初から内蔵されています。)
実際、
- コントローラークラスのメソッド内でフィールドの設定を行う
- サービス層を経由せずに直接DAOを呼び出している
- 日付オブジェクトを直接newしている*1
などの箇所をコンパイル時に検証して、警告やエラーとして検出できます。
たとえば、Spring MVCのコントローラークラスのメソッド内でフィールドの設定を行っている箇所を警告として検出するには以下のようなアスペクトを書くだけです。
package sample.mvc; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; public aspect CondingRules { pointcut setFieldInHandlerMethod() : withincode(@RequestMapping * (@Controller *).*(..)) && set(* *); declare warning : setFieldInHandlerMethod() : "コントローラークラスのリクエストハンドラーメソッド中でフィールドに設定しています。"; }
こうすると、
のように警告してくれます。FindBugsなどの静的解析ツールと併用することで、かなり強力な規約チェックが実施できます。
(補足)
構文的には
declare warning : ポイントカット : 警告メッセージ; declare error : ポイントカット : エラーメッセージ;
のようにアスペクト中で指定するだけなのですが、一番難しいのは警告やエラー対象の場所指定をするポイントカット式の書き方の部分ですね。この部分は例を参考にしながら書き方を覚えるしかありません。
AspectJ/ポイントカット - アスペクト指向なWiki
http://www.limy.org/program/aspect/
あと、制約としてポイントカットの部分にcflowとかcflowbelowなど動的なものは使えません。こうした動的なポイントカットによるチェックが必要な場合は、例外を発生させるアドバイスを適用して、自動化テストなどでアプリを動作させて検証させるしかありません。
*1:多くの場合、システム試験時に時刻を容易に変更可能なようにサービスモジュール経由で日付を生成することが普通