S2AOPを読む

動機

仕事でSAStrutsを使うことになったけどストラッツとかディーアイとかエーオーピーとかわかんないです。

目的

メソッド呼び出しがインターセプトされるまでの流れを理解する。インターセプタの起動順序はどこで決まるのか把握。

更新履歴

  • 20090327
    • MethodInvocation#proceedの使われ方について加筆

このコードの流れを追ってみる

http://s2container.seasar.org/2.3/ja/aop.html#nodicon

Pointcut pointcut = new PointcutImpl(new String[]{"getTime"});
Aspect aspect = new AspectImpl(new TraceInterceptor(), pointcut);
AopProxy aopProxy = new AopProxy(Date.class, new Aspect[]{aspect});
Date proxy = (Date) aopProxy.create();
proxy.getTime();

JVMにメソッド起動をフックするしくみでもあるのかと思ったらプロキシオブジェクト作ってるだけだった。意外とシンプル。
実際にはDIコンテナがオブジェクト生成時にこのへんの処理をやってくれるんだと思う。だからDIコンテナとAOPがセットになってんですかね。

用語はここ参照。
適用したい処理はIntercepterに、どこへ適用するかはPointcutに。
Aspectでふたつをむすびつける。

手元にあった s2-framework-2.4.34.jar を参照しました。

Pointcut

Pointcutインタフェースの定義は以下:

public interface Pointcut {
    boolean isApplied(Method method);
}

渡したメソッドにインターセプタを適用すべきか返す。

Pointcut pointcut = new PointcutImpl(new String[]{"getTime"});

PointcutImplのコンストラクタで「適用すべきメソッド名」の配列を渡してる。正規表現が使えるもよう。
他にもClassやMethodを渡すコンストラクタもあり。
isAppliedでは保持したメソッド/メソッド名と一致するかどうかみてるだけですね

Aspect

Aspect aspect = new AspectImpl(new TraceInterceptor(), pointcut);

AspectはMethodIntercepter,Pointcutを保持する単なるBean。

AopProxy

AopProxy aopProxy = new AopProxy(Date.class, new Aspect[]{aspect});

クラスとAspectからAspectが適用されたプロクシクラスのインスタンスを作るクラス。

最終的に呼ばれるコンストラクタはこれ。

public AopProxy(final Class targetClass, final Aspect[] aspects,
		final InterType[] interTypes, final Map parameters) {
	if ((aspects == null || aspects.length == 0)
			&& (interTypes == null || interTypes.length == 0)) {
		throw new EmptyRuntimeException("aspects and interTypes");
	}

	this.targetClass = targetClass;
	defaultPointcut = new PointcutImpl(targetClass);
	// 以降はあとで

interTypes,parametersにはnullが渡される。
targetClassがプロクシ対象。
defaultPointcutはPointcutImplのコンストラクタにtargetClass渡してて、これは

public PointcutImpl(Class targetClass) throws EmptyRuntimeException {
	if (targetClass == null) {
		throw new EmptyRuntimeException("targetClass");
	}
	setMethodNames(getMethodNames(targetClass));
}
private static String[] getMethodNames(Class targetClass) {
	Set methodNameSet = new HashSet();
	if (targetClass.isInterface()) {
		addInterfaceMethodNames(methodNameSet, targetClass);
	}
	for (Class clazz = targetClass; clazz != Object.class && clazz != null; clazz = clazz
			.getSuperclass()) {
		Class[] interfaces = clazz.getInterfaces();
		for (int i = 0; i < interfaces.length; ++i) {
			addInterfaceMethodNames(methodNameSet, interfaces[i]);
		}
	}
	if (methodNameSet.isEmpty()) {
		addClassMethodNames(methodNameSet, targetClass);
	}
	return (String[]) methodNameSet
			.toArray(new String[methodNameSet.size()]);

}

となって対象クラスが実装してる全インターフェースのメソッドが対象となるpointCut。
プロクシ作る都合上、インタフェース由来じゃないメソッドは対象とならないのかしら。

AopProxyコンストラクタ続き。

	weaver = new AspectWeaver(targetClass, parameters);
	setupAspects(aspects);
	weaver.setInterTypes(interTypes);
	enhancedClass = weaver.generateClass();
}

