进入新的项目组,
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()方式获取。
最后类应该像这样:
// Action无任何要求(哦,不对,要求有一个没有参数的构造函数,不算太高吧^_^)
public class ItemAction {
private ItemService itemService;
// IoC注入
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
// 只有一个forward "success" 时,也可以用void
// ItemForm写实际类型,不要用Object,然后在函数内强制转型,多麻烦
// 建议参数加final
public String viewAllItems(final ItemForm itemForm) {
itemForm.setItems(itemService.getItems());
return "success";
}
//多个跳转,返回String识别
public String saveItem(final ItemForm itemForm) {
return itemService.saveItem(itemForm.getItem()) ? "success" : "failure";
}
}
不用说,这样的类是易于测试的。
例如:
public void testRightAllViewItems() {
ItemAction itemAction = new ItemAction();
ItemService itemService = new ItemServiceMock();
itemAction.setItemService(itemService);
ItemsForm itemsForm = new ItemsForm();
String forward = itemAction.viewAllItems(itemsForm);
assertEquals("没有正确跳转!", "success", forward);
assertNotNull("没有向itemsForm写入数据!", itemsForm.getItems());
// 下面这些断言是判断和ItemServiceMock中写硬编码的值是否一样
assertEquals("取得的items大小与ItemServiceMock中的不一致!", 1, itemsForm.getItems().size());
Item item = (Item) itemsForm.getItems().iterator().next();
assertEquals("获取的item的Id不对!", new Long(5), item.getId());
assertEquals("获取的item的CategoryId不对!", new Long(2), item.getCategoryId());
assertEquals("获取的item的Name不对!", "aaa", item.getName());
assertEquals("获取的item的Price不对!", new Float(1.2), item.getPrice());
}
当然还有测试传入一个null的ItemsForm等的其它测试,这里就不例举了。
好,明确目标后,开始重构,重构后要先保证以前的代码也能运行。
由于要使Action彻底独立,我暂时想到的办法是反射回调。
我先写一个通用的Action,然后回调具体的控制器类。
实现如下:
通用Action
package com.ynmp.webapp.frame;
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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 com.ynmp.webapp.frame.provider.ServiceProvider;
import com.ynmp.webapp.frame.provider.SpringServiceProvider;
import com.ynmp.webapp.frame.util.ClassUtils;
public class BaseAction extends Action {
private static final Log log = LogFactory.getLog(BaseAction.class);
private static final String UNCALL_METHOD = "*";
private static final String SUCCESS_FORWARD = "success";
private static final String ERROR_FORWARD = "error";
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
String forward;
try {
ActionContext.initCurrentContext(request, response);
forward = getActionForward(mapping.getScope(), mapping.getParameter(), form);
} catch (Exception e) {
e.printStackTrace();
log.warn(e);
forward = ERROR_FORWARD;
request.setAttribute("BeanActionException", e);
}
return mapping.findForward(forward);
}
// 处理forward
private String getActionForward(String scope, String config, Object model) throws Exception { // TODO Exception处理待重构
String forward = SUCCESS_FORWARD;
ActionConfig actionConfig = new ActionConfig(config);
Object actionObject = populateActionClass(actionConfig.getClassName());
Object returnObject = callActionMethod(actionObject, actionConfig.getMethodName(), model, scope);
if (returnObject!= null && String.valueOf(returnObject) != null) {
forward = String.valueOf(returnObject);
}
return forward;
}
// 处理action类
private Object populateActionClass(String className) throws Exception {
Class actionClass = Class.forName(className);
Object action = actionClass.newInstance();
injectService(action);
return action;
}
// 简单实现IoC
private void injectService(Object action) throws Exception {
ServiceProvider serviceProvider = new SpringServiceProvider(getServlet()); // TODO 待重构为策略
Method[] methods = action.getClass().getMethods();
for (int i = 0; i < methods.length; i ++) {
if (methods[i].getName().startsWith("set")) {
Class[] parameters = methods[i].getParameterTypes();
if (parameters != null && parameters.length == 1) {
String methodName = methods[i].getName();
String serviceName = methodName.substring(3,4).toLowerCase() + methodName.substring(4);
methods[i].invoke(action, new Object[]{serviceProvider.getService(serviceName, parameters[0])});
}
}
}
}
// 处理action方法
private Object callActionMethod(Object action, String methodName, Object model, String scope) throws Exception {
if (UNCALL_METHOD.equals(methodName)) return null;
Method actionMethod = ClassUtils.findMethodByName(action.getClass(), methodName);
Object[] parameters = initParameters(actionMethod, model);
Object returnObject = actionMethod.invoke(action, parameters);
outParameters(getScopeMap(scope), parameters);
return returnObject;
}
// 组装action方法的参数列表
private Object[] initParameters(Method actionMethod, Object model) throws Exception {
Class[] parameterTypes = actionMethod.getParameterTypes();
int parameterSize = parameterTypes.length;
if (parameterSize == 0) {
return new Object[0];
} else if (parameterSize == 1) {
return new Object[] {model};
} else {
Object[] parameters = new Object[parameterSize];
parameters[0] = model;
for (int i = 1; i < parameterSize; i ++) {
parameters[i] = parameterTypes[i].newInstance();
}
return parameters;
}
}
// 向指定范围域输出参数
private void outParameters(Map scopeMap, Object[] parameters) throws Exception {
if (parameters.length < 2) return ;
for (int i = 1; i < parameters.length; i ++) {
String name = ClassUtils.getLowerClassName(parameters[i].getClass());
scopeMap.put(name, parameters[i]);
}
}
// 通过scope配置找到相应域Map
private Map getScopeMap(String scope) {
if ("request".equals(scope)) {
return ActionContext.getActionContext().getRequestMap();
} else if ("session".equals(scope)) {
return ActionContext.getActionContext().getSessionMap();
} else if ("application".equals(scope)) {
return ActionContext.getActionContext().getApplicationMap();
}
throw new RuntimeException("不合法的scope:" + scope + ",scope必需为request,session,application中的一个!");
}
}
IoC的Service供给接口
package com.ynmp.webapp.frame.provider;
public interface ServiceProvider {
public Object getService(String serviceName, Class serviceClass) throws Exception;
}
Spring的Service供给实现,依赖Spring,作为一种策略应该不成问题。
package com.ynmp.webapp.frame.provider;
import javax.servlet.http.HttpServlet;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
public class SpringServiceProvider implements ServiceProvider {
private HttpServlet servlet;
public SpringServiceProvider(HttpServlet servlet) {
this.servlet = servlet;
}
public Object getService(String serviceName, Class serviceClass) throws Exception {
ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servlet.getServletContext());
Object serviceObject = ctx.getBean(serviceName);
if (serviceObject == null) {
throw new RuntimeException("在IoC容器中未找到引用:" + serviceName);
}
return serviceObject;
}
}
线程安全的Servlet相关信息持有类,
还有几个Map的封装,不贴也应该猜得到,
就是map.put时调用request,session,cookie,application相应的setAttribute等,get也类似
package com.ynmp.webapp.frame;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ynmp.webapp.frame.map.ApplicationMap;
import com.ynmp.webapp.frame.map.CookieMap;
import com.ynmp.webapp.frame.map.ParameterMap;
import com.ynmp.webapp.frame.map.RequestMap;
import com.ynmp.webapp.frame.map.SessionMap;
public class ActionContext {
private static final ThreadLocal localContext = new ThreadLocal();
private HttpServletRequest request;
private HttpServletResponse response;
private Map cookieMap;
private Map parameterMap;
private Map requestMap;
private Map sessionMap;
private Map applicationMap;
private ActionContext() {
cookieMap = new HashMap();
parameterMap = new HashMap();
requestMap = new HashMap();
sessionMap = new HashMap();
applicationMap = new HashMap();
}
static void initCurrentContext(HttpServletRequest request,
HttpServletResponse response) {
ActionContext ctx = getActionContext();
ctx.request = request;
ctx.response = response;
ctx.cookieMap = null;
ctx.parameterMap = null;
ctx.requestMap = null;
ctx.sessionMap = null;
ctx.applicationMap = null;
}
public static ActionContext getActionContext() {
ActionContext ctx = (ActionContext) localContext.get();
if (ctx == null) {
ctx = new ActionContext();
localContext.set(ctx);
}
return ctx;
}
public Map getCookieMap() {
if (cookieMap == null) {
cookieMap = new CookieMap(request, response);
}
return cookieMap;
}
public Map getParameterMap() {
if (parameterMap == null) {
parameterMap = new ParameterMap(request);
}
return parameterMap;
}
public Map getRequestMap() {
if (requestMap == null) {
requestMap = new RequestMap(request);
}
return requestMap;
}
public Map getSessionMap() {
if (sessionMap == null) {
sessionMap = new SessionMap(request);
}
return sessionMap;
}
public Map getApplicationMap() {
if (applicationMap == null) {
applicationMap = new ApplicationMap(request);
}
return applicationMap;
}
public HttpServletRequest getRequest() {
return request;
}
public HttpServletResponse getResponse() {
return response;
}
public String getAppURL() {
StringBuffer url = new StringBuffer();
int port = request.getServerPort();
if (port < 0) {
port = 80;
}
String scheme = request.getScheme();
url.append(scheme);
url.append("://");
url.append(request.getServerName());
if ((scheme.equals("http") && (port != 80))
|| (scheme.equals("https") && (port != 443))) {
url.append(':');
url.append(port);
}
url.append(request.getContextPath());
return url.toString();
}
}
Struts配置中,parameter属性是作为扩展用的,所以我们可以利用它。
改动:
parameter指定控制器类的类名和方法名,格式为:包名.类名:函数名
type固定为com.ynmp.webapp.frame.BaseAction
如:
<action path="/item_list" parameter="com.ynmp.webapp.action.ItemAction:viewAllItems" name="itemsForm" type="com.ynmp.webapp.frame.BaseAction">
<forward name="success" path="/item_list.jsp" />
</action>
配置管理类:
package com.ynmp.webapp.frame;
public class ActionConfig {
private static final String ACTION_CONFIG_REGEX = "^([A-Z|a-z|_]+\\.)+[A-Z|a-z|_]+\\:(([A-Z|a-z|_]+)|\\*)$";
private String className;
private String methodName;
public ActionConfig(String config) {
if (config == null
|| config.length() == 0
|| ! config.replaceAll(" ", "").matches(ACTION_CONFIG_REGEX)) {
throw new RuntimeException("Parameter=\"" + config + "\" 格式不合法!应为:包名.类名:方法名,如:com.company.UserAction:login");
}
int index = config.indexOf(":");
className = config.substring(0, index).trim();
methodName = config.substring(index + 1).trim();
}
public ActionConfig(String className, String methodName) {
this.className = className;
this.methodName = methodName;
}
public String getClassName() {
return className;
}
public String getMethodName() {
return methodName;
}
public void setClassName(String className) {
this.className = className;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
}
Class辅助工具类
package com.ynmp.webapp.frame.util;
import java.lang.reflect.Method;
public class ClassUtils {
public static Method findMethodByName(Class clazz, String methodName) {
int count = 0;
Method actionMethod = null;
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i ++) {
if (methods[i].getName().equals(methodName)) {
// 其实也可以不检查函数是否重载,直接返回第一个定义的,
// 但这样有可能会使程序员迷惑,还是检查一下重载。
count ++;
if (count > 1) {
throw new RuntimeException(clazz + " 类中有重载的同名方法: " + methodName + ",无法判定使用哪一个!");
}
actionMethod = methods[i];
}
}
if (count == 0 || actionMethod == null) {
throw new RuntimeException(clazz + " 类中找到不方法: " + methodName);
}
return actionMethod;
}
public static String getLowerClassName(Class clazz) {
String className = clazz.getName();
int index = className.lastIndexOf(".");
if (index != -1) {
className = className.substring(index + 1);
}
return className.substring(0,1).toLowerCase() + className.substring(1);
}
}
其它JUnit的测试类就不贴了。
现在Action解放了,Form对象还没解放
还是必需继承ActionForm,
因为<form-bean配置会检查该对象是否继承于ActionForm,否则报错。
验证框架和ActionForm也有点藕合。
我现在还没有想到好的办法,先搞个权宜之计,
写了一个BaseForm,继承于ActionForm,然后将从ActionForm中继承来的方法全给final掉
其它Form都继承于BaseForm,这样先保证Form不会重写ActionForm中的方法,
看起来像个POJO,若以后能去掉ActionForm,就只要改BaseForm。