Spring MVCでコントローラーのリクエストハンドラメソッドのメタ情報を記録する方法
正攻法でアクションハンドラメソッドのメタ情報を取得することは困難
開発で利用するフレームワークを作成する際には、実行対象となるオブジェクトの型やメソッドに付けられたアノテーションなどのメタ情報を利用したい場合が多くあります。特に、Spring MVCを拡張してさまざまなことを裏でやらせたい場合には、現在実行中のクラスやメソッドのメタ情報(ClassクラスやMethodクラスのインスタンス)をスレッドごとにグローバルにアクセス可能なコンテキストオブジェクト内に記録しておくと便利です。なぜなら、Springの場合ViewResolverやViewなど独自に拡張可能なインターフェースが多数提供されているものの、通常の手段では実行中のクラスやメソッドの情報をパラメーターなどから簡単に取得できないことが多いからです。
たとえば、Spring MVCでセッション属性のキーをコントローラーごとに別々にするには - 達人プログラマーを目指してでは、HandlerExposingHandlerInterceptorという独自のハンドラインターセプターを作成し、現在実行中のコントローラーのインスタンスをリクエストコンテキスト中に記録することで、セッションのキー名にクラス名を付加するテクニックを紹介しています。
それでは、コントローラのオブジェクトではなく、現リクエストで実行対象になったアクションメソッドのメタ情報はどのように記録したらよいのでしょうか?たとえば、Spring MVCでJSONデータを返すための手順 - 達人プログラマーを目指しての最後に紹介した、
@RequestMapping(value="/jsonTable", method=RequestMethod.GET) @ResponseBody @ToGrid({"id", "name", "age"}) public List<Person> jsonTable() { return findPersonList(); }
のようなメソッドで@ToGridという独自のアノテーションを裏で処理させたい場合、jsonTable()に付けられたアノテーションの情報を取得する必要があります。
一般にSpring Frameworkは拡張性が高くできているのだから、こういった処理は簡単にできるだろうと期待されるのですが、残念ながら今のところ正攻法ではかなり困難な構造になっています。実際、コントローラーのメソッド起動はAnnotationMethodHandlerAdapterというクラスのinvokeHandlerMethodというメソッドで実装されています。
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ServletHandlerMethodResolver methodResolver = getMethodResolver(handler); Method handlerMethod = methodResolver.resolveHandlerMethod(request); ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver); ServletWebRequest webRequest = new ServletWebRequest(request, response); ExtendedModelMap implicitModel = new BindingAwareModelMap(); Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest); methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); return mav; }
上記のコードでServletHandlerMethodInvokerというクラスを使いリクレクションを使って対象のメソッドを起動しているのですが、残念ながら、ServletHandlerMethodInvokerはprivateな内部クラスとなっていて、容易に置き換えや拡張が不可能です。
AspectJを使った簡単な解決策
AspectJが利用できる環境ならば、以下のようなアスペクトを定義することで以上の問題に簡単に対処できます。
import org.aspectj.lang.reflect.MethodSignature; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; public aspect HandlerMethodExposingAspect { public pointcut handlerMethod() : execution(@RequestMapping * *(..)); before() : handlerMethod() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); MethodSignature methodSignature = (MethodSignature) thisJoinPointStaticPart.getSignature(); requestAttributes.setAttribute(Controllers.HANDLER_METHOD_KEY, methodSignature.getMethod(), RequestAttributes.SCOPE_REQUEST); } }
さらに、リクエストコンテキストに格納した情報に簡単にアクセスする以下のユーティリティクラスを定義します。
import java.lang.reflect.Method; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; public class Controllers { public static final String HANDLER_METHOD_KEY = "_HANDLER_METHOD_KEY"; private Controllers() {} public static Method getCurrentHandlerMethod() { return (Method)RequestContextHolder.getRequestAttributes().getAttribute(HANDLER_METHOD_KEY, RequestAttributes.SCOPE_REQUEST); } }
あとは、
Method currentHandlerMethod = Controllers.getCurrentHandlerMethod()
の呼び出しで、どこでも実行対象のメソッドのメタ情報にアクセスできます。
ProxyベースのSpring AOPを使う場合
AspectJが使えない場合、ProxyベースのSpring AOPの機能を用いて代用することができます。ただし、一般的にコントローラーのメソッドはインターフェースを実装しないため、動的プロキシーを使ったProxy生成が利用できないため、cglibを併用する必要があります。Mavenを使うなら、pomに以下の依存関係を忘れずに追記します。
<dependency> <groupId>cglib</groupId> <artifactId>cglib-nodep</artifactId> <version>2.2</version> </dependency>
それから、先に紹介したアスペクトは@Aspect形式を使って以下のように書き直します。
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import sample.mvc.Controllers; @Aspect public class HandlerMethodExposingAspect { @Pointcut("execution(@org.springframework.web.bind.annotation.RequestMapping * *(..))") public void handlerMethod() {} @Before("handlerMethod()") public void interceptHandlerMethod(JoinPoint jp) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); MethodSignature methodSignature = (MethodSignature) jp.getSignature(); requestAttributes.setAttribute(Controllers.HANDLER_METHOD_KEY, methodSignature.getMethod(), RequestAttributes.SCOPE_REQUEST); } }
ちなみに、@Aspect形式とは、AspectJにマージされたAspectWerkzから引き継いだ機能であり、Java言語とアノテーションを用いてアスペクトを定義する形式です。ややこしいのですが、Spring AOPではメカニズムは本来のAspectJとはまったく異なるものの、アスペクトの定義は@Aspect形式で定義する限り、AspectJとほぼ共通の記述*1が可能となっています。
次に、Spring MVCのBean定義ファイルに以下の定義を追加します。
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"> ... <aop:aspectj-autoproxy proxy-target-class="true"/> <bean class="sample.mvc.HandlerMethodExposingAspect" /> </beans>
なお、ここではアクションメソッドのMethodオブジェクトをリクエストコンテキストに記憶する汎用のアスペクトの書き方を紹介しましたが、もちろん、アスペクト内で個別に処理可能なロジックであればこのようなことは不要で、アドバイス中で直接アノテーションなどの情報を取得すればよいのです。ただし、Viewインターフェースを独自に拡張したりするような場合、アスペクトの外部でアクションメソッドのメタ情報にアクセスできると便利であるということです。
実際、本文の最初の例の場合に@ToGridを解釈して2次元配列のJSONを返すには、標準のMappingJacksonHttpMessageConverterを拡張したクラスを作成し、writeInternal()メソッドをオーバーライドして、そこでハンドラメソッドのアノテーションからメタ情報を取得すればよいのです。
import java.io.IOException; import java.lang.reflect.Method; import org.springframework.http.HttpOutputMessage; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; public class ExMappingJacksonHttpMessageConverter extends MappingJacksonHttpMessageConverter { @Override protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { Method currentHandlerMethod = Controllers.getCurrentHandlerMethod(); if (currentHandlerMethod != null && currentHandlerMethod.isAnnotationPresent(ToGrid.class)) { // @ToGridが付いていたら2次元配列に変換してからJSON化する。 ToGrid toGrid = currentHandlerMethod.getAnnotation(ToGrid.class); super.writeInternal(Grids.toArray(toGrid.value()), outputMessage); } else { super.writeInternal(o, outputMessage); } } }
あとは、このように拡張したコンバーターをBeanとして登録するだけです。
<bean id="annotationMethodHandlerAdapter" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <bean class="spring.mvc.sample.ExMappingJacksonHttpMessageConverter" /> </list> </property> </bean> <!-- Configures the @Controller programming model --> <mvc:annotation-driven />
*1:アスペクトの優先順位やaroundアドバイスの意味など一部異なる部分もあるので完全互換というわけではない。