setupAspects(aspects)では

  • アスペクトにPointcutが設定されてなかったらデフォルトのを設定する
  • 対象クラスの各メソッドについて、AspectWeaver#setInterceptorsでweaverにAspectのリストを登録

という処理を行っている(リファクタリングしたほうが良くないかこれ)

weaver.setInterTypesは今回interTypeつかってないのでスルー。

AspectWeaver#generateClassでプロキシクラス生成してenhancedClassにセット。

AspectWeaver#setInterceptors

AopProxy#setupAspectsから呼ばれる。引数は(対象メソッド,そのメソッドに適用すべきインターセプタのリスト)。

public void setInterceptors(final Method method,
		final MethodInterceptor[] interceptors) {
	final String methodInvocationClassName = getMethodInvocationClassName(method);
	final MethodInvocationClassGenerator methodInvocationGenerator = new MethodInvocationClassGenerator(
			classPool, methodInvocationClassName, enhancedClassName);

	final String invokeSuperMethodName = createInvokeSuperMethod(method);
	methodInvocationGenerator.createProceedMethod(method,
			invokeSuperMethodName);
	enhancedClassGenerator.createTargetMethod(method,
			methodInvocationClassName);

	final Class methodInvocationClass = methodInvocationGenerator
			.toClass(ClassLoaderUtil.getClassLoader(targetClass));
	setStaticField(methodInvocationClass, "method", method);
	setStaticField(methodInvocationClass, "interceptors", interceptors);
	setStaticField(methodInvocationClass, "parameters", parameters);
	methodInvocationClassList.add(methodInvocationClass);
}

methodInvocationClassってなんだ…… 1メソッドインターセプトするために1クラス作られるということだろうか。
製作担当はMethodInvocationClassGenerator。

MethodInvocationClassGenerator

MethodInvocationクラスを生成するものらしい。
以下、proceed()を作るコード

public static String createProceedMethodSource(final Method targetMethod,
		final String enhancedClassName, final String invokeSuperMethodName) {
	final StringBuffer buf = new StringBuffer(1000);
	buf.append("{");
	buf.append("if (interceptorsIndex < interceptors.length) {");
	buf.append("return interceptors[interceptorsIndex++].invoke(this);");
	buf.append("}");
	buf.append(createReturnStatement(targetMethod, enhancedClassName,
			invokeSuperMethodName));
	buf.append("}");
	return new String(buf);
}

このへんのコードでインターセプタを起動してるようだ。

生成されるコードが

if(interceptorsIndex < interceptors.length) {
	return interceptors[interceptorsIndex++].invoke(this);
}
return メソッド本体()

に見えるんだが……。proceedされるたびにインターセプタを一個ずつ起動、最後にメソッド本体起動というのはいったいどういうことなのだろう。proceed()のつかわれかたがよくわからない。
→ インターセプタ起動時にMethodInvocationが渡されて、インターセプタ内で再度proceed()を呼ぶようになってた。

AspectWeaver#generateClass

AopProxyのコンストラクタから呼ばれる。設定されたアスペクトとクラスからプロクシクラスを生成する。

public Class generateClass() {
	if (enhancedClass == null) {
		enhancedClass = enhancedClassGenerator.toClass(ClassLoaderUtil
				.getClassLoader(targetClass));

		for (int i = 0; i < methodInvocationClassList.size(); ++i) {
			final Class methodInvocationClass = (Class) methodInvocationClassList
					.get(i);
			setStaticField(methodInvocationClass, "targetClass",
					targetClass);
		}
	}

	return enhancedClass;
}

enhancedClassフィールドはキャッシュ用だとおもう。
enhancedClass作って、ついでにmethodInvocationClassListの各々のtargetClassフィールドにターゲットクラスを設定(なんでこのタイミングで?)、その後enhancedClass返す。

