AppFuse改造のStrutsフレームワーク分離


新しいプロジェクトグループに入り、
チェックアウトプロジェクトが下りてきて、
全体の構造とコードを見てみると、まあ、散らかっています.
私の提案を経て、部門のマネージャーは私にこのプロジェクトを全面的に再構築させました.
まず、このプロジェクトは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のパラメータHttpServeretRequest,HttpServeretResponse,Serveretコンテナとの接続
6.ActionクラスはStruts管理のため、制御権を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注入サービス、すなわちIoCをサポートします.もちろん、特定のIoCコンテナに依存することはできません.Springのように動作しません.さもないとajooに悪口を言われます.何ですか.IoC:容器類.getXXX()?
7.もちろん、サーブレットに関する情報を持つスレッドの安全なコンテナクラスも実現します.
このように、特別な要求があればHttpServeretRequest,HttpServeretResponseにアクセスする必要がある
容器類.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を書き、具体的なコントローラクラスをコールバックします.
次のようになります.
汎用アクション

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のサービス供給インタフェース

package com.ynmp.webapp.frame.provider;

public interface ServiceProvider {

	public Object getService(String serviceName, Class serviceClass) throws Exception;

}

Springのサービス供給実装は,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;
	}

}

スレッドセキュリティのサーブレット関連情報保持クラス、
まだいくつかのMapのパッケージがあり、貼らなくても推測できるはずですが、
mapです.putでrequest,session,cookie,アプリケーション対応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のテストクラスは貼りません.
現在アクションは解放されていますが、Formオブジェクトはまだ解放されていません.
ActionFormを継承する必要があります
検証フレームワークとActionFormも少し合わせています.
私はまだ良い方法を考えていません.まず適当な方法をします.
BaseFormと書いてActionFormに継承し、ActionFormから継承したメソッドをfinalにすべて削除します.
他のFormはBaseFormに継承され、FormがActionFormのメソッドを書き換えないことを保証します.
POJOのように見えますが、後でActionFormを取り除くことができれば、BaseFormを変更するだけです.