DispatchActionとSpringAOP

Struts1.2.9から追加されたEventDispatchActionは、submitボタンのname属性(html:submitのproperty属性)を見てActionのメソッドを決定してくれるので、Strutsで1画面1Actionの形態がかなり取りやすくなりました。最近のWebフレームワークの方向性にも通じるものがあるので、今の仕事でもこのActionを主に利用しています。・・・ただし、Springとの組み合わせで使う場合に落とし穴がありました。
SpringはProxyベースのAOPを採用しているので、オブジェクト内で自分のメソッドを呼んだ場合は、アスペクトが適用されません。なので、executeメソッド内から実行メソッドが呼ばれるDispatchActionでは、AOPが適用されないことになってしまいます。Seasar2なら何の問題も無く使えるので、ついつい同じ感覚で考えてしまっていました・・・
Springのドキュメントによると、対策方法としては、「オブジェクト内でProxyFactoryを使って自身のProxyを作成してそこからメソッドを呼ぶ」か「AspectJのload time weavingを使う」かの2通りの方法があるらしいです。前者は思いっきり実装がSpringに依存してしまうし、後者はagentによるweavingと、APServer側の対応が必要らしいです。・・・両方選択するには厳しい・・・
というわけで、RequestProcessorを拡張して、DispatchActionのexecuteメソッド内のロジックを全部RequestProcessor側に移してみました。
(2008/04/28追記:例外処理がダメダメだったので修正(苦笑))

package springsample.spring.web.struts;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.actions.DispatchAction;
import org.apache.struts.util.MessageResources;
import org.springframework.web.struts.DelegatingTilesRequestProcessor;

public class DispatchActionDelegatingTilesRequestProcessor extends
		DelegatingTilesRequestProcessor {
	/**
	 * The message resources for this package.
	 */
	protected static MessageResources messages = MessageResources
			.getMessageResources("org.apache.struts.actions.LocalStrings");

	@Override
	protected ActionForward processActionPerform(HttpServletRequest request,
			HttpServletResponse response, Action action, ActionForm form,
			ActionMapping mapping) throws IOException, ServletException {

		if (!(action instanceof DispatchAction)) {
			return super.processActionPerform(request, response, action, form,
					mapping);
		}
		DispatchAction da = DispatchAction.class.cast(action);

		try {
			return execute(da, mapping, form, request, response);
		} catch (Exception e) {
			if (e instanceof InvocationTargetException
					&& e.getCause() instanceof Exception) {
				e = Exception.class.cast(e.getCause());
			}
			return (processException(request, response, e, form, mapping));
		}
	}

	protected ActionForward execute(DispatchAction action,
			ActionMapping mapping, ActionForm form, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		if (isCancelled(action, request)) {
			ActionForward af = cancelled(action, mapping, form, request,
					response);
			if (af != null) {
				return af;
			}
		}

		// Get the parameter. This could be overridden in subclasses.
		String parameter = getParameter(action, mapping, form, request,
				response);

		// Get the method's name. This could be overridden in subclasses.
		String name = getMethodName(action, mapping, form, request, response,
				parameter);

		// Prevent recursive calls
		if ("execute".equals(name) || "perform".equals(name)) {
			String message = messages.getMessage("dispatch.recursive", mapping
					.getPath());

			log.error(message);
			throw new ServletException(message);
		}

		// Invoke the named method, and return the result
		return dispatchMethod(action, mapping, form, request, response, name);

	}

	protected boolean isCancelled(DispatchAction action,
			HttpServletRequest request) throws Exception {
		Method method = Action.class.getDeclaredMethod("isCancelled",
				HttpServletRequest.class);
		method.setAccessible(true);
		return Boolean.class.cast(method.invoke(action, request));
	}

	protected ActionForward cancelled(DispatchAction action,
			ActionMapping mapping, ActionForm form, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		Method method = DispatchAction.class.getDeclaredMethod("cancelled",
				ActionMapping.class, ActionForm.class,
				HttpServletRequest.class, HttpServletResponse.class);
		method.setAccessible(true);
		return ActionForward.class.cast(method.invoke(action, mapping, form,
				request, response));
	}

	protected String getParameter(DispatchAction action, ActionMapping mapping,
			ActionForm form, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		Method method = DispatchAction.class.getDeclaredMethod("getParameter",
				ActionMapping.class, ActionForm.class,
				HttpServletRequest.class, HttpServletResponse.class);
		method.setAccessible(true);
		return String.class.cast(method.invoke(action, mapping, form, request,
				response));
	}

	protected String getMethodName(DispatchAction action,
			ActionMapping mapping, ActionForm form, HttpServletRequest request,
			HttpServletResponse response, String parameter) throws Exception {
		Method method = DispatchAction.class.getDeclaredMethod("getMethodName",
				ActionMapping.class, ActionForm.class,
				HttpServletRequest.class, HttpServletResponse.class,
				String.class);
		method.setAccessible(true);
		return String.class.cast(method.invoke(action, mapping, form, request,
				response, parameter));
	}

	protected ActionForward dispatchMethod(DispatchAction action,
			ActionMapping mapping, ActionForm form, HttpServletRequest request,
			HttpServletResponse response, String name) throws Exception {

		// Make sure we have a valid method name to call.
		// This may be null if the user hacks the query string.
		if (name == null) {
			return unspecified(action, mapping, form, request, response);
		}

		// Identify the method object to be dispatched to
		Method method = getMethod(action, name);

		ActionForward forward = null;
		Object args[] = { mapping, form, request, response };
		forward = (ActionForward) method.invoke(action, args);

		// Return the returned ActionForward instance
		return (forward);
	}

	protected ActionForward unspecified(DispatchAction action,
			ActionMapping mapping, ActionForm form, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		Method method = DispatchAction.class.getDeclaredMethod("unspecified",
				ActionMapping.class, ActionForm.class,
				HttpServletRequest.class, HttpServletResponse.class);
		method.setAccessible(true);
		return ActionForward.class.cast(method.invoke(action, mapping, form,
				request, response));
	}

	protected Method getMethod(DispatchAction action, String name)
			throws Exception {
		Method method = DispatchAction.class.getDeclaredMethod("getMethod",
				String.class);
		method.setAccessible(true);
		return Method.class.cast(method.invoke(action, name));
	}

}

最終的に、RequestProcessorからDispatchActionの実行メソッドを呼び出すことになるので、これでAOPが効くことが確認できました。
DispatchAction内で、自身のメソッドを呼び出している部分はリフレクションでそのまま呼び出しています。これならどのDispatchActionでも対応出来る筈・・・
まぁでもかなり強引な方法だなぁ・・・SpringユーザはこのAOP制限に不満を感じないのだろうか?