引自JAvatar AppFuse改造之Struts框架隔离

引自JAvatar AppFuse改造之Struts框架隔离

AppFuse改造之Struts框架隔离

关键字: Struts       

进入新的项目组,
checkout项目下来,
看了一下其整体结构与代码,哎,比较乱。
经过我的建议,部门经理让我对该项目进行全面重构。

首先,此项目是以AppFuse作为基础的,还记得robin说它就一toy
选择的模板是iBatis + Spring + Struts
我的第一步工作是要隔离Struts。

Struts是老牌的MVC实现,那个年代IoC和轻量级还没现在流行,框架侵入性也没有得到重视。
所以Struts的实现让应用程序严重依赖它:
1.所有控制器都必需继承Action类
2.所有数据封装类必需继承ActionForm
3.控制器方法execute必需返回ActionForward,与Struts藕合
4.控制器方法execute的参数ActionMapping,与Struts藕合
5.控制器方法execute的参数HttpServletRequest,HttpServletResponse,与Servlet容器藕合
6.由于Action类是Struts管理的,不支持Service类的IoC注入,除非将控制权委托给IoC容器,再配一遍(如:org.springframework.web.struts.DelegatingActionProxy)。

目标:
1.控制器不继承任何类,也不实现任何接口
2.数据封装Form类应为简单的POJO,不要继承ActionForm
3.execute返回值可以是任意对象(包括基本类型和void),
标准返回String,即forward的索引值,
如果返回其它类型对象就调用其toString。
如果返回类型为void或返回值为null,forward到默认的"success"
4和5.execute只传入POJO的Form,
如果该动作不需要Form数据,也可以保持空的参数列表。
如果有多个参数,第一个参数为Form(作为传入,也作为传出,这个是struts已经实现的规则),后面的都为传出对象,必需保证为POJO,传出对象会根据struts的action配置的scope,放入相应域。
6.支持IoC注入Service,即然IoC,当然不能依赖某个具体IoC容器,没有Spring一样运行。要不然会被ajoo一顿臭骂,什么什么? IoC还:容器类.getXXX()?
7.当然,还要实现一个线程安全的容器类,持有与Servlet相关的信息,
这样,若有特殊要求需要访问HttpServletRequest,HttpServletResponse
则可以通过:容器类.get当前线程容器().getRequest()方式获取。

最后类应该像这样:

java 代码
  1. // Action无任何要求(哦,不对,要求有一个没有参数的构造函数,不算太高吧^_^)   
  2. public class ItemAction {   
  3.        
  4.     private ItemService itemService;   
  5.        
  6.     // IoC注入   
  7.     public void setItemService(ItemService itemService) {   
  8.         this.itemService = itemService;   
  9.     }   
  10.        
  11.     // 只有一个forward "success" 时,也可以用void   
  12.     // ItemForm写实际类型,不要用Object,然后在函数内强制转型,多麻烦   
  13.     // 建议参数加final   
  14.     public String viewAllItems(final ItemForm itemForm) {   
  15.         itemForm.setItems(itemService.getItems());   
  16.         return "success";   
  17.     }   
  18.        
  19.     //多个跳转,返回String识别   
  20.     public String saveItem(final ItemForm itemForm) {   
  21.         return itemService.saveItem(itemForm.getItem()) ? "success" : "failure";   
  22.     }   
  23. }   

 

不用说,这样的类是易于测试的。
例如:

java 代码
  1. public void testRightAllViewItems() {   
  2.     ItemAction itemAction = new ItemAction();   
  3.     ItemService itemService = new ItemServiceMock();   
  4.     itemAction.setItemService(itemService);   
  5.   
  6.     ItemsForm itemsForm = new ItemsForm();   
  7.     String forward = itemAction.viewAllItems(itemsForm);   
  8.   
  9.     assertEquals("没有正确跳转!""success", forward);   
  10.     assertNotNull("没有向itemsForm写入数据!", itemsForm.getItems());   
  11.   
  12.     // 下面这些断言是判断和ItemServiceMock中写硬编码的值是否一样   
  13.     assertEquals("取得的items大小与ItemServiceMock中的不一致!"1, itemsForm.getItems().size());   
  14.     Item item = (Item) itemsForm.getItems().iterator().next();   
  15.     assertEquals("获取的item的Id不对!"new Long(5), item.getId());   
  16.     assertEquals("获取的item的CategoryId不对!"new Long(2), item.getCategoryId());   
  17.     assertEquals("获取的item的Name不对!""aaa", item.getName());   
  18.     assertEquals("获取的item的Price不对!"new Float(1.2), item.getPrice());   
  19. }   

