`
javatar
  • 浏览: 1680361 次
  • 性别: Icon_minigender_1
  • 来自: 杭州699号
社区版块
存档分类
最新评论

AppFuse改造之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()方式获取。

最后类应该像这样:
// 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。
分享到:
评论
8 楼 shaucle 2007-03-09  
injection也可用annotation

其solaris版本不支持jdk5.0, 晕
7 楼 lgx522 2007-03-09  
我用的是JSF版本,从头到尾一个人干,30多张表的管理系统,差不多也就一个多月,效率还是很高的。
此系统稳定运行了一年,说明Appfuse是可靠的。
目前第二个系统正要上线。规模差不多,稍复杂,但有了上一个系统的经验,20天左右也就搞定了。
建议同道们不要再搞自行一套的集成方案了。有了Appfuse这种久经考验的东西,开发得快,学习交流起来也方便。
6 楼 javatar 2007-01-26  
heyosi 写道
你可以去看看stecks!annotation风格的struts,不需要直接继承struts的Action和Form了。


谢谢你的提议,但此项目使用weblogic8.1,其solaris版本不支持jdk5.0,
annotation是个不错的主意,可以更好的aop,也省些xml配置。
5 楼 javatar 2007-01-26  
graymood 写道
为什么要要把ActionContext放在ThreadLocal中?
假如我在项目的其它地方也用ThreadLocal那样不是会产生冲突的?


怎么会冲突呢?
ThreadLocal的实现是比较简单的,
其内部持有一个Map,以当前Thread作为key,
去取其中的所存的对象,
这样就可以保证每个Thread拿到的对象实例都不同。
以实现线程安全。
且在一个Thread内的程序都能拿到。

你没理解的是一个ThreadLocal,只存一种类实例。
两个ThreadLocal根本不会相干。


如我们有两个Holder类,都用了ThreadLocal技术:
public class AHolder {

    ThreadLocal aContext = new ThreadLocal(); //注意这里new

    public static AHolder getAHolder() {
       // 写法主帖有....
    }
}


public class BHolder {

    ThreadLocal bContext = new ThreadLocal(); //注意这里也new


    public static BHolder getBHolder() {
       // 写法主帖有....
    }
}


aContext和bContext分别实例化了,
这样aContext和bContext内部分别持有一个Map

你用当前线程作为key,
去AHolder里拿到的是aContext持有的对象
去BHolder里拿到的是bContext持有的对象
怎么会冲突?

4 楼 heyosi 2007-01-23  
你可以去看看stecks!annotation风格的struts,不需要直接继承struts的Action和Form了。
3 楼 graymood 2007-01-22  
为什么要要把ActionContext放在ThreadLocal中?
假如我在项目的其它地方也用ThreadLocal那样不是会产生冲突的?
2 楼 javatar 2007-01-16  
嗯,多谢,正在参考它的原理,
但DynaActionForm在Action还是要依赖它,
而且有点像用Map,没有强类型。
1 楼 seairy 2007-01-16  
ActionForm可以用DynaActionForm和LazyValidatorForm代替呀,数据验证用Validator

相关推荐

    基于AppFuse框架的B2C电子商务系统研究与实现

    基于AppFuse框架的B2C电子商务系统研究与实现

    appfuse/display/strutsMenu

    主要对于初学appfuse及想了解其中所运的技术display. StrutsMenu的人.

    appfuse2.0.2 Struts2 hibernate Spring 构建的基于SQLServer2005 的ssh2项目的过程全记录

    appfuse2.0.2 Struts2 hibernate Spring 构建的基于SQLServer2005 的ssh2项目的过程全记录 网上很多帖子介绍appfuse2构建过程的,但是基于SQLServer2005的没有,顶多一笔带过,另外对于期间出现的各种问题也没有个说明,...

    Struts2、Spring和Hibernate应用实例.

    现在就将笔者使用Myeclipse工具应用struts2 + spring2 + hibernate3 实现CRUD操作的步骤一一纪录下来,为初学者少走弯路略尽绵薄之力!在本文中,笔者将Struts2.0.6、Spring2.0.6和Hibernate3.1进行整合,希望通过...

    appfuse-tutorial-struts-1.6.zip_appfuse

    关于企业人员管理的struts应用样例,包含人员添加、信息修改及注销等。

    SSH学习及开发框架-appfuse

    appfuse 有struts2+hibernate+spring的整合 springmvc+hibernate+spring的整合 多模块,但模块都有 学习开发参考使用非常方便 可以到官方下载最新版的,我只是把自己下载的打包整理一下 注意哈,都是基于maven的...

    Appfuse框架解析

    Appfuse框架解析,包括登陆验证,执行顺序,加载策略等。

    可直接使用的appfuse项目

    AppFuse是一个集成了众多当前最流行开源框架与工具(包括Hibernate、ibatis、Struts、Spring、DBUnit、Maven、Log4J、Struts Menu、Xdoclet、SiteMesh、OSCache、JUnit、JSTL等(现在还有lucene的,无敌了))于一身的...

    AppFuse学习笔记(J2EE入门级框架)

    Appfuse是Matt Raible 开发的一个指导性的入门级J2EE框架,它对如何集成流行的Spring、Hibernate、iBatis、Struts、xDcolet、Junit、Taperstry、JSF等基础框架给出了示范。在持久层,AppFuse采用了Hibernate O/R映射...

    appfuse

    使用appfuse2.0,下载过来的实例源码,没有jar包

    Appfuse教程Appfuse开发.pdf

    Appfuse是由Matt Raible开发的一个指导性的入门级J2EE框架,它对如何集成流行的Spring、Hibernate、iBatis、struts、Xdoclet、junit 等基础框架给出了示范。提供了对Taperstry和JSF的支持。  AppFuse是一个集成了...

    AppFuse项目研究

    Appfuse是由Matt Raible开发的一个指导性的入门级J2EE框架,它对如何集成流行的Spring、Hibernate、iBatis、struts、Xdoclet、junit 等基础框架给出了示范。提供了对Taperstry和JSF的支持

    AppFuse

    本文以一个 J2EE 开发者的角度,借助一个简单的应用示例,在融合了个人经验的基础上介绍了如何用 AppFuse 一步步地构建 J2EE 项目。通过阅读本文,读者不仅能够学会用 AppFuse 进行开发,而且能够充分体会到 AppFuse...

    appfuse1.4-architecture

    06年时的appfuse,学习SSH架构的经典入门框架。相对比较老的资料,可以欣赏一下当时的架构,向牛人致敬

    AppFuse入门文档(AppFuse与SpringMVC+mybatis整合)

    本文档详细描述了AppFuse与SpringMVC+mybatis整合的过程,只要你懂一些基本的eclipse操作和基本的maven命令,就可以在三分钟之内迅速的搭建出一个AppFuse的架构

    APPFUSE工具研究.doc

    Appfuse是一个开源的工程应用,它集成了现在最流行的开发框架到该应用中,使用Maven可以很方便的开发和部署因为。也可以集成到现在流行的开源开发工具如eclipse,idea等。现在让我们简单的看看APPFUSE开发应用的简单...

    appfuse开发框架(myapp)使用说明文档

    对用appfuse 做快速开发进行说明 本框架推荐使用的Tomcat版本为5.0.*,JDK使用1.4。若使用JDK 1.5+,则要将Tomcat安装目录下的common\endorsed 里面两个JAR(xercesImpl.jar,xmlParserAPIs.jar)去掉。Ps:...

    Using Struts 2 - AppFuse 2 - Confluence(1).pdf

    Using Struts 2

    建立项目原型骨架的步骤(最新版本appfuse)appfuse2.1.0-M2

    建立项目原型骨架的步骤(最新版本appfuse)appfuse2.1.0-M2 spring3.0 hibernte3.3 struts2.1.8

    appfuse-documentation-2.1.0官方文档

    AppFuse是一个集成了众多当前最流行开源框架与工具(包括Hibernate、ibatis、Struts、Spring、DBUnit、Ant、Log4J、Struts Menu、Xdoclet、SiteMesh、OSCache、JUnit、JSTL)于一身的Web开发框架。AppFuse提供了Web...

Global site tag (gtag.js) - Google Analytics