实现在js客户端对Spring的动态调用(一)
一、目的:
JS客户端在编写程序时,可以像调用本地方法一样调用服务器端的Spring中Bean的方法,实现对Spring的动态调用,从而减少客户端的每个请求都要配置成action的麻烦,使业务模块的开发只关注业务逻辑的展示页面的开发。
二、设计思路:
1.服务器端利用Struts机制,提供统一的动态action,名称为DynamicAjaxAction,接收客户端的请求,并返回处理结果;
2.DynamicAjaxAction内部解析请求后,通过java反射机制动态调用spring中的baen方法;
3.客户端封装异步请求方法,提供调用接口;
总体架构如下示意图:
三、详细设计:
1.DynamicAjaxAction的设计:
public class DynamicAjaxAction extends ActionSupport {
private ActionInvoker actionInvoker;
private DynamicAjaxActionRouting routing;
private static final Log log = LogFactory.getLog(DynamicAjaxAction.class);
protected static ArrayList<String> ExcludeValues = new ArrayList<String>();
public DynamicAjaxAction() {
ExcludeValues.add("_de");
ExcludeValues.add("serviceUrl");
ExcludeValues.add("cacheData");
}
public String execute() throws Exception {
log.debug("DynamicAjaxAction.execute");
String key = this.getServiceUrl();
Map<String, Object> session = ServletActionContext.getContext().getSession();
HttpServletResponse response = ServletActionContext.getResponse();
data = this.invokerAction();
if(data == null) ExtObject.writeJsonString(response, (new ExtResultObject(true)).toString());
else ExtObject.writeJsonString(response, data.toString());
return null;
}
protected Object invokerAction() throws Exception {
Object[] params = this.getInvokerParams().toArray();
Object obj = actionInvoker.invokerAction(this.getRouting().getServiceName(),
this.getRouting().getActionName(), params);
return obj;
}
private String getServiceUrl() {
HttpServletRequest request = ServletActionContext.getRequest();
String serviceUrl = request.getParameter("serviceUrl");
return serviceUrl;
}
private boolean isCacheData() {
HttpServletRequest request = ServletActionContext.getRequest();
String serviceUrl = request.getParameter("cacheData");
if(StringUtility.isNullOrEmpty(serviceUrl)) return false;
return serviceUrl.equals("true");
}
protected DynamicAjaxActionRouting getRouting() {
String serviceUrl = this.getServiceUrl();
routing = new DynamicAjaxActionRouting(serviceUrl);
return routing;
}
@SuppressWarnings("unchecked")
protected HashMap<String, Object> getRequestParams() {
HttpServletRequest request = ServletActionContext.getRequest();
HashMap<String, Object> requestParams = new HashMap<String, Object>();
Object[] params = request.getParameterMap().keySet().toArray();
for(int i = params.length; i > 0; i --) {
String item = params[i - 1].toString();
if(!ExcludeValues.contains(item)) {
String[] value = request.getParameterValues(item);
if(value.length == 0) {
//do nothing
} else if(value.length == 1) {
requestParams.put(item, value[0]);
} else {
requestParams.put(item, value);
}
}
}
return requestParams;
}
@SuppressWarnings("unchecked")
protected ArrayList getInvokerParams() {
HttpServletRequest request = ServletActionContext.getRequest();
ArrayList requestParams = new ArrayList();
requestParams.addAll(this.getRouting().getParams());
log.debug("加入调用参数:" + StringUtility.arrayToString(this.routing.getParams()));
Object[] params = request.getParameterMap().keySet().toArray();
for(int i = params.length; i > 0; i --) {
String item = params[i - 1].toString();
if(!item.equals("serviceUrl") && !item.equals("_dc")) {
Object value = request.getParameter(item);
//log.debug("发现调用参数:" + item + "=" + value);
requestParams.add(value);
}
}
//新增加一个Session对象,允许DWR的方法接收请求的Session对象
requestParams.add(ServletActionContext.getRequest().getSession());
return requestParams;
}
public ActionInvoker getActionInvoker() {
return actionInvoker;
}
public void setActionInvoker(ActionInvoker actionInvoker) {
this.actionInvoker = actionInvoker;
}
}
DynamicAjaxAction是一个标准的Struts的Action,类的execute方法中,首先接收到请求的参数,重点是名称为serviceUrl的参数,此参数据形如“textService/test/abc”,参数分为三段,以“/”分隔,第一部分是bean的名称,第二部分为方法名称,第三部分为方法的参数,如果有多个参数,分别顺序添加即可。
然后,调用ActionInvoker的方法,通过反射访问目标方法,接收返回结果。
最后,接返回的结果写入response,返回给客户端。
当然, 需要将此类在Struts和Spring中分别进行配置,以确保客户端能正常访问。
2.java反射机制的封装:
public class ActionInvoker {
private static final Log log = LogFactory.getLog(ActionInvoker.class);
public Object invokerAction(String beanName, String methodName, Object[] parameters) {
Object beanIns = BeanFactoryUtility.factory.getBean(beanName);
Method methodIns = this.pairMethod(beanIns.getClass(), methodName, parameters);
if(methodIns == null) {
ExtObject obj = new ExtObject();
obj.addException("没有找到同名方法" + beanIns.getClass().getName() + "." + methodName);
return obj;
}
Object[] mParams = this.getMethodParams(methodIns, parameters);
log.debug("调用方法:" + beanName + "." + methodName + ",参数:" + StringUtility.arrayToString(mParams));
try {
Object result = methodIns.invoke(beanIns, mParams);
return result;
} catch (Exception ex) {
ExtObject obj = new ExtObject();
obj.add("exception", ex);
return obj;
}
}
//查询最匹配的方法
private Method pairMethod(Class cls, String methodName, Object[] parameters) {
log.debug("查询方法:" + cls.getSimpleName() + "." + methodName + ",参数:" + StringUtility.arrayToString(parameters));
//根据方法名称查询
//名称相同,参数数目相同,则返回,否则返回异常
//名称相同,参数数目比传入的少,则将传入的参数处理成所需的参数
Method[] ms = this.findMethodByName(cls, methodName, parameters.length);
if(ms.length == 0) return null;
if(ms.length == 1) return ms[0];
Method m = this.pairMethod(ms, parameters);
return m;
//TODO 同名方法同参数数据方法,根据参数类型
//判断将调用参数的最可能类型,匹配最相近的方法
//转换不成功,则异常
//TODO 根据参数名称匹配最合适的方法
}
/**
* 根据调用参数信息,查询最匹配的方法
、围
* @param parameters 调用的参数数组
* @return 从第一个参数开始查询,将不匹配的方法去掉,
* 然后再从可以匹配的方法中找到参数最多的,也就是最匹配的方法,
* 如果参数最多的有多个,返回随机的一个(其实应该不可能,此处不再抛出异常)
*/
private Method pairMethod(Method[] ms, Object[] parameters) {
//首先找到所有可以调用的方法
ArrayList<Method> methodsCanInvoke = new ArrayList<Method>();
for(Method m : ms) {
if(isCanInvoke(m, parameters)) methodsCanInvoke.add(m);
}
if(methodsCanInvoke.size() == 0) return null;
if(methodsCanInvoke.size() == 1) return methodsCanInvoke.get(0);
//从查询的方法中找到参数最多的一个
Method result = methodsCanInvoke.get(0);
for(Method m : methodsCanInvoke) {
if(m.getParameterTypes().length > result.getParameterTypes().length)
result = m;
}
return result;
}
private boolean isCanInvoke(Method m, Object[] parameters) {
if(m.getParameterTypes().length > parameters.length) return false;
int i = 0;
for(Class c : m.getParameterTypes()) {
if(!c.equals(this.getParamType(parameters[i]))) {
return false;
}
i ++;
}
return true;
}
private Object[] getMethodParams(Method m, Object[] srcParams) {
Object[] mParams = new Object[m.getParameterTypes().length];
for(int i = 0; i < mParams.length; i ++) {
mParams[i] = this.formatParam(srcParams[i], m.getParameterTypes()[i]);
}
log.debug("处理后的方法参数:" + StringUtility.arrayToString(mParams));
return mParams;
}
private Class getParamType(Object value) {
if(StringUtility.isInteger(value)) return Integer.class;
if(StringUtility.isLong(value)) return Long.class;
if(StringUtility.isDouble(value)) return Double.class;
if(StringUtility.isUUID(value)) return UUID.class;
return String.class;
}
private Object formatParam(Object value, Class type) {
log.debug("转换参数值:" + value + "为类型" + type.getSimpleName());
if(type == int.class || type == Integer.class) {
return Integer.parseInt(value.toString());
} else if(type == long.class || type == Long.class) {
return Long.parseLong(value.toString());
} else if(type == double.class || type == Double.class) {
return Double.parseDouble(value.toString());
} else if (type.equals(boolean.class)) {
return Boolean.parseBoolean(value.toString());
} else if (type.equals(UUID.class)) {
return UUID.fromString(value.toString());
}
return value;
}
private Method[] findMethodByName(Class cls, String methodName, int paramCount) {
ArrayList<Method> ms = new ArrayList<Method>();
for(Method m : cls.getMethods()) {
if(m.getName().equals(methodName)) {
if(m.getParameterTypes().length <= paramCount) {
ms.add(m);
}
}
}
return ms.toArray(new Method[ms.size()]);
}
}
ActionInvoker是一个针对java动态调用方法的封装,用来根据传入的参数,动态调用请求的方法。
设计的基本思路是:根据beanName和方法名称,确定方法,如果可以确定唯一的目标方法(即方法没有重载),则将请求的参数,按要调用的方法所需的参数进行转换,转换成功后,即可进行调用。如果转换失败或没有发现目标方法,则调用不成功。
如果目标方法不唯一,那么要看请求的参数个数,确定方法中的参数个数小于等于请求的参数个数的方法为目标方法(这个逻辑可以根据自己的应用情况进行修改),然后按照方法的需要把参数转换为所需的类型即可调用。
调用成功后,返回结果。
3.客户端异步请求方法的实现 :
Ext.namespace("justgin.bap");
/**
* 全局的Ajax请求类,类似于Ext.Ajax,但个性化了一些领域业务
*/
justgin.bap.Ajax = new Ext.data.Connection({
autoAbort : false
});
/**
* 个性化的领域业务,包括:
* 动态Ajax请求的封装(用户只需指定serviceUrl参数即可完成请求,serviceUrl的格式为:
* {bean名称}.{方法名}.{参数1}.{参数2}.。。。.{参数n});
* 返回数据的处理(自动将返回的结果包括成json格式的object,供客户端使用);
* 请求失败的处理(目前只是弹出提示窗口);等。
*/
justgin.bap.Ajax.on('beforerequest', function(conn, config) {
Ext.apply(config, {url: '/Domain/DynamicAjax.do', method: 'GET'});
Ext.apply(config, {params: {serviceUrl: config.serviceUrl}});
var successCallback = config.success;
Ext.apply(config, {success: function(r, options) {
var data = r.responseText.trim();
var o = data;
try {
o = eval('(' + data + ')');
} catch(e) {}
ajaxRequestSuccess(successCallback, o, config.dataCache);
}})
Ext.apply(config, {failure: function(r, options) {
ajaxRequestFailuere('请求' + config.serviceUrl + '失败。');
}});
});
ajaxRequestSuccess = function(callback, o, dataCache) {
callback.call(null, o, dataCache);
}
ajaxRequestFailuere = function(msg) {
alert(msg);
}
客户端异步请求的原理很简单,只是对Ext.Ajax.request的一个扩展,用户不需要指定请求的url和参数,只要指定请求的serviceUrl即可,请求失败时,可以根据应用的情况,统一实现一个异常处理的机制,使用者不需要专门为失败编写代码。请求成功后,调用使用都传入的回调方法,将responseText传递给使用都。
四、使用方法:
1.服务器端textService的建立:
public class testService {
public ExtObject test(String id) {
//id = "abc";
ExtObject eo = new ExtObject();
eo.add("id", "abc");
eo.add("name", "abcd");
return eo;
}
}
2.服务器端bean配置
<bean id="testService" class="justgin.jbap.samples.TestService">
</bean>
3.客户端调用
justgin.bap.ajax.request({
serviceUrl: 'testService/test/abc',
success: function(data) {
alert(data.id + data.name);
}
});
五、总结
动态调用Srping的实现目的主要为了提高js客户端访问服务器端业务逻辑的便捷性,为客户端与服务器端的通讯提供一种解决方案,为了开发者提供方便的使用方式,使其在在业务模块的设计时,更关注于内部逻辑。当然,相关的安全、性能等问题没有列入其中,具体的情况可以根据应用场景的不同进行优化。
下一篇会根据上述内容,针对业务模块开发时常用的列表数据和表单数据提供更具体的分析和设计,使动态调用更有针对性,例如service的参数可以直接是一个业务对象,最大限度地减轻客户端与服务器端通讯时,业务对象和Json之间转换的工作量,敬请关注。