原创作者: fins   阅读:4807次   评论:9条   更新时间:2011-06-01    
目前流行的新型的MVC框架 几乎都在"增强单元测试能力"上做了很多文章.
目的就是让 Controller 可以脱离web容器单独进行单元测试.

大多数采用的方法都是使 Controller 中的方法的参数 和 返回值 与 j2ee特有的类(如HttpXXXX)无关.

例如 传入的 是若干个 HashMap ,返回的是纯的字符串.
这样在单元测试的时候,只要new一个Controller,准备一些装有测试数据的Map,
然后执行相关的Controller方法,再然后看看返回值,就可以了.

显然,流行的Struts 1.X (>=1.2) 是不具备这样的特性的.
如果您现在有一个遗留的使用Struts 1.X(>=1.2) 的系统,或者因为人力资源的原因,不能立即引入ww或Struts2.x,
那么如何让您的Action类似的可测性了呢(其实除了可测试性,这样的改进往往也会大大的减少代码的开发量).

下面我就把我的一些心得写出来,和大家一起分享,欢迎大家拍砖.
注意:以下讨论均以改造 DispatchAction 为例(不使用DispatchAction的情况应该比较少吧 呵呵).

先说明一下改造的目的:
简化开发,减少代码量
增强单元测试能力
全面兼容现有DispatchAction(这点很重要)

一个传统的 DispatchAction 代码结构
public class MyClassicAction extends DispatchAction {
	// 一个添加user的操作
	public ActionForward addUser(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// 1 取得参数 ...
		// 2 执行相关的BO DAO方法
		// 3 取得执行后的返回值
		
		 if (添加成功) {
			return mapping.findForward("Succee");
		 }
		
		return mapping.findForward("Fail");
		// 还有一种情况是 返回 new ActionForward(....).
	}
}

对于上面的代码大家再熟悉不过了,我们现在要做的事情就是通过改造,让上面的action方法变成下面的样子.


	// 一个添加user的操作
	public String addUser( 传入与web容器无关的参数 )
			throws Exception {
		// 1 取得参数 ...
		// 2 执行相关的BO DAO方法
		// 3 取得执行后的返回值
		
		 if (添加成功) {
			return "Succee";
		 }
		
		return "Fail";
		// 对于action中返回 new ActionForward(....).的情况,我们可以设定另外一种特殊的前缀.
		// 例如  return "NEW:/addUserFail.jsp";
	}



上面的代码是一个最终的目标,我们先一步一步来.
首先这种改造我们一定要从 Struts 提供的 DispatchAction 做起.
总体思路很明显:
自己写个类 继承 DispatchAction ,并且添加或修改一些方法, 然后项目中的其他的DispatchAction都继承这个自己写的DispatchAction类.

简单说一下Struts 的 DispatchAction的工作流程.

首先 它也是一个action,所以请求来了 他会自动去执行execute方法
execute方法内部做一些操作后,会调用 dispatchMethod 方法
dispatchMethod方法会,调用getMethod方法 来取得欲执行的方法,并且执行之

改造的核心就是围绕最关键的dispatchMethod方法.
而由于我们要"全面兼容现有DispatchAction",所以对于getMethod我们不做修改,而是选择增加一个类似的方法 getMethodTD

首先写一个新的 .DispatchAction基类,他继承org.apache.struts.actions.DispatchAction.


public class TDDispatchAction extends DispatchAction {
	
    protected Class[] typesTD = {TDServletWrapper.class };
    
    protected final static String TD_METHODCACHE_PREFIX="TD_METHODCACHE_PREFIX";
    protected final static String TD_DEFAULT_METHOD="defaultMethod";
    protected final static String TD_NEW_FORWARD_PREFIX="NEW:";
    

    
    protected Method getMethodTD(String name)
        throws NoSuchMethodException {

    	String cacheKey=TD_METHODCACHE_PREFIX+name;
    	
        synchronized (methods) {
            Method method = (Method) methods.get(cacheKey);

            if (method == null) {
                method = clazz.getMethod(name, typesTD);
                methods.put(cacheKey, method);
            }

            return (method);
        }
    }

// 对unspecified这个方法的扩展实际上与本次讨论无关,只是为我们框架加的一个小功能
// 提供一个默认的方法.也就是说,如果您的action中,有名为 defaultMethod 的方法,那么无需在请求的参数中加入相关信息,就会自动执行.
    protected ActionForward unspecified(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		return dispatchMethod(mapping, form, request, response, TD_DEFAULT_METHOD);
	}


   protected ActionForward dispatchMethod(ActionMapping mapping,
        ActionForm form, HttpServletRequest request,
        HttpServletResponse response, String name)
        throws Exception {
	// 后面再说

	}
 }



首先来看一下这个  getMethodTD 方法.
原getMethod(String name)方法的作用是:
取得一个名字为name,参数类型为 ActionMapping , ActionForm , HttpServletRequest ,HttpServletResponse 的一个方法.