当然还有测试传入一个null的ItemsForm等的其它测试,这里就不例举了。

 

好,明确目标后,开始重构,重构后要先保证以前的代码也能运行。

由于要使Action彻底独立,我暂时想到的办法是反射回调。
我先写一个通用的Action,然后回调具体的控制器类。
实现如下:
通用Action

java 代码
  1. package com.ynmp.webapp.frame;   
  2.   
  3. import java.lang.reflect.Method;   
  4. import java.util.Map;   
  5.   
  6. import javax.servlet.http.HttpServletRequest;   
  7. import javax.servlet.http.HttpServletResponse;   
  8.   
  9. import org.apache.commons.logging.Log;   
  10. import org.apache.commons.logging.LogFactory;   
  11. import org.apache.struts.action.Action;   
  12. import org.apache.struts.action.ActionForm;   
  13. import org.apache.struts.action.ActionForward;   
  14. import org.apache.struts.action.ActionMapping;   
  15.   
  16. import com.ynmp.webapp.frame.provider.ServiceProvider;   
  17. import com.ynmp.webapp.frame.provider.SpringServiceProvider;   
  18. import com.ynmp.webapp.frame.util.ClassUtils;   
  19.   
  20. public class BaseAction extends Action {   
  21.        
  22.     private static final Log log = LogFactory.getLog(BaseAction.class);   
  23.        
  24.     private static final String UNCALL_METHOD = "*";   
  25.        
  26.     private static final String SUCCESS_FORWARD = "success";   
  27.        
  28.     private static final String ERROR_FORWARD = "error";   
  29.        
  30.     public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {   
  31.         String forward;   
  32.         try {   
  33.             ActionContext.initCurrentContext(request, response);   
  34.             forward = getActionForward(mapping.getScope(), mapping.getParameter(), form);   
  35.         } catch (Exception e) {   
  36.             e.printStackTrace();   
  37.             log.warn(e);   
  38.             forward = ERROR_FORWARD;   
  39.             request.setAttribute("BeanActionException", e);   
  40.         }   
  41.         return mapping.findForward(forward);   
  42.     }   
  43.        
  44.     // 处理forward   
  45.     private String getActionForward(String scope, String config, Object model) throws Exception { // TODO Exception处理待重构   
  46.         String forward = SUCCESS_FORWARD;   
  47.         ActionConfig actionConfig = new ActionConfig(config);   
  48.         Object actionObject = populateActionClass(actionConfig.getClassName());   
  49.         Object returnObject = callActionMethod(actionObject, actionConfig.getMethodName(), model, scope);   
  50.         if (returnObject!= null && String.valueOf(returnObject) != null) {   
  51.             forward = String.valueOf(returnObject);   
  52.         }   
  53.         return forward;   
  54.     }   
  55.        
  56.     // 处理action类   
  57.     private Object populateActionClass(String className) throws Exception {   
  58.         Class actionClass = Class.forName(className);   
  59.         Object action = actionClass.newInstance();   
  60.         injectService(action);   
  61.         return action;   
  62.     }   
  63.        
  64.     // 简单实现IoC   
  65.     private void injectService(Object action) throws Exception {   
  66.         ServiceProvider serviceProvider = new SpringServiceProvider(getServlet()); // TODO 待重构为策略   
  67.         Method[] methods = action.getClass().getMethods();   
  68.         for (int i = 0; i < methods.length; i ++) {   
  69.             if (methods[i].getName().startsWith("set")) {   
  70.                 Class[] parameters = methods[i].getParameterTypes();   
  71.                 if (parameters != null && parameters.length == 1) {   
  72.                     String methodName = methods[i].getName();   
  73.                     String serviceName = methodName.substring(3,4).toLowerCase() + methodName.substring(4);   
  74.                     methods[i].invoke(action, new Object[]{serviceProvider.getService(serviceName, parameters[0])});   
  75.                 }   
  76.             }   
  77.         }   
  78.     }   
  79.        
  80.     // 处理action方法   
  81.     private Object callActionMethod(Object action, String methodName, Object model, String scope) throws Exception {   
  82.         if (UNCALL_METHOD.equals(methodName)) return null;   
  83.         Method actionMethod = ClassUtils.findMethodByName(action.getClass(), methodName);   
  84.         Object[] parameters = initParameters(actionMethod, model);   
  85.         Object returnObject = actionMethod.invoke(action, parameters);   
  86.         outParameters(getScopeMap(scope), parameters);   
  87.         return returnObject;   
  88.     }   
  89.        
  90.     // 组装action方法的参数列表   
  91.     private Object[] initParameters(Method actionMethod, Object model) throws Exception {   
  92.         Class[] parameterTypes = actionMethod.getParameterTypes();   
  93.         int parameterSize = parameterTypes.length;   
  94.         if (parameterSize == 0) {   
  95.             return new Object[0];   
  96.         } else if (parameterSize == 1) {   
  97.             return new Object[] {model};   
  98.         } else {   
  99.             Object[] parameters = new Object[parameterSize];   
  100.             parameters[0] = model;   
  101.             for (int i = 1; i < parameterSize; i ++) {   
  102.                 parameters[i] = parameterTypes[i].newInstance();   
  103.             }   
  104.             return parameters;   
  105.         }   
  106.     }   
  107.        
  108.     // 向指定范围域输出参数   
  109.     private void outParameters(Map scopeMap, Object[] parameters) throws Exception {   
  110.         if (parameters.length < 2return ;   
  111.         for (int i = 1; i < parameters.length; i ++) {   
  112.             String name = ClassUtils.getLowerClassName(parameters[i].getClass());   
  113.             scopeMap.put(name, parameters[i]);   
  114.         }   
  115.     }   
  116.        
  117.     // 通过scope配置找到相应域Map   
  118.     private Map getScopeMap(String scope) {   
  119.         if ("request".equals(scope)) {   
  120.             return ActionContext.getActionContext().getRequestMap();   
  121.         } else if ("session".equals(scope)) {   
  122.             return ActionContext.getActionContext().getSessionMap();   
  123.         } else if ("application".equals(scope)) {   
  124.             return ActionContext.getActionContext().getApplicationMap();   
  125.         }   
  126.         throw new RuntimeException("不合法的scope:" + scope + ",scope必需为request,session,application中的一个!");   
  127.     }   
  128. }   

 

IoC的Service供给接口

java 代码
  1. package com.ynmp.webapp.frame.provider;   
  2.   
  3. public interface ServiceProvider {   
  4.   
  5.     public Object getService(String serviceName, Class serviceClass) throws Exception;   
  6.   
  7. }   

 

Spring的Service供给实现,依赖Spring,作为一种策略应该不成问题。

java 代码
  1. package com.ynmp.webapp.frame.provider;   
  2.   
  3. import javax.servlet.http.HttpServlet;   
  4.   
  5. import org.springframework.context.ApplicationContext;   
  6. import org.springframework.web.context.support.WebApplicationContextUtils;   
  7.   
  8. public class SpringServiceProvider implements ServiceProvider {   
  9.        
  10.     private HttpServlet servlet;   
  11.        
  12.     public SpringServiceProvider(HttpServlet servlet) {   
  13.         this.servlet = servlet;   
  14.     }   
  15.   
  16.     public Object getService(String serviceName, Class serviceClass) throws Exception {   
  17.         ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servlet.getServletContext());   
  18.         Object serviceObject = ctx.getBean(serviceName);   
  19.         if (serviceObject == null) {   
  20.             throw new RuntimeException("在IoC容器中未找到引用:" + serviceName);   
  21.         }   
  22.         return serviceObject;   
  23.     }   
  24.   
  25. }   

 

