Rpc远程调用框架的设计与实现
1 Rpc远程调用框架设计概述
1.1 研究背景
1.1.1 传统的Web开发方式
在传统的Web应用程序中,一般都是采取请求→刷新→显示的模式。即每当用户通过单击按钮或链接向服务器发送一个请求时,都由服务器接收请求并处理,处理完毕后服务器将信息发送至浏览器进行显示。而在服务器处理的时间里,浏览器处于Loading状态,显示为空白和无响应状态,用户能做的事情只有等待。事实上,用户想获得的可能仅仅是一件商品的价格信息,但为了这么一个微小的请求却不得不刷新整个页面,让其他所有的数据都被重新运算和下载。这不仅加大了网络流量,也加大了服务器的处理负担,更主要的用户体验比较差。
随着Web技术的发展,用户更多的参与到与网站的交互中,网页不仅是显示数据的载体同时也是接收用户输入的窗口。这种时候过多的页面元素往往会使页面载入的速度非常慢,因为用户每操作一下页面都要完整的刷新一次,也就是说用户每次都要被迫接收他不需要的信息。虽然网页设计师想了很多办法来解决类似的问题,比如降低页面模块的粒度,采用iframe延迟加载,但并没有从根本上解决问题。
1.1.2 Ajax的兴起
在Web2.0热潮中,Ajax是被运用最多的技术之一,它打破了传统Web的访问方式,使得Web应用正面临全新的改变,最重要的是它改变了传统Web的应用体验和编程模式,有效地解决了Web应用所需要的各种特性,从而使得Web应用的功能和开发方式发生了根本性的变化,并逐渐成为企业应用开发的主流和首选。Ajax使用XMLHttpRequest进行远程数据读取,直接操作Dom实现动态的数据显示与交互,实现了无刷新的访问方式,使得用户体验大为提高。
1.1.3 Ajax开发中所面临的问题
遗憾的是针对Ajax的JavaScript编程不是那么的方便,它本质上是一个浏览器端的技术,首先面临无可避免的第一个问题即是浏览器的兼容性问题。各浏览器对于JavaScript的Api,DOM操作方式与CSS样式的支持总有部分不太相同或是有Bug,甚至同一浏览器的各个版本间对于这些元素的支持也有可能部分不一样。这导致程序员在写Ajax应用时花大部分的时间在调试浏览器的兼容性而非在应用程序本身。另外,构建与Ajax请求相对应的服务端程序与获取其所返回的数据及对数据的格式转换与处理也是很繁杂的工作。因此,如何简化Ajax的使用过程,让开发人员能更多的关注业务逻辑的实现,成为一个很有意义的课题。
1.2 研究现状
1.2.1 引入Ajax库,降低工作量
针对上面所提到的问题,开源社区里出现了一些对Ajax进行简单封装的JsLib,通过引入这些库,可以有效减少开发人员手动创建XMLHttpRequest对象的次数。不过这类框架只能屏蔽不同浏览器间JavaScript创建XMLHttpRequest对象的差异,并不能解决客户端与服务端之间繁杂的通信与数据转换,因此更加方便的Ajax框架出现了。
1.2.2 目前主流的Ajax框架及其优缺点
(1) AJAX Tags
AJAX Tag是一组Jsp标签,用来简化AJAX(Asynchronous JavaScript and XML )技术在JSP页面中的使用.它提供了一些常见功能的标签如下拉级联选择,用户在文本框中输入字符自动从指定的数据中匹配用户输入的字符等。它构建在Prototype框架之上。
(2) AjaxAnywhere
AjaxAnywhere被设计成能够把任何一套现存的JSP组件转换成AJAX感知组件而不需要复杂的JavaScript编码。它利用标签把Web页面简单地划分成几个区域,然后使用AjaxAnywhere来刷新那些需要被更新地区域。
(3) DWR
DWR(Direct Web Remoting)是一个WEB远程调用框架。利用这个框架可以让AJAX开发变得很简单。利用DWR可以在客户端利用JavaScript直接调用服务端的Java方法并返回值给JavaScript就好像直接本地客户端调用一样(DWR根据Java类来动态生成JavaScrip代码)。
AJAX Tag与AjaxAnywhere对于使用Jsp的Web项目提供了不错的支持,但是使用Velocity, FreeMarker模板语言来作为View层的Web项目就无能为力了。DWR拥有很好的设计思想,不过使用远程调用对象时需要做一些配置,没有提供统一的数据交互格式。
1.2.3 Rpc远程调用框架的优势
Rpc对上述的开源框架的一些优缺点进行了集成和改进,是一个便捷,快速,方便的Ajax开发框架,它主要有以下几个特点:
(1) 提供与DWR类似的远程调用对象模型,可以在本地直接调用服务端方法并接收数据,在这个过程中通信协议以及浏览器的差异对于开发人员来说是透明的,这意味着客户端代码再也不需要直接处理XMLHttpRequest 对象或者服务器的响应。不再需要编写对象的序列化代码或者使用第三方工具才能把对象变成 XML。甚至不再需要编写 servlet 代码把 Ajax 请求调整成对 Java 域对象的调用。开发人员只需要关注Java端的业务逻辑的编写。
(2) 与主流Ioc开源框架Spring相结合,提供远程bean对象调用方式。框架允许在客户端获取Spring Context中的任意bean对象,而且这个过程中Spring对于客户端几乎是开放的,用户可以认为Rpc在客户端与Spring容器建立了一个连接,非常方便。
(3) 使用Annotation定义的RemoteMethod是Rpc的一大特色。由于客户端的安全性不是很高,开发者需要指定哪些类方法是可以被访问的而哪些方法是禁止客户端直接调用的。在DWR中,开发者需要配置xml文件,这是让人头痛的事,配置繁多且容易出错。在Rpc中,开发者只需要给允许被调用的方法加上RemoteCall的Annotation,这样,框架就根据注解来构建可用方法的List。
(4) Ajax应用中开发者需要面临数据格式的定义以及转换。自定义的数据结构还需要开发者自己解析,容易出错,与主流JavaScript框架的结合也不好。Rpc采用Json做为前后端数据类型转换的纽带,将客户端数据转化为Json串发送到Java端,转换为Java方法所接收的数据类型,后端方法的返回值经过框架的处理转换为Json串返回客户端,再经过处理,客户端接收到的是Json类型的数据。
1.3 要点难点分析
1.3.1 建立远程调用模型
远程调用模型中最重要的部分是建立前后端的可用远程方法的映射,让使用者感觉后端方法就像在客户端一样。这需要对远程bean的Id,方法名,参数列表通过Ajax的方式传递到后台做解析和转换。
1.3.2 数据格式的统一
JavaScript与Java加起来常用的数据类型有几十种,在框架使用层面,这些对开发者来说应该是透明的。也就是说,对于这么多的数据类型,框架要在底层进行映射和转换,这个比较复杂的一个过程,因为框架并不知道开发者需要传什么样的数据类型到后端并用什么样的参数类型去接手前端的参数,因此需要对每一种数据类型都建立相应的可转换映射,还需要对可能发生的无法Cast的异常做处理。
1.3.3 安全及性能方面的考虑
从安全角度说,Rpc通过远程bean直接将业务层暴露给前端,这在需要进行权限验证,角色校验的业务系统中可能会有问题,而解决这个问题目前比较好的办法是Aop。通过Aop拦截Rpc控制器,并在这之前对权限做交验并决定是否允许这个请求到Rpc控制器去。
框架的性能也是值得关注的点。动态脚本的生成与反射调用的效率都有可能是制约框架性能提升的瓶颈。目前看来比较好的办法是对于动态脚本做Cache,在Web容器的生命周期内只生成一次脚本,不过在远程bean对象过多的情况下对服务器的内存是个挑战。后续也可以通过Cache接口实现更为复杂的Cache算法,对于不常用的方法进行清除,提供内存的可用率。
2 远程调用核心模型的设计
2.1 面向Web的Rpc
2.1.1 传统意义上的Rpc
Rpc的英文原义是:Remote Procedure Call Protocol。一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。传统意义上的Rpc是用于分布式系统的一种远程调用方式。
2.1.2 基于Ajax的Ajax-Rpc模型
本框架提出的概念是针对Web的Rpc远程调用,是为了解决Web编程中Ajax使用不便而推出的一种Ajax远程调用解决方案。远程调用的实质是Ajax请求。Rpc是作为Web应用程序中的servlet,filter部署的。把它看作一个黑盒子,这个 servlet主要有两个作用:首先,对于公开的每个类和方法,Rpc动态地生成包含在 Web页面中的 JavaScript。生成的JavaScript 包含远程调用函数,代表 Java 类上的对应方法并在幕后执行 XMLHttpRequest。这些请求被发送给Rpc,这时它的第二个作用就是把请求翻译成服务器端 Java 对象上的方法调用并把方法的返回值放在filter响应中发送回客户端,编码成Json。
图2-1 Rpc远程请求模型
2.2 远程bean对象模型
2.2.1 与Spring集成的远程bean对象
Spring是一个开源框架,它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。简单来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。正是因为有这样的特性,Spring迅速在开源社区被推广和使用。鉴于Spring使用的广泛性,Rpc提供了与Spring集成的接口,使Rpc与Spring无缝接合。
Spring拥有ApplicationContext来维护Ioc容器中所有的bean对象,而我们要获得容器中的对象的就得获取它的上下文。Spring提供了WebApplicationContextUtils工具类来操作它的上下文。利用getWebApplicationContext并传入Servlet的上下文,我们就能获得当前已经启动的ApplicationContext容器。通过ApplicationContext的getBean(String beanName)方法就能从Ioc容器中获取由Spring为我们创建的对象了。
2.2.2 远程bean对象的注册
要使用远程bean对象首先要对其进行注册。这样Rpc核心脚本控制器才能根据注册信息来生成与之相对应的远程访问脚本。Rpc框架采用引入动态脚本库的方式来进行注册。举个例子,只需在页面引入“”,这样名为testBean的bean对象就注册好了,在Spring的配置文件配置其对应的实现类即可。整体流程见图2-2。
图2-2 远程bean对象模型
2.2.3 基于Annotation的远程调用方法
众所周知客户端脚本在安全性上并不是很好,用户可以将网页另存篡改脚本从而改变网页的行为。Rpc远程调用的方式更是存在这样的问题,有些类方法是非常敏感的,是不能被公开的。在Rpc中,使用Annotation来对公开的方法进行注解。Annotationshi是在JDK1.5(tiger)中增加新的特色。Annotation提供一种机制,将程序的元素如:类,方法,属性,参数,本地变量,包和元数据联系起来。这样编译器可以将元数据存储在Class文件中。这样虚拟机和其它对象可以根据这些元数据来决定如何使用这些程序元素或改变它们的行为。Annotation最大的一个特点是无侵入性,相比xml配置文件的方式,它更简单,方便。
在Rpc远程调用框架中对远程方法的Annotation叫RemoteCall。下面是它的定义:
package com.zjnu.rpc;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Target(value=METHOD)
@Retention(value=RUNTIME)
public @interface RemoteCall{}
Target属性说明这个Annotation是用于方法的,而Retuntion为RUNTIME指这个注解会在编译时存储在class字节码中,可以被JVM通过反射读取。通过这个特性,可以识别对象内哪些方法是可以被公开的。
2.3 客户端/服务端的Rpc交互
2.3.1 前端核心库
使用Rpc框架需要在前端页面引入一个名为rpcCore.js 的Js库。rpcCore主要的职责是:
(1) 封装Ajax调用过程,提供统一的远程请求接口供动态生成的远程脚本调用
(2) 提供统一的客户端数据格式转换器
rpcCore主要有三个包ajaxTool,Rpc,Json。
其中ajaxTool是提供Ajax调用相关API的,主要是对浏览器间不同的Ajax调用方式做了简单的封装,对传参及获取返回值做了统一的处理。ajaxTool的主要方法有:
a) createXMLHttpRequest(私有)
创建XmlHttpRequest对象,根据不同的浏览器创建不同类型的异步请求对象。支持主流的浏览器,比如IE,FireFox等。
b) handleStateChange(私有)
处理Ajax请求返回的信息,判断Http请求是否成功,并获取执行结果的返回值。
c) sendXmlParmRequest(公开)
发送请求到服务端,默认采用“POST”,同步的方式将发送请求,并在成功时调用handleStageChange返回信息。
Rpc包是供服务端生成的远程bean调用脚本来使用的。Rpc包主要是用来处理客户端参数,并判断参数类型,将其组织为POST数据格式,利用远程bean的beanId和方法名等信息将这些数据发送到后端执行并获取返回结果再将其转化为JavaScript能解析的数据类型(Json)。Rpc包的方法主要有:
a) register(公开)
获取服务端传回的远程方法的相关信息,为其发送服务端请求做好数据准备。
b) remoteCall(公开)
Rpc包的核心方法,是处理客户端数据与服务端回传数据的总控。核心代码如下:
remoteCall:function(args){
var argLength = args.length;var param;
for(i=0;i
Json包主要封装了一些转换器,用来将各数据类型转化为Json字符串以便传给服务端以及将服务端回传的Json串转化为Js的数据类型以便前端处理。主要的方法有:
a) toJsonStr
将数据转化为Json串
b) toJson
将字符串串转化为Json
c) toJsType
将数据转化为Js格式的数据类型
d) toJavaType
将数据转化为Java端可用的数据格式
2.3.2 动态脚本控制器
动态脚本控制器是整个Rpc的核心部分,能让开发者像调用客户端方法一样调用服务端方法的关键就在这个控制器上。这个核心控制器是一个Servlet,在web.xml里我们是这样配置它的:
rpcInterfaceServlet
com.zjnu.rpc.RpcInterfaceServlet
rpcInterfaceServlet
/Rpc/*
这个Serlvet把所有ContextPath是“Rpc”的请求都拦截下来。“*”是通配符,匹配的是任意的远程bean的Id。首先控制器会通过拦截的请求获取bean的Id。根据这个Id,去Spring的容器中去获取相对应的bean对象。根据对象的class来反射出这个类的Method对象数组。接下来对这个数组进行遍历,根据Annotation反射选出其中加了RemoteCall标注的方法,这些方法是需要远程访问的,最关键的一步就是为这些方法生成相应的访问脚本。
在rpcCore核心库中的Rpc包里有一个remoteCall方法,这个方法就是用来处理客户端数据与服务端回传数据的。所以生成的脚本是这样的结构:
methodName:function(){
var rpc = new Rpc();
rpc.register(beanName,methodName);
rpc.remoteCall(arguments);
}
对于这样的映射只需要关注方法的名字,而无须关注到参数的长度,因为使用了关键字arguments来处理JavaScript端接收的参数,在最后调用服务端方法的时候再交验客户端所传参数的长度和格式是否能和服务端方法所匹配即可。
动态脚本控制器的核心代码如下:(只为描述实现思路,故异常处理,日志打印代码都被删减)
String beanName = req.getPathInfo().substring(1);
ApplicationContext apt = getApplicationContext();
bean = apt.getBean(beanName);
List remoteMethodList = new ArrayList();
StringBuilder sBuilder = new StringBuilder();
sBuilder.append("var ").append(beanName).append("={");
Method[] methods = bean.getClass().getDeclaredMethods();
for(int i=0,length=methods.length;i
动态脚本控制器实现流程见图2-3。
图2-3 动态脚本控制器工作流程
2.3.3 远程请求分发器
通过动态脚本控制器的动态脚本生成,针对每一个远程方法都已经在客户端有同名的JavaScript方法,此时解析出的网页已经具有了发送Rpc请求的能力。服务端同样需要对Rpc请求进行特殊的处理,这个处理器叫RpcDispatcher,意思就是远程请求的分发器,通过解析Rpc的相关参数去调用后端实际的方法。RpcDispatcher是一个过滤器,我们同样需要对它做一些配置。
rpcDispatcher
com.zjnu.rpc.RpcDispatcher
rpcDispatcher
*.call
过滤器拦截所有以“.call”结尾的请求。这类请求就是前端利用生成的脚本向后端发送的Rpc请求。因为面向对象的重载性,所以仅靠方法名是无法确定一个方法的。确定一个方法需要方法名和它的相关参数信息。在Rpc中,采用对方法进行编码的方式作为方法的唯一Key。比如对于方法
Public void getString(String abc,Integer bcd)来说,经过反射获取其方法名及参数类型组成的字符串为“getString(s,i)”,这个串就可以在某个类中确定一个方法了。在生成动态脚本时将此信息传递到前台,在前台调用远程方法时再回传到服务端。通过这个值来遍历获取要调用的Method对象。
除了获取要执行的Mehod外还要对客户端传过来的参数进行处理,Rpc请求包含三种类型的数据(见图2-4),所有数据都是包装在POST过来的Request中。遍历Rpc请求得到这三部分数据,接着执行Method并接收返回值,转换类型后传回客户端。
图2-4 Rpc请求模型
远程请求分发器的核心代码如下:
Map paramMap = request.getParameterMap();
Set keySet = paramMap.keySet();
String methodName = null,beanName = null;
Object param[] = new String[keySet.size()-2];
for(Iterator it = keySet.iterator();it.hasNext();){
String key = (String)it.next();
if("beanName".equals(key)){
beanName = ((String[])(paramMap.get("beanName")))[0];
}else if("methodName".equals(key)){
methodName = ((String[])(paramMap.get("methodName")))[0];
}else {
Integer paramIndex = Integer.parseInt(key);
param[paramIndex] = ((String[])(paramMap.get(key)))[0];
}
}
Object bean =apt.getBean(beanName);
Method method = getMethod(methodName);//根据方法唯一串来获取 Method对象
returnValue = method.invoke(bean, param);
远程请求分发器的工作流程见图2-5。
图2-5 远程请求分发器工作流程