而这个新写的 getMethodTD 方法的作用是:
取得一个名字为name,参数类型为 TDServletWrapper 的一个方法.
这个大家和 原getMethod一对比就能看出来作用和差异了,注意那个cacheKey=TD_METHODCACHE_PREFIX+name,这个是必须的,不能直接用name做key.

其中这个 TDServletWrapper 就是封装的一个"可以"与web容器脱离的对象,注意,这里用的是可以.而不是直接就脱离了.
对此后面再做解释.

要添加的 getMethodTD  方法添加完了.
下面来看一下重点 改造 dispatchMethod方法:

原dispatchMethod方法的流程其实很简单:
1 调用 getMethod(name)方法,取得真正要执行的方法 并执行(传入的参数类型为 ActionMapping , ActionForm , HttpServletRequest ,HttpServletResponse ).
2 返回 方法执行的 结果 (返回结果类型为 ActionForward ).

改造后的目的是:

1 调用 getMethodTD(name)方法,取得真正要执行的方法并执行(传入的参数类型为TDServletWrapper ).
2 取得 方法执行的 结果 (返回结果类型为 String ).
3 根据这个返回的String, 创建ActionForward对象,并返回

但是我们还有一个目的是"兼容",所以最后确定的流程是

1 调用 getMethod(name)方法,取得真正要执行的方法,如果取得,则按原方式继续.
2 如果没有取得 调用 getMethodTD(name)方法,取得真正要执行的方法,若取得则执行.
3 根据返回结果的类型不同 做不同的操作.

// 改造的 dispatchMethod方法,重点看 //TD...字样所在的代码段
    protected ActionForward dispatchMethod(ActionMapping mapping,
        ActionForm form, HttpServletRequest request,
        HttpServletResponse response, String name)
        throws Exception {

        if (name == null) {
            return this.unspecified(mapping, form, request, response);
        }

        Method method = null;
        
//TD:   Modified by Wei Zijun
        boolean useTDMethod=false;
        try {
        	// 按传统方式取 action方法
            method = getMethod(name);
        } catch (NoSuchMethodException ex) {
        	try {
        	// 没取到的话 再按新方式取 action方法
        	 method = getMethodTD(name);
        	 // 标识一下到底是使用的原始方法 还是新方法
        	 useTDMethod=true;
	        } catch (NoSuchMethodException e) {
	            String message =
	                messages.getMessage("dispatch.method", mapping.getPath(), name);
	
	            log.error(message, e);
	
	            String userMsg =
	                messages.getMessage("dispatch.method.user", mapping.getPath());
	            throw new NoSuchMethodException(userMsg);
	        }
        	
        }

        ActionForward forward = null;

        try {
//TD:	Modified by Wei Zijun
        	Object forwardR=null;
        	
        	if (!useTDMethod){
        	// 如果是原始 action 方法
        		Object[] args = { mapping, form, request, response };
        		forwardR= method.invoke(this, args);
        		
        	}else{
        	// 如果是新 action 方法
        		// TDServletWrapperFactory.getInstance创建一个封装的对象
        		Object[] args = { TDServletWrapperFactory.getInstance(mapping, form,request, response) };
        		forwardR= method.invoke(this, args);
        	}
        	
    		if (forwardR!=null){
    			if (forwardR instanceof ActionForward){
    				forward=(ActionForward)forwardR;
    			}else{
    				// 如果返回值是字符串:
    				// 以 TD_NEW_FORWARD_PREFIX("NEW:")开头,则new ActionForward;
    				// 否则 调用 mapping.findForward.
        			if ( (String.valueOf(forwardR) ).toUpperCase().startsWith(TD_NEW_FORWARD_PREFIX)){
        				forward = new ActionForward(String.valueOf(forwardR).substring(TD_NEW_FORWARD_PREFIX.length()) );
            		}else{
            			forward = mapping.findForward(String.valueOf(forwardR));
        			}
    			}
    		}
    		
        } catch (ClassCastException e) {
            String message =
                messages.getMessage("dispatch.return", mapping.getPath(), name);

            log.error(message, e);
            throw e;
        } catch (IllegalAccessException e) {
            String message =
                messages.getMessage("dispatch.error", mapping.getPath(), name);

            log.error(message, e);
            throw e;
        } catch (InvocationTargetException e) {

            Throwable t = e.getTargetException();

            if (t instanceof Exception) {
                throw ((Exception) t);
            } else {
                String message =
                    messages.getMessage("dispatch.error", mapping.getPath(),
                        name);

                log.error(message, e);
                throw new ServletException(t);
            }
        }

        return (forward);
    }



这样做完后,我们的action就可以支持下面的任何一种类型的方法了:

// 传统方式
public ActionForward testSubmit(ActionMapping mapping,ActionForm form, HttpServletRequest request,HttpServletResponse response) throws Exception;