线程安全的Servlet相关信息持有类,
还有几个Map的封装,不贴也应该猜得到,
就是map.put时调用request,session,cookie,application相应的setAttribute等,get也类似

代码
  1. package com.ynmp.webapp.frame;   
  2.   
  3. import java.util.HashMap;   
  4. import java.util.Map;   
  5.   
  6. import javax.servlet.http.HttpServletRequest;   
  7. import javax.servlet.http.HttpServletResponse;   
  8.   
  9. import com.ynmp.webapp.frame.map.ApplicationMap;   
  10. import com.ynmp.webapp.frame.map.CookieMap;   
  11. import com.ynmp.webapp.frame.map.ParameterMap;   
  12. import com.ynmp.webapp.frame.map.RequestMap;   
  13. import com.ynmp.webapp.frame.map.SessionMap;   
  14.   
  15. public class ActionContext {   
  16.   
  17.     private static final ThreadLocal localContext = new ThreadLocal();   
  18.   
  19.     private HttpServletRequest request;   
  20.   
  21.     private HttpServletResponse response;   
  22.   
  23.     private Map cookieMap;   
  24.   
  25.     private Map parameterMap;   
  26.   
  27.     private Map requestMap;   
  28.   
  29.     private Map sessionMap;   
  30.   
  31.     private Map applicationMap;   
  32.   
  33.     private ActionContext() {   
  34.         cookieMap = new HashMap();   
  35.         parameterMap = new HashMap();   
  36.         requestMap = new HashMap();   
  37.         sessionMap = new HashMap();   
  38.         applicationMap = new HashMap();   
  39.     }   
  40.   
  41.     static void initCurrentContext(HttpServletRequest request,   
  42.             HttpServletResponse response) {   
  43.         ActionContext ctx = getActionContext();   
  44.         ctx.request = request;   
  45.         ctx.response = response;   
  46.         ctx.cookieMap = null;   
  47.         ctx.parameterMap = null;   
  48.         ctx.requestMap = null;   
  49.         ctx.sessionMap = null;   
  50.         ctx.applicationMap = null;   
  51.     }   
  52.   
  53.     public static ActionContext getActionContext() {   
  54.         ActionContext ctx = (ActionContext) localContext.get();   
  55.         if (ctx == null) {   
  56.             ctx = new ActionContext();   
  57.             localContext.set(ctx);   
  58.         }   
  59.         return ctx;   
  60.     }   
  61.   
  62.     public Map getCookieMap() {   
  63.         if (cookieMap == null) {   
  64.             cookieMap = new CookieMap(request, response);   
  65.         }   
  66.         return cookieMap;   
  67.     }   
  68.   
  69.     public Map getParameterMap() {   
  70.         if (parameterMap == null) {   
  71.             parameterMap = new ParameterMap(request);   
  72.         }   
  73.         return parameterMap;   
  74.     }   
  75.   
  76.     public Map getRequestMap() {   
  77.         if (requestMap == null) {   
  78.             requestMap = new RequestMap(request);   
  79.         }   
  80.         return requestMap;   
  81.     }   
  82.   
  83.     public Map getSessionMap() {   
  84.         if (sessionMap == null) {   
  85.             sessionMap = new SessionMap(request);   
  86.         }   
  87.         return sessionMap;   
  88.     }   
  89.   
  90.     public Map getApplicationMap() {   
  91.         if (applicationMap == null) {   
  92.             applicationMap = new ApplicationMap(request);   
  93.         }   
  94.         return applicationMap;   
  95.     }   
  96.   
  97.     public HttpServletRequest getRequest() {   
  98.         return request;   
  99.     }   
  100.   
  101.     public HttpServletResponse getResponse() {   
  102.         return response;   
  103.     }   
  104.   
  105.     public String getAppURL() {   
  106.         StringBuffer url = new StringBuffer();   
  107.         int port = request.getServerPort();   
  108.         if (port < 0) {   
  109.             port = 80;   
  110.         }   
  111.         String scheme = request.getScheme();   
  112.         url.append(scheme);   
  113.         url.append("://");   
  114.         url.append(request.getServerName());   
  115.         if ((scheme.equals("http") && (port != 80))   
  116.                 || (scheme.equals("https") && (port != 443))) {   
  117.             url.append(':');   
  118.             url.append(port);   
  119.         }   
  120.         url.append(request.getContextPath());   
  121.         return url.toString();   
  122.     }   
  123.   
  124. }   

 

