進(jìn)入新的項(xiàng)目組, checkout項(xiàng)目下來, 看了一下其整體結(jié)構(gòu)與代碼,哎,比較亂。 經(jīng)過我的建議,部門經(jīng)理讓我對該項(xiàng)目進(jìn)行全面重構(gòu)。
首先,此項(xiàng)目是以AppFuse作為基礎(chǔ)的,還記得robin說它就一toy 選擇的模板是iBatis + Spring + Struts 我的第一步工作是要隔離Struts。
Struts是老牌的MVC實(shí)現(xiàn),那個(gè)年代IoC和輕量級還沒現(xiàn)在流行,框架侵入性也沒有得到重視。 所以Struts的實(shí)現(xiàn)讓應(yīng)用程序嚴(yán)重依賴它: 1.所有控制器都必需繼承Action類 2.所有數(shù)據(jù)封裝類必需繼承ActionForm 3.控制器方法execute必需返回ActionForward,與Struts藕合 4.控制器方法execute的參數(shù)ActionMapping,與Struts藕合 5.控制器方法execute的參數(shù)HttpServletRequest,HttpServletResponse,與Servlet容器藕合 6.由于Action類是Struts管理的,不支持Service類的IoC注入,除非將控制權(quán)委托給IoC容器,再配一遍(如:org.springframework.web.struts.DelegatingActionProxy)。
目標(biāo): 1.控制器不繼承任何類,也不實(shí)現(xiàn)任何接口 2.數(shù)據(jù)封裝Form類應(yīng)為簡單的POJO,不要繼承ActionForm 3.execute返回值可以是任意對象(包括基本類型和void), 標(biāo)準(zhǔn)返回String,即forward的索引值, 如果返回其它類型對象就調(diào)用其toString。 如果返回類型為void或返回值為null,forward到默認(rèn)的"success" 4和5.execute只傳入POJO的Form, 如果該動(dòng)作不需要Form數(shù)據(jù),也可以保持空的參數(shù)列表。 如果有多個(gè)參數(shù),第一個(gè)參數(shù)為Form(作為傳入,也作為傳出,這個(gè)是struts已經(jīng)實(shí)現(xiàn)的規(guī)則),后面的都為傳出對象,必需保證為POJO,傳出對象會(huì)根據(jù)struts的action配置的scope,放入相應(yīng)域。 6.支持IoC注入Service,即然IoC,當(dāng)然不能依賴某個(gè)具體IoC容器,沒有Spring一樣運(yùn)行。要不然會(huì)被ajoo一頓臭罵,什么什么? IoC還:容器類.getXXX()? 7.當(dāng)然,還要實(shí)現(xiàn)一個(gè)線程安全的容器類,持有與Servlet相關(guān)的信息, 這樣,若有特殊要求需要訪問HttpServletRequest,HttpServletResponse 則可以通過:容器類.get當(dāng)前線程容器().getRequest()方式獲取。
最后類應(yīng)該像這樣:
java 代碼
-
- public class ItemAction {
-
- private ItemService itemService;
-
-
- public void setItemService(ItemService itemService) {
- this.itemService = itemService;
- }
-
-
-
-
- public String viewAllItems(final ItemForm itemForm) {
- itemForm.setItems(itemService.getItems());
- return "success";
- }
-
-
- public String saveItem(final ItemForm itemForm) {
- return itemService.saveItem(itemForm.getItem()) ? "success" : "failure";
- }
- }
不用說,這樣的類是易于測試的。 例如:
java 代碼
- public void testRightAllViewItems() {
- ItemAction itemAction = new ItemAction();
- ItemService itemService = new ItemServiceMock();
- itemAction.setItemService(itemService);
-
- ItemsForm itemsForm = new ItemsForm();
- String forward = itemAction.viewAllItems(itemsForm);
-
- assertEquals("沒有正確跳轉(zhuǎn)!", "success", forward);
- assertNotNull("沒有向itemsForm寫入數(shù)據(jù)!", itemsForm.getItems());
-
-
- 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());
- }
當(dāng)然還有測試傳入一個(gè)null的ItemsForm等的其它測試,這里就不例舉了。
好,明確目標(biāo)后,開始重構(gòu),重構(gòu)后要先保證以前的代碼也能運(yùn)行。
由于要使Action徹底獨(dú)立,我暫時(shí)想到的辦法是反射回調(diào)。 我先寫一個(gè)通用的Action,然后回調(diào)具體的控制器類。 實(shí)現(xiàn)如下: 通用Action
java 代碼
- 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);
- }
-
-
- private String getActionForward(String scope, String config, Object model) throws 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;
- }
-
-
- private Object populateActionClass(String className) throws Exception {
- Class actionClass = Class.forName(className);
- Object action = actionClass.newInstance();
- injectService(action);
- return action;
- }
-
-
- private void injectService(Object action) throws Exception {
- ServiceProvider serviceProvider = new SpringServiceProvider(getServlet());
- 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])});
- }
- }
- }
- }
-
-
- 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;
- }
-
-
- 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]);
- }
- }
-
-
- 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中的一個(gè)!");
- }
- }
IoC的Service供給接口
java 代碼
- package com.ynmp.webapp.frame.provider;
-
- public interface ServiceProvider {
-
- public Object getService(String serviceName, Class serviceClass) throws Exception;
-
- }
Spring的Service供給實(shí)現(xiàn),依賴Spring,作為一種策略應(yīng)該不成問題。
java 代碼
- 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相關(guān)信息持有類, 還有幾個(gè)Map的封裝,不貼也應(yīng)該猜得到, 就是map.put時(shí)調(diào)用request,session,cookie,application相應(yīng)的setAttribute等,get也類似
代碼
Struts配置中,parameter屬性是作為擴(kuò)展用的,所以我們可以利用它。 改動(dòng): parameter指定控制器類的類名和方法名,格式為:包名.類名:函數(shù)名 type固定為com.ynmp.webapp.frame.BaseAction 如:
xml 代碼
- <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>
配置管理類:
java 代碼
- 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 + "\" 格式不合法!應(yīng)為:包名.類名:方法名,如: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輔助工具類
java 代碼
- 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 + ",無法判定使用哪一個(gè)!");
- }
- 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的測試類就不貼了。
現(xiàn)在Action解放了,F(xiàn)orm對象還沒解放 還是必需繼承ActionForm, 因?yàn)?lt;form-bean配置會(huì)檢查該對象是否繼承于ActionForm,否則報(bào)錯(cuò)。 驗(yàn)證框架和ActionForm也有點(diǎn)藕合。 我現(xiàn)在還沒有想到好的辦法,先搞個(gè)權(quán)宜之計(jì), 寫了一個(gè)BaseForm,繼承于ActionForm,然后將從ActionForm中繼承來的方法全給final掉 其它Form都繼承于BaseForm,這樣先保證Form不會(huì)重寫ActionForm中的方法, 看起來像個(gè)POJO,若以后能去掉ActionForm,就只要改BaseForm
|