// 新方式
public String testSubmit1(TDServletWrapper servletWrapper) throws Exception;

// 以下两者是则中方式.
public String testSubmit2(ActionMapping mapping,ActionForm form, HttpServletRequest request,HttpServletResponse response) throws Exception;

public ActionForward testSubmit3(TDServletWrapper servletWrapper) throws Exception;



下面简单说一下 TDServletWrapper .
这个类实际上就是封装了 ActionMapping mapping,ActionForm form, HttpServletRequest request,HttpServletResponse response,
同时提供了一些用来简化开发的方法,例如

public String getParameter(String key,String ifNull);
public Integer getIntegerParameter(String key);
public void bind(Object bean);
等等.
不再具体叙述,看一下代码就应该都明白了.


TDServletWrapper 是一接口,他有两个实现:
TDServletWrapperTestImpl 为测试专用的,是一个与web容器无关的实现.
他主要就是利用"模拟"request response的方式来处理
目前他的实现还不是很完全(例如没有模拟session等),大家可以根据自己的需求来补充.

大家也可以根据这个原理 不用TDServletWrapper ,而完全的使用Map等,
总之,通过dispatchMethod的改造,几乎可以实现你全部的合理的想法(别超出Struts1的能力极限就行),我在这里提供的只是一个思路而已.



经过上面的改造,一个可测试的action就诞生了:

public class MyClassicAction extends TDDispatchAction {
	// 一个添加user的操作
	public ActionForward addUser(TDServletWrapper servletWrapper) throws Exception {
		
		UserInfoVO vo=new UserInfoVO();
		servletWrapper.bind(vo);
	
		// 校验,并执行相关DAO操作

		 if ( 添加成功 ) {
			return "Succee";
		 }
		
		return "Fail";

	}
}


测试的可以这样写:
	public static void testAddUser() throws Exception {
		// 创建一个action对象

		TestAction test=new TestAction();

		//准备一些测试数据 放入map里,模仿request的Parameters
		Map requestParameters=new HashMap();
		
		requestParameters.put("userName", "asdqe");
		requestParameters.put("password", "mghjghj");
		requestParameters.put("userRole", "1");
		requestParameters.put("email", "ut_email");
		requestParameters.put("gender", "1");
		requestParameters.put("age", "22");
		
		// TDServletWrapperFactory.getTestInstance 方法用来取得一个脱离web容器的 TDServletWrapper对象 )
		Assert.isTrue("Sucess".equals(test.testSubmit1(TDServletWrapperFactory.getTestInstance(requestParameters))) );

	}


至此全部改造完毕.
这样的改造对于大家来说也许没什么意义,不过多少也是一种尝试.
由于Struts1.X(>=1.2)的 局限性,以及我个人能力的局限性,很多改造还不够彻底 不够合理,
对于前者我无能为力,对于后者,还恳请大家多多指正我的不足 谢谢了先.

上次发了一篇对 spring jdbctemplate的改造,这次又发了一篇对 struts 1 的改造,
(其实我那个ecside 也是对老版extremecomponents的改造)
也许很多人会觉得我太落伍了,这些老掉牙的东西还改造个什么劲儿啊 呵呵
其实,很多时候,对于我来说,改造就是一个智力游戏,在一些现有的东西实现一些自己的新的想法,这样的感觉总是快乐的.

而且,我们的项目就是用的老技术 老东西,在不能换新东西的情况下,努力的挖掘现有东西的剩余价值,也许更具有实际意义.

:D


评论 共 9 条 请登录后发表评论
9 楼 keynescao 2009-11-27 00:07
挖掘剩余价值也是一种自我提升!
8 楼 sunwenran 2009-09-27 10:52
实在不敢恭维 。。。用反射的坏处楼主自己看看
7 楼 710346072 2009-09-25 09:04
6 楼 gao0516 2009-07-24 15:07
認同你的想法,我們應該向你學習。
5 楼 yun_kai_123 2009-07-22 11:45
  // 如果是新 action 方法  
                // TDServletWrapperFactory.getInstance创建一个封装的对象  
                Object[] args = { TDServletWrapperFactory.getInstance(mapping, form,request, response) };

版主,我想知道这段代码的奥秘?尤其是TDServletWrapperFactory这个工厂中的实现。能不能给小弟一个完整的例子?谢谢。想学习!!
4 楼 果0o冻 2009-06-14 12:04
学习了, LZ思想很好,向你学习!
3 楼 zzmjxy 2009-03-29 01:47
system.out.println("good")
2 楼 yzzlijun 2009-02-15 19:48
真是厉害啊.
1 楼 sunzhicheng 2009-02-11 15:12
不错!欣赏你的思想!向你学习

发表评论

您还没有登录,请您登录后再发表评论

文章信息

  • letle在2007-06-25创建
  • letle在2011-06-01更新
Global site tag (gtag.js) - Google Analytics