Struts配置中,parameter属性是作为扩展用的,所以我们可以利用它。
改动:
parameter指定控制器类的类名和方法名,格式为:包名.类名:函数名
type固定为com.ynmp.webapp.frame.BaseAction
如:

xml 代码
  1. <action path="/item_list" parameter="com.ynmp.webapp.action.ItemAction:viewAllItems" name="itemsForm" type="com.ynmp.webapp.frame.BaseAction">  
  2.     <forward name="success" path="/item_list.jsp" />  
  3. </action>  

 

配置管理类:

java 代码
  1. package com.ynmp.webapp.frame;   
  2.   
  3. public class ActionConfig {   
  4.        
  5.     private static final String ACTION_CONFIG_REGEX = "^([A-Z|a-z|_]+\\.)+[A-Z|a-z|_]+\\:(([A-Z|a-z|_]+)|\\*)$";   
  6.        
  7.     private String className;   
  8.        
  9.     private String methodName;   
  10.        
  11.     public ActionConfig(String config) {   
  12.         if (config == null    
  13.                 || config.length() == 0    
  14.                 || ! config.replaceAll(" """).matches(ACTION_CONFIG_REGEX)) {   
  15.             throw new RuntimeException("Parameter=\"" + config + "\" 格式不合法!应为:包名.类名:方法名,如:com.company.UserAction:login");   
  16.         }   
  17.         int index = config.indexOf(":");   
  18.         className = config.substring(0, index).trim();   
  19.         methodName = config.substring(index + 1).trim();   
  20.     }   
  21.        
  22.     public ActionConfig(String className, String methodName) {   
  23.         this.className = className;   
  24.         this.methodName = methodName;   
  25.     }   
  26.        
  27.     public String getClassName() {   
  28.         return className;   
  29.     }   
  30.   
  31.     public String getMethodName() {   
  32.         return methodName;   
  33.     }   
  34.   
  35.     public void setClassName(String className) {   
  36.         this.className = className;   
  37.     }   
  38.   
  39.     public void setMethodName(String methodName) {   
  40.         this.methodName = methodName;   
  41.     }   
  42.        
  43. }   

 