EnhancedClassGenerator
public static String createTargetMethodSource(final Method method,
		final String methodInvocationClassName) {
	final StringBuffer buf = new StringBuffer(200);
	buf.append("Object result = new ").append(methodInvocationClassName)
			.append("(this, $args).proceed();");
	final Class returnType = method.getReturnType();
	if (returnType.equals(void.class)) {
		buf.append("return;");
	} else if (returnType.isPrimitive()) {
		buf.append("return ($r) ((result == null) ? ");
		if (returnType.equals(boolean.class)) {
			buf.append("false : ");
		} else {
			buf.append("0 : ");
		}
		buf.append(fromObject(returnType, "result")).append(");");
	} else {
		buf.append("return ($r) result;");
	}
	String code = new String(buf);

	final Class[] exceptionTypes = normalizeExceptionTypes(method
			.getExceptionTypes());
	if (exceptionTypes.length != 1
			|| !exceptionTypes[0].equals(Throwable.class)) {
		code = aroundTryCatchBlock(exceptionTypes, code);
	}

	return "{" + code + "}";
}

呼ばれたメソッドに対応したMethodInvocationを生成してproceed()する。

AopProxy#create()

Date proxy = (Date) aopProxy.create();

enhancedClassをインスタンシエートするだけですな。コンストラクタに引数渡すバージョンもあり。

public Object create() {
	return ClassUtil.newInstance(enhancedClass);
}

プロキシを介したメソッドの呼び出し

proxy.getTime();

getTime()用のMethodInvocation#proceedが呼ばれる→インターセプタ起動→Date#getTime起動→結果返る

デバッグモードでスタックトレースを見てみた。

$$java.util.Date$$EnhancedByS2AOP$$56f631(java.util.Date).getTime() 行: 866 [ローカル変数は使用不可] //本体
$$java.util.Date$$EnhancedByS2AOP$$56f631.$$getTime$$invokeSuperMethod$$() 行: 使用不可 [ローカル変数は使用不可] // ここでDate#getTime起動
$$java.util.Date$$EnhancedByS2AOP$$56f631$$MethodInvocation$$getTime0.proceed() 行: 使用不可 // interceptorsIndex=1なのでメソッド本体を呼ぶ
org.seasar.framework.aop.interceptors.TraceInterceptor.invoke(org.aopalliance.intercept.MethodInvocation) 行: 73 // トレース処理してから、渡されたMethodInvocationをproceed
$$java.util.Date$$EnhancedByS2AOP$$56f631$$MethodInvocation$$getTime0.proceed() 行: 使用不可 // TraceInterceptor.invoke(this)する
$$java.util.Date$$EnhancedByS2AOP$$56f631.getTime() 行: 使用不可 [ローカル変数は使用不可] // Date#getTime用のMethodInvocationを生成してproceed()
Main.main(java.lang.String[]) 行: 17 // proxy.getTime()

おおお、納得。次のインターセプタを起動するかどうかはインターセプタに任されるということか。

インターセプタが複数ある場合、

  1. MethodInvocation#proceed()
  2. Intercepter1#invoke(MethodInvocation)
  3. MethodInvocation#proceed()
  4. Intercepter2#invoke(MethodInvocation)
  5. ...
  6. MethodInvocation#proceed()
  7. ターゲットメソッド本体

という具合にコールスタックが深くなっていくわけか。インターセプタがMethodInvocation#proceed()呼ばずに独自の値を返すことも可能と。でもそうなると、最初のほうのインターセプタで中断された場合後続のインターセプタが起動しなくなるわけか。

結論

  • つかれた
  • ソースきれい
  • インターセプタの呼び出し順序ですが、たぶんAopProxyのコンストラクタのアスペクトリストに準ずる(MethodInvocationClassから参照されてるインターセプタリストがどこで設定されてるのかよくわからないんですが……)。ただし呼び出し順序が規格で保証されてるかというとされてない気がする、どうなんだろう。インターセプタのチェーンが途中で中断することを考えると順番重要だが、AopProxyのjavadocには渡すAspectの順序についての言及がない……

次はDIコンテナとAOPの連携部分でも読むか……