Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード

2011-02-12

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>

<aop:aspectj-autoproxy>にproxy-target-class="true"属性を指定することで、cglibを使い直接クラスに対するProxyを生成するようにすることがポイントです。

なお、ここではアクションメソッドの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>
	
	
	<mvc:annotation-driven />

*1アスペクトの優先順位やaroundアドバイスの意味など一部異なる部分もあるので完全互換というわけではない。

2011-01-26

Groovy言語とAspectJの人気が今ひとつな本当の理由

先日DevLOVEの主催するぐるぐるGroovyという勉強会に参加してきました。

1月24日 DevLOVE ぐるぐるGroovy -Easy Going Groovy-(東京都)

Groovy言語については、構文がJava言語に非常に近い上に、Javaの既存ライブラリーとの相互運用性も高く、さらに、Java言語に比べて非常に簡潔にプログラムが書けるという特徴があります。動的言語Rubyのような柔軟性とJavaプログラマーにとっての学習のしやすさをいう面を兼ね備えた軽量言語であり、私としてはSI業界でもきっと流行るはずに違いない思いと数年前から注目していました。

同じJVMを対象にした言語として、最近Scalaに注目が集まっているようですが、

ということがあり、一般のJavaプログラマーが理解して使いこなすには相当敷居が高いところがあります。対してGroovyの場合は、一部の例外を除きJavaの構文のままでも正しいGroovyなのであり、セミコロンをはずすなど段階的にGroovyらしいコードに移行していくことができます。現実問題として多くの職業プログラマースキルが低いといわれているのですから、学習曲線のなだらかさは無視できないポイントであると思います。

もちろん、私はGroovyScalaのような議論はナンセンスだと思いますし、そもそも両者ではターゲットとするものが違うのであると思っています。Scalaは型安全性や関数型による並行プログラミングの容易性などの特徴から、システム寄りのプログラミングフレームワークの構築に適した言語であるのに対して、Groovyは気軽に業務ロジックを書いたり、テストクラスを書いたり、ツールを作ったりするのに向いています。要するに使い分けなのですが、どちらかというとGroovyエンタープライズ開発をメインターゲットにした言語なのであると理解しています。だから、対象となるプログラマーの人数も多いはずなのです。その証拠に現在ではSpringが中心になってGroovyの開発を進めていますし、欧米ではエンタープライズ開発を中心にかなりメジャーな言語として使われているという話も聞きます。一方、対照的に日本ではかなりマイナーな言語*1というか、名前すら聞いたことのないJavaプログラマーも結構いたりするのでしょうか?

同様に、今のところAspectJエンタープライズ開発の現場ではあまり活用されていないようですね。AspectJは一見業務開発とは無縁な特殊な技術のように思われる方もおられるかもしれませんが、Springでも積極的に取り入れられているように実は業務システムの開発とは非常に相性がよいものだと思います。先日書いた、AJDTを使って規約違反のコードを検出する方法のような機能は、SIer脳的発想のSEやマネージャーに対しても受けがよいはずですし、アスペクト指向によって難しい技術的なコーディングを局所化することで、偶発的複雑性を極力排除し、普通のプログラマーは業務ロジックの記述に集中できるようになります。

先日、エンタープライズ開発者が負け組として軽蔑される日本のSI業界ってという記事を書いたのですが、Groovy言語やAspectJといった本来エンタープライズ系でもっと積極的に使われるべき技術の人気がどれも今ひとつなのは、技術面で世界の進歩から相当の遅れをとっている日本のSI業界(特に軽蔑・軽視される下流工程のプログラミング関連技術)を象徴しているからなのだろうかと思えますね。つまり、本来の技術自体がダメなのではなく、新しい技術を積極的に使おうとしない業界の体質によるものではないかと思います。そして、我が国ではたまたま運悪く生産性や品質が軽視される領域とメインターゲットが重なってしまっているということです。*2

私はこのような技術の有用性が日本のSI業界で見直され、もっと活用されるようになれば生産性や品質の向上に確実に寄与すると思います。

(追記)

ちなみに、この記事ではAspectJGroovyについて書きましたが、必ずしも両者の相性が良いということではありませんのでご注意ください。併用する場合は注意が必要だと思います。将来的にはAspectGなるものが出てくるのかもしれませんが。

no title

*1:実案件での適用例はまだまだ少ないですが、日本でもユーザーグループで積極的に活動が行われています。JGGUG

*2:別の意味ではScalaなどバリバリ使いこなすハイスキルプログラマーと一般の職業プログラマーとの間でスキルが極端に二極化しているということであり、中間的なスキルプログラマーがいないということもいえるかもしれません。

2011-01-25

AJDTを使って規約違反のコードを検出する方法

AspectJというと、メソッドなどに処理を織り込むAOPのイメージが強いと思いますが、AJDTというeclipseプラグインを使うと強力なコード検証ツールとして利用できることは意外と知られていないようです。(AJDTはSpring Tool Suiteには最初から内蔵されています。)

実際、

などの箇所をコンパイル時に検証して、警告やエラーとして検出できます。

たとえば、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()
        : "コントローラークラスのリクエストハンドラーメソッド中でフィールドに設定しています。";
}

こうすると、

f:id:ryoasai:20110126013234p:image

のように警告してくれます。FindBugsなどの静的解析ツールと併用することで、かなり強力な規約チェックが実施できます。

(補足)

構文的には

declare warning : ポイントカット : 警告メッセージ;
declare error : ポイントカット : エラーメッセージ;

のようにアスペクト中で指定するだけなのですが、一番難しいのは警告やエラー対象の場所指定をするポイントカット式の書き方の部分ですね。この部分は例を参考にしながら書き方を覚えるしかありません。

AspectJ/ポイントカット - アスペクト指向なWiki

no title

あと、制約としてポイントカットの部分にcflowとかcflowbelowなど動的なものは使えません。こうした動的なポイントカットによるチェックが必要な場合は、例外を発生させるアドバイスを適用して、自動化テストなどでアプリを動作させて検証させるしかありません。

*1:多くの場合、システム試験時に時刻を容易に変更可能なようにサービスモジュール経由で日付を生成することが普通

2010-10-29

AspectJ in Action

AspectJ in Action: Enterprise AOP with Spring Applications

AspectJ in Action: Enterprise AOP with Spring Applications

Manningのin actionシリーズはMEAP(Manning Early Access Program)という事前評価プロセスを通して、クオリティーを十分に高めてから出版されることもあり、比較的あたりはずれが少なく良書が多いと思います。(MEAP版を買わないと多少タイムリーさでは劣る傾向があるようですが。)

内容もちょうどいいレベル感のものが多く、私も参考書として日頃からいろいろとお世話になっているシリーズなのですが、この本は最近のin actionシリーズの中でも、特に読み応えのあった本で隠れた名著だと思います。実際、amazon.comの書評も上々です。

Springではバージョン2からAOPAspectJの@Aspectを部分採用していますし、ProxyベースのSpring AOPだけでなく、本格的なバイトコードウィービングやロードタイムウィービングへの移行も意外にスムーズにできます。この本は単にAOPAspectJの解説というだけでなくて、エンタープライズアプリケーション開発プロジェクトにおける具体的なAOPの使用例が満載されており、非常に参考になりました。

Spring RooではAspectJのITDを利用していることもありますし、Springを利用したエンタープライズアプリケーション開発者にとって、AspectJは今後必須の知識となると思います。