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ファイルのの前後に以下のように記述します。ここでのポイントとして、の前でannotationMethodHandlerAdapterの宣言を追記することに注意してください。

	<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のススメ

*1:Spring MVCの伝統的な方言ではコマンドオブジェクトとも呼ばれる。

*2:ただし、@MVCの中心的なクラスであるAnnotationMethodHandlerAdapterクラスは現状Godクラスアンチパターンになっており、きわめて拡張が困難なため、リファクタリングすることが望まれます。