Class辅助工具类

java 代码
  1. package com.ynmp.webapp.frame.util;   
  2.   
  3. import java.lang.reflect.Method;   
  4.   
  5. public class ClassUtils {   
  6.        
  7.     public static Method findMethodByName(Class clazz, String methodName) {   
  8.         int count = 0;   
  9.         Method actionMethod = null;   
  10.         Method[] methods = clazz.getMethods();   
  11.         for (int i = 0; i < methods.length; i ++) {   
  12.             if (methods[i].getName().equals(methodName)) {   
  13.                 // 其实也可以不检查函数是否重载,直接返回第一个定义的,   
  14.                 // 但这样有可能会使程序员迷惑,还是检查一下重载。   
  15.                 count ++;   
  16.                 if (count > 1) {   
  17.                     throw new RuntimeException(clazz + " 类中有重载的同名方法: " + methodName + ",无法判定使用哪一个!");   
  18.                 }   
  19.                 actionMethod = methods[i];   
  20.             }   
  21.         }   
  22.         if (count == 0 || actionMethod == null) {   
  23.             throw new RuntimeException(clazz + " 类中找到不方法: " + methodName);   
  24.         }   
  25.         return actionMethod;   
  26.     }   
  27.        
  28.     public static String getLowerClassName(Class clazz) {   
  29.         String className = clazz.getName();   
  30.         int index = className.lastIndexOf(".");   
  31.         if (index != -1) {   
  32.             className = className.substring(index + 1);   
  33.         }   
  34.         return className.substring(0,1).toLowerCase() + className.substring(1);   
  35.     }   
  36.        
  37. }   

 

其它JUnit的测试类就不贴了。

现在Action解放了,Form对象还没解放
还是必需继承ActionForm,
因为<form-bean配置会检查该对象是否继承于ActionForm,否则报错。
验证框架和ActionForm也有点藕合。
我现在还没有想到好的办法,先搞个权宜之计,
写了一个BaseForm,继承于ActionForm,然后将从ActionForm中继承来的方法全给final掉
其它Form都继承于BaseForm,这样先保证Form不会重写ActionForm中的方法,
看起来像个POJO,若以后能去掉ActionForm,就只要改BaseForm

你可能感兴趣的:(引自JAvatar AppFuse改造之Struts框架隔离)