Rop开发手册(2):最简单的服务开放平台框架

目录  
1.快速了解Rop  
2.请求服务模型  
3.应用授权及验证  
4.服务会话管理 
5.错误处理模型 
6.响应报文控制 
7.文件上传 
8.服务安全控制 
9.拦截器及事件体系 
10.性能调优 
11.开发客户端SDK 
12.参考资料 

传统Web Service请求模型  

请求模型设计的好坏将直接影响服务调用的难易程度,设计良好的请求模型可以让服务调用成为随时随地、信手拈来的事。此外,还能使服务接口清晰化,降低开发者理解服务的难度。我们先来了解一下传统Web Service的请求模型。 

SOAP请求模型  


Web Service基本上都是使用HTTP传输协议进行交互的,服务的响应报文一般支持XML和JSON两种格式,但Web Service服务请求模型却各有千秋。 
传统的Web Service采用SOAP请求报文,任何服务都对应一组SOAP请求/响应报文,服务的调用及报文解析都比较麻烦。举例来说,即使是调用一个诸如查看当天天气的简单服务,该服务仅有一个city的参数,在SOAP的世界里,您也必须将其封装成一个复杂的SOAP请求报文才行。一般情况下,不借助CXF、Axis这类框架你很难访问SOAP。 

REST请求模型  


但是,很多情况下,开发者往往希望自由地随时随地访问服务,比如,通过一个形如
引用
http://www.xxx.com/weather/{city}

的URL就可以访问服务获取响应。把服务看成一个类似于文档、图片式的普通资源,通过一个唯一的URL进行定位和调用――这就是现在方兴未艾的REST Web Service的中心思想。 
REST Web Service充分挖掘了HTTP通讯协议的内涵,借助HTTP方法(如GET、POST、PUT、DELETE等)及合理设计的服务URL,让Web Service达到不言自明的效果。 

豆瓣网的API就是采用标准的REST Web Service开发的,来看一个获取图书信息的API: 

引用
http://api.douban.com/book/subject/isbn/{isbnID}


该服务使用HTTP的GET方式调用,说白了就是您可以简单地在浏览器地址栏中敲入以下URL,就可以发起服务调用: 

引用
http://api.douban.com/book/subject/isbn/9787508630069


以上请求将获得《史蒂夫•乔布斯传》这本书的服务响应报文,它是一个XML报文,您既可以在浏览器中预览,也可以写一个程序消费这个响应报文,完成您要干的事情。这种服务调用方式,对于服务调用者非常亲切,因为它和访问一个网页并无二致。如果要学习REST Web Service的设计,豆瓣网的API就是不错的学习案例,我们来欣赏一下豆瓣网其它几个API: 

  • GET http://api.douban.com/movie/subject/{subjectID}:获取某个专题的信息,GET表示使用HTTP请求方法,下同;
  • GET http://api.douban.com/people/{userID}:获取某个用户的信息;
  • GET http://api.douban.com/people?q=douban&start-index=10&max-results=5:搜索用户,用户名通过q参数传递,其它两个参数是分页控制参数;
  • DELETE http://api.douban.com/review/{reviewID}:删除某篇评论。


采用REST请求模型发布的服务接口很清晰化、调用也很简单,REST服务已经模糊了服务和网页资源的界限。简单就是最好的,从这个意义上说REST确实优于SOAP,开发者也纷纷用脚做出了投票,弃SOAP之暗而投REST之明。 
REST在扛起挑战SOAP大旗时,对SOAP的战斗檄文是:复杂,笨重,EJB死灰复燃。但是,当REST得天下后,我们发现REST本身也存在一些刻板的东西。 

首先,经典的REST对HTTP请求方法的使用过于教条化:新增、更改、删除、获取资源的服务分别对应POST、PUT、DELETE和GET的HTTP请求方法。一般的Web服务器和浏览器都只支持GET和POST这两种HTTP请求方法,所以在实际应用中,REST希望充分挖掘HTTP请求方法能力的倡议遭遇了困难。 

其次,REST提倡为每个服务设计一个“达意”的URL,让服务的URL望文生义。从可读性,清晰化的角度上看,REST的这个建议是非常值得称赞的。但是,服务的消费者主体是程序,让每个服务对应不同的URL,反而让客户端程序不好写。 

综上所述,当前如日中天的REST Web Service自身也存在一些待改进的地方。淘宝的TOP的请求模型可以看成是REST的变体,首先,TOP提供的所有服务的URL都是一样的:即为http://gw.api.taobao.com/router/rest,使用method参数指定服务API名称,再通过其它参数指定服务的入参。由于平台所有服务的URL都相同,不同的服务方法通过method参数区分,反而让服务的调度变得简单了。 

Rop请求模型  

Rop请求模型的设计直接借鉴了TOP的思想,服务开放平台的所有服务URL是相同的,请求参数分为系统级参数和业务级参数两部分,系统级参数是所有服务API都拥有的参数,而业务级参数由具体服务API定义。 

统一服务URL  

采用Rop的服务开放平台,其所有的服务都使用统一的URL,Rop通过method系统级参数将请求路由到指定的服务方法中完成服务受理。如何设置这个统一的服务URL呢?答案很简单,即是通过RopServlet的<servlet-mapping>进行定义。 

服务平台最终的URL为:<开放平台根URL>/<RopServlet的映射URI>。举例来说,服务器URL为api.xxx.com,而RopServlet的映射URI为/router,则服务统一URL为: 

引用
http://api.xxx.com/router。


系统级参数  

系统级参数是由开放平台定义的一组参数,每个服务都拥有这些参数,用以传送框架级的参数信息。如我们前面提到的method就是一个系统级参数,使用该参数指定服务的名称。Rop共有7个系统级参数,在下表中说明: 

参数名称 是否必须 参数说明
appKey 应用键,开放平台用以确定客户端应用的身份,如000001,000002等。应用键对应一个密钥secret。要基于服务平台开发应用,必须事先通过申请获取appKey/secret后,才能进行应用的开发。
sessionId 会话ID,一般是一个36位的UUID,在登录服务平台后获取;
method 服务方法名,一般采用“名词+动词”的结构定义。如user.get、user.create等;
v 服务方法的版本号,如1.0、2.0等。一个具体的服务方法上method+v两者唯一确定。因此服务平台必须保证所有服务的method+v的唯一性。
format 通信报文格式,可选值为xml和json,默认为xml。
locale 本地化类型,默认为zh_CN。
sign 签名串,请求参数的签名,服务平台通过它验证请求数据的合法性。


locale、format这两个系统级参数的功用是不言自明的,而其它的系统级参数由于涉及到服务开放平台很多的领域性问题,需要一些背景知识的铺垫,因此我们将在后续内容中进行专门的介绍。 

默认情况下,系统级参数名是固定的,一般情况下,并不需要对调整它。如果希望使用自行定义的参数名称,可以使用<rop:sysparams/>进行定义,如下所示: 

sampleRopApplicationContext.xml:定义系统级参数名 
Xml代码   收藏代码
  1. <rop:sysparams   
  2.             format-param-name="messageFormat"  
  3.             appkey-param-name="app_key"/>  


业务级参数  

业务级参数,顾名思义是由业务逻辑需要自行定义的,每个服务API都可以定义若干个自己的业务级参数。Rop根据参数名和RopRequest类属性名相等的契约,将业务级参数绑定到RopRequest中。 

如LogonRequest定义了两个userName和password两个属性,Rop就会将HTTP请求参数值绑定到LogonRequest对象的同名属性中。 

参数数据绑定与验证  

参数数据绑定  


当客户端调用服务平台某个服务时,其实质是向服务平台的URL发送若干个请求参数(包括系统级和业务级的参数)。Rop框架在接收到这些请求参数后,就会将其绑定到RopRequest请求对象中,服务方法可通过这个RopRequest对象获取请求参数信息,进而执行相应的服务API并返回响应结果。下图描述了请求参数的转换过程: 


首先,客户端的服务请求通过HTTP报文发送给服务端的Servlet服务器(即HTTP服务器),Servlet服务器将HTTP报文转换成一个HttpServletRequest对象。然后通过RopServlet转交给Rop框架,Rop框架将HttpServletRequest转换成一个RopRequestContext对象。接着,ServiceRouter将RopRequestContext传给ServiceMethodAdapter,ServiceMethodAdapter在内部将RopRequestContext转换成RopRequest对象,输送给最终的服务方法。 
从上面的数据转换过程中,我们知道每当客户端发起一个服务调用时,Rop都会在内部创建一个RopRequestContext实例,它包含了所有的请求数据信息。 

下面,我们来了解一下RopRequestContext接口的方法: 

  • String getAppKey():获取appKey系统级参数的值。RopRequestContext为每个系统级参数都分配了一个对应的接口方法,如String getMethod()、String getSessionId()等;
  • HttpAction getHttpAction():获取HTTP请求方法,HttpAction是一个枚举,仅有两个枚举值,即GET和POST。这也说明,Rop仅支持GET和POST两个HTTP请求方法;
  • String getIp():获取请求来源的IP地址。由于在集群环境下,请求通过前端的负载均衡器再传给后端集群的某个具体服务节点。因此,直接使用ServletRequest#getRemoteAddr()返回的值将是前端负载均衡服务器的IP,在此Rop使用了一些技巧,以保证后端服务获取的IP是客户端的IP。具体实现可以参见com.rop.ServletRequestContextBuilder#getRemoteAddr(HttpServletRequest request)的实现;
  • Object getRawRequestObject():获取原请求对象,即服务请求对应的HttpServletRequest对象;
  • Map<String, String> getAllParams():获取服务请求所对应的所有请求参数。可以通过String getParamValue(String paramName)获取某个具体参数的值;
  • RopContext getRopContext():获取Rop框架上下文的信息。RopContext之于Rop框架相当于ServletContext之于Servlet容器,它包含了很多Rop框架的运行期信息,所有Rop的服务方法都注册在RopContext 中。


概括来说,RopRequestContext为每个系统级参数都提供了一个方法,如getAppKey()、getMethod()等。对于业务级参数,则可以使用RopRequestContext的getParamValue("<参数名>")获取。此外,RopRequestContext还提供了获取原始请求对象、客户端IP等方法。 

所有服务方法的入参都是RopRequest接口或其实现类,RopRequest接口仅有一个方法: 

RopRequestContext getRopRequestContext(); 

RopRequest的实现类负责定义业务级参数对应的属性,这样,在服务方法内部,就可以通过RopRequest#getRopRequestContext()获取RopRequestContext,再通过RopRequestContext访问到系统级参数了。而业务级参数是可通过RopRequest实现类的属性获取。 

在下面的getSession()服务方法中,我们定义了一个LogonRequest的请求对象,它就是一个实现了RopRequest接口的对象,在服务方法内部,可以通过LogonRequest访问到系统级参数、业务级参数及其它相关的信息,如下所示: 

UserService.java 
Java代码   收藏代码
  1. @ServiceMethod(method = "user.getSession",needInSession = NeedInSessionType.NO)   
  2. public RopResponse getSession(LogonRequest request) {  
  3.   
  4. //①访问系统级参数  
  5.         String appKey = request.getRopRequestContext().getAppKey();  
  6.   
  7. //②-1 访问业务级参数:通过类属性  
  8.         String userName1 = request.getUserName();  
  9. //②-2 访问业务级参数:通过RopRequestContext获取  
  10.         String userName2 = request.getRopRequestContext().getParamValue("userName");  
  11.   
  12. //③获取其它信息  
  13.         String ip = request.getRopRequestContext().getIp();  
  14. }  
通过上面的实例,我们可以知道通过RopRequest可以很方便地获取系统级参数、业务级参数及客户端的相关信息。 

参数数据验证  

由于应用客户端和服务平台都是服务报文进行通信的,所有的请求参数都以字符串的形式传送过来。为了保证服务得到正确执行,必须事先对请求参数进行数据合法性验证,只有在服务请求所有参数都符合约定的情况下,服务平台才执行具体的服务操作,否则直接驳回请求,返回错误的报文。 

数据校验从责任主体上看,可分为客户端校验和服务端校验两种。对于一个封闭式的应用软件来说,由于服务端和客户端都是一体化开发的,为了减少开发工作量,有时仅需要进行客户端校验就可以了。但是,服务开放平台的潜在调用者是不受限的,应用开发者可基于服务平台开发出众多丰富多彩的应用,在这种场景下,服务端校验是必不可少的。 

服务请求参数的校验是开放平台的一项重要的基础功能,Rop和Spring MVC一样使用JSR 303注解定义参数的校验规则,当请求数据违反校验规则时,直接返回对应的错误报文,只有所有请求参数都通过合法性验证后,才调用目标服务方法。 

下面的CreateUserRequest使用了JSR 303注解的,来看一下具体的使用方法: 

CreateUserRequest.java:使用JSR 303对业务级参数进行校验 
Java代码   收藏代码
  1. public class CreateUserRequest extends AbstractRopRequest {  
  2.   
  3.     @Pattern(regexp = "\\w{4,30}")  
  4.     private String userName;  
  5.   
  6.     @IgnoreSign  
  7.     @Pattern(regexp = "\\w{6,30}")  
  8.     private String password;  
  9.   
  10.     @DecimalMin("1000.00")  
  11.     @DecimalMax("100000.00")  
  12.     @NumberFormat(pattern = "#,###.##")  
  13.     private long salary;  
  14.   
  15. …  
  16. }  
对于系统级的参数,Rop本身会负责校验,开发者仅需关注业务级参数的校验即可。当请求的参数违反校验规则后,Rop将把这些错误“翻译成”对应的错误报文。假设salary格式不对,其对应的错误报文为: 
Xml代码   收藏代码
  1. <?xml version="1.0" encoding="utf-8" standalone="yes"?>  
  2. <error code="33">  
  3.     <message>非法的参数</message>  
  4.     <solution>请查看根据服务接口对参数格式的要求</solution>  
  5.     <subErrors>  
  6.         <subError code="isv.parameters-mismatch:salary-and-yyy">  
  7.             <message>传入的参数salary和aaa不匹配,两者有一定的对应关系</message>  
  8.         </subError>  
  9.     </subErrors>  
  10. </error  
关于错误处理模型及报文格式,我们将后续内容中讲解。 

XML和JSON参数绑定  

如果某个请求参数的值是一个XML或JSON串,能否正确地进行绑定呢?Rop框架支持将XML或JSON格式的参数值透明地绑定到RopRequest的复合属性中。 

我们通过rop-sample实例项目的UserService#addUser(CreateUserRequest request)讲解XML/JSON参数值绑定的内容。 CreateUserRequest拥有一个Address的业务级参数,如下所示: 

CreateUserRequest.java:复合属性 
Java代码   收藏代码
  1. package com.rop.sample.request;  
  2. import javax.validation.Valid;  
  3. …  
  4. public class CreateUserRequest extends AbstractRopRequest {  
  5.   
  6.     @Pattern(regexp = "\\w{4,30}")  
  7.     private String userName;  
  8.   
  9. …  
  10.       
  11.     //① 可绑定XML或JSON的复合属性,必须打上@Valid注解进行数据校验    
  12.     @Valid  
  13.     private Address address;  
  14. }  
Address是一个复合对象属性,它的类结构对应XML的结构: 

CreateUserRequest.java:复合属性 
Java代码   收藏代码
  1. package com.rop.sample.request;  
  2.   
  3. import javax.validation.constraints.Pattern;  
  4. import javax.xml.bind.annotation.*;  
  5. import java.util.List;  
  6.   
  7. // ①使用JSR 222注解,定义了基于属性名进行数据绑定的规则。  
  8. @XmlAccessorType(XmlAccessType.FIELD)   
  9. @XmlRootElement(name = "address")  
  10. public class Address {  
  11.   
  12.     //②使用JSR 222定义数据绑定规则,使用JSR 303注解定义数据校验规则。  
  13.     @XmlAttribute                    
  14.     @Pattern(regexp = "\\w{4,30}")  
  15.     private String zoneCode;  
  16.   
  17.     @XmlAttribute  
  18.     private String doorCode;  
  19.   
  20.   
  21.     // ③使用JSR 222注解指定列表数据的绑定规则  
  22.     @XmlElementWrapper(name = "streets")   
  23.     @XmlElement(name = "street")  
  24.     private List<Street> streets;  
  25. }  
JSR 222标准规范(也即JAXB),已经作为XML数据绑定官方标准添加到JDK 6.0核心库中。因此,我们直接使用JSR 222注解定义XML数据的绑定规则。官方标准的JAXB库只支持XML数据的绑定,很多开源进行了扩展,支持JSON数据的绑定,Rop使用Jackson项目完成JSON数据的绑定。 

请求数据绑定时一般都需要进行数据校验,因此您还需要使用JSR 303的注解定义数据校验规则。通过JSR 222和JSR 303注解两者珠联璧合,Rop很完美地解决了请求数据绑定和数据校验的问题。 

开发者仅需要在CreateUserRequest中标注上注解,无需做任何其它的开发工作,就可以绑定客户端的XML和JSON数据了。rop-sample项目的UserServiceRawClient有一个testServiceXmlRequestAttr()测试方法,它演示了XML参数数据绑定的场景: 

UserServiceRawClient.java:XML请求参数 
Java代码   收藏代码
  1. @Test  
  2. public void testServiceXmlRequestAttr() {  
  3.     RestTemplate restTemplate = new RestTemplate();  
  4.     MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();  
  5.     form.add("method""user.add");  
  6.     form.add("messageFormat""xml");//①定消息报文格式为XML格式  
  7.     …  
  8.       
  9.     //②ML格式的参数数据  
  10.     form.add("address",  
  11.             "<address zoneCode=\"0001\" doorCode=\"002\">\n" +   
  12.             "  <streets>\n" +  
  13.             "    <street no=\"001\" name=\"street1\"/>\n" +  
  14.             "    <street no=\"002\" name=\"street2\"/>\n" +  
  15.             "  </streets>\n" +  
  16.             "</address>");  
  17.   
  18.     //手工对请求参数进行签名  
  19.     String sign = RopUtils.sign(form.toSingleValueMap(), "abcdeabcdeabcdeabcdeabcde");  
  20.     form.add("sign", sign);  
  21.   
  22.      //调用服务获取响应报文  
  23.     String response = restTemplate.postForObject(SERVER_URL, form, String.class);  
  24. }  
如果有某个请求参数的内容是XML,必须将报文格式设置成xml,如①所示。在②处,address的参数值即是一个XML格式的字符串,它将正确绑定到CreateUserRequest的address属性中。 

相似的,下面的testServiceJsonRequestAttr()测试方法则使用JSON格式为address参数提供数据: 

UserServiceRawClient.java:XML请求参数 
Java代码   收藏代码
  1. public void testServiceJsonRequestAttr() {  
  2.     RestTemplate restTemplate = new RestTemplate();  
  3.     MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();  
  4.     form.add("method""user.add");  
  5.     form.add("messageFormat""json");//①指定消息格式为JSON格式  
  6.   
  7.         //②JSON格式的参数数据  
  8.     form.add("address",  
  9.             "{\"zoneCode\":\"0001\",\n" +                           
  10.             " \"doorCode\":\"002\",\n" +  
  11.             " \"streets\":[{\"no\":\"001\",\"name\":\"street1\"},\n" +  
  12.             "            {\"no\":\"002\",\"name\":\"street2\"}]}");  
  13.   
  14.     String sign = RopUtils.sign(form.toSingleValueMap(), "abcdeabcdeabcdeabcdeabcde");  
  15.     form.add("sign", sign);  
  16.   
  17.     String response = restTemplate.postForObject(SERVER_URL, form, String.class);  
  18. }  
将报文格式设置为json,即可支持JSON格式参数数据的绑定。在默认情况下,Rop不允许同时使用XML和JSON,仅能两者取一:请求和响应报文要么是XML,要么是JSON。 

自定义数据转换器  

对于复合结构的参数,我们推荐使用XML或JSON的格式指定参数内容。除此以外,Rop允许您通过注册自定义转换器支持自定义格式的参数。Spring 3.0新增了一个类型转换的核心框架,可以实现任意两个类型对象数据的转换,即org.springframework.core.convert.ConversionService,FormattingConversionService扩展于ConversionService,添加了格式化数据的功能。Spring的数据类型转换体系是高度可扩展的,Rop就是基于Spring的类型转换体系实施参数数据绑定的工作,因此,Rop允许开发者定义自己的类型转换器。 

在Spring的类型转换服务体系中,转换器是由Converter<S, T>接口定义,它仅能实现单向转换,即从S到T的转换。但是Rop需要双向转换功能:在服务端将参数绑定到RopRequest时,将S转换成T,而在客户端将RopRequest流化成请求报文时,需要将T转换成S。因此,Rop对Converter<S, T>接口进行了扩展,定义了一个可以实现双向转换的接口,如下所示: 

Java代码   收藏代码
  1. package com.rop.request;  
  2. import org.springframework.core.convert.converter.Converter;  
  3. public interface RopConverter<S, T> extends Converter<S, T> {  
  4.   
  5.     S unconvert(T target);  
  6.   
  7.     Class<S> getSourceClass();  
  8.     Class<T> getTargetClass();  
  9. }  
Converter<S, T>接口定义了一个T convert(S source)的方法,RopConverter<S, T>新增了一个S unconvert(T target)的方法,这样就可以实现S和T两者的双向转换了。 

开发一个类型转换器是件轻松的事情,仅需扩展RopConverter<S, T>接口并实现S和T相互转换的逻辑即可。rop-sample中定义了一个可实现格式化电话号码和Telephone对象的双向转换器: 

Java代码   收藏代码
  1. TelephoneConverter.java:双向类型转换器  
  2. package com.rop.sample.request;  
  3.   
  4. import com.rop.request.RopConverter;  
  5. import org.springframework.core.convert.converter.Converter;  
  6. import org.springframework.util.StringUtils;  
  7.   
  8. public class TelephoneConverter implements RopConverter<String, Telephone> {  
  9.   
  10.     @Override  
  11.     public Telephone convert(String source) {//①将格式化字符串转换为Telephone   
  12.         if (StringUtils.hasText(source)) {  
  13.             String zoneCode = source.substring(0, source.indexOf("-"));  
  14.             String telephoneCode = source.substring(source.indexOf("-") + 1);  
  15.             Telephone telephone = new Telephone();  
  16.             telephone.setZoneCode(zoneCode);  
  17.             telephone.setTelephoneCode(telephoneCode);  
  18.             return telephone;  
  19.         } else {  
  20.             return null;  
  21.         }  
  22.     }  
  23.   
  24.     @Override  
  25.     public String unconvert(Telephone target) {//②将Telephone转换为格式化字符串  
  26.         StringBuilder sb = new StringBuilder();  
  27.         sb.append(target.getZoneCode());  
  28.         sb.append("-");  
  29.         sb.append(target.getTelephoneCode());  
  30.         return null;  
  31.     }  
  32.   
  33.     @Override  
  34.     public Class<String> getSourceClass() {  
  35.         return String.class;  
  36.     }  
  37.   
  38.     @Override  
  39.     public Class<Telephone> getTargetClass() {  
  40.         return Telephone.class;  
  41.     }  
  42. }  
接下来的工作是如何将TelephoneConverter注册到Rop中,以便Rop在进行参数数据绑定时利用这个转换器。 
Rop的<rop:annotation-driven/>拥有一个formatting-conversion-service属性,可以通过该属性指定一个Spring的FormattingConversionService。在FormattingConversionService中即可注册自定义的Converter,如下所示: 

sampleRopApplicationContext.xml:注册自定义类型转换器 
Xml代码   收藏代码
  1. <rop:annotation-driven formatting-conversion-service="conversionService"/>  
  2. <bean id="conversionService"   
  3. class="org.springframework.format.support.FormattingConversionServiceFactoryBean">  
  4.         <property name="converters">  
  5.             <set>  
  6.                 <!--将xxxx-yyy格式化串转换为Telephone对象-->  
  7.                 <bean class="com.rop.sample.request.TelephoneConverter"/>  
  8.             </set>  
  9.         </property>  
  10.  </bean>  


CreateUserRequest中拥有一个Telephone的属性: 

CreateUserRequest.java 
Java代码   收藏代码
  1. public class CreateUserRequest extends AbstractRopRequest {  
  2.   
  3.     @Pattern(regexp = "\\w{4,30}")  
  4.     private String userName;  
  5.   
  6.     @IgnoreSign  
  7.     @Pattern(regexp = "\\w{6,30}")  
  8.     private String password;  
  9.   
  10.     private Telephone telephone;  
  11.   
  12. …  
  13. }  
Telephone类拥有zoneCode和telephoneCode两个属性,Rop在处理Telephone类型的数据绑定时,将自动调用TelephoneConverter进行数据转换。 

UserServiceClient#testCustomConverter()演示了客户端使用TelephoneConverter的方法: 

UserServiceClient.java:测试自定义类型转换器 
Java代码   收藏代码
  1. @Test  
  2. public void testCustomConverter() {  
  3.       
  4. ropClient.addRopConvertor(new TelephoneConverter());// ①  
  5.       
  6. CreateUserRequest request = new CreateUserRequest();  
  7.     request.setUserName("tomson");  
  8.     request.setSalary(2500L);  
  9.     Telephone telephone = new Telephone();  
  10.     telephone.setZoneCode("0592");  
  11.     telephone.setTelephoneCode("12345678");  
  12.   
  13.     CompositeResponse response = ropClient.buildClientRequest()  
  14. .post(request, CreateUserResponse.class"user.add""1.0");  
  15.   
  16.     assertNotNull(response);  
  17.     assertTrue(response.isSuccessful());  
  18.     assertTrue(response.getSuccessResponse() instanceof CreateUserResponse);  
  19. }  
在①处,RopClient注册了一个TelephoneConverter实现,当调用post()发送服务请求时,TelephoneConverter就会自动将Telephone对象转换成一个xxx-yyy的格式化串,并以请求报文的方式发送给服务端。而服务端则会利用注册在ConversionService中的TelephoneConverter,将xxx-yyy格式的电话号转为Telephone对象。 

从上面的分析可知,RopConverter#unconvert()是服务于客户端,而RopConverter#convert()则服务于服务端。由于客户端和服务端位于不同的JVM中,因此必须各自独立注册RopConverter,关于RopClient的更多内容,将在后续内容中介绍。 


请求服务映射  

Spring MVC通过@RequestMapping注解实现HTTP请求到处理方法的映射。类似的,Rop使用@ServiceMethod注解实现HTTP请求到服务处理方法的映射。@ServiceMethod只能对Bean的方法进行标注,且该方法的签名是受限的:拥有一个RopRequest的入参和一个返回对象。 

@ServiceMethod的method和version属性值是必须的,method代码服务方法名,而version表示版本号。如代码清单10-1的getSession()服务方法对应的注解是@ServiceMethod(method = "user.getSession", version = "1.0"),它对应如下的服务请求:  
引用
http://<serverUrl>/<ropServletUri>?method=user.getSession&v=1.0&...
来看一个具体的例子: 

UserService.java 
Java代码   收藏代码
  1. @Service //①服务类必须是一个Spring的Bean  
  2. public class UserService {  
  3.   
  4.     //②服务方法对应如下的HTTP请求:?method=user.add&v=1.0&…  
  5.     @ServiceMethod(method = "user.add", version = "1.0")   
  6.     public RopResponse addUser(CreateUserRequest request) {  
  7.        ...  
  8.     }  
  9. }  
服务开放平台一旦将服务发布出去后,其内部实现可以不断优化和调整,但是服务接口必须保证不变,否则基于服务开发的第三方应用的运行稳定性就得不到保障。如果要调整服务接口定义,必须升级版本,这也是Rop为什么要求方法名一定要和版本同时提供的原因。 

一个服务方法可以同时存在多个版本,客户端可以调用指定版本的服务。来看几个不同版本的服务及对应的客户端调用参数: 

  • @ServiceMethod(method = "user.add", version = "2.0"):对应method=user.add&v=2.0;
  • @ServiceMethod(method = "user.add", version = "3.0"):对应method=user.add&v=3.0;
  • @ServiceMethod(method = "user.get", version = "1.5"):对应method=user.get&v=1.5;
@ServiceMethod除了method和version属性外,还拥有多个其它的属性,分别说明如下: 
  • group:服务分组名。服务的分组没有特殊的意义,您可以为服务定义一个分组,以便在事件监听器、服务拦截器中利用分组信息进行特殊的控制。默认的分组为ServiceMethodDefinition.DEFAULT_GROUP;
  • groupTitle:服务分组标识;
  • tags:tags的类型是一个String[],您可以给服务打上一个或多个TAG,以便在事件处理监听器、服务拦截器利用该信息进行特殊的处理;
  • title:服务的标识;
  • httpAction:服务允许的HTTP请求方法,可选值在HttpAction枚举中定义,即GET或POST,如果不指定则不限制;
  • needInSession:表示该服务方法是否需要工作在会话环境中,默认所有的服务方法必须工作于会话环境中,也即请求的sessionId不能为空。如果某个方法不需要工作于会话环境中(如登录的服务方法、获取应用最新版本的服务方法),则必须显式设置:needInSession = NeedInSessionType.NO;
  • ignoreSign:表示该服务方法是否要进行请求数据签名验证,默认为需要。如果不需要,可以设置:ignoreSign=IgnoreSignType.NO。正式环境务必开启请求签名验证的功能,这样才能对客户端请求的合法性进行校验;
  • timeout:服务超时时间,单位为秒。如果服务方法执行时间超过timeout后,Rop将直接中断服务并返回错误的报文。
@ServiceMethod拥有众多的可设置属性,它们都和Rop具体的领域性问题相关联,因此,在这里只要知道method和version的属性就可以了,后面会对其它的属性进行深入的讲解。 

如果一个服务类中拥有多个服务方法,而它们拥有一些共同的属性,如group、version等,能否在某个地方统一定义呢?答案是肯定的,Rop为复用服务方法元数据信息提供了一个类级别的@ServiceMethodBean。 
@ServiceMethodBean拥有一套和@ServiceMethod类似的属性,其属性值会被同一服务类中所有的@ServiceMethod继承。 

@ServiceMethodBean类本身已经标注了Spring的@Service,所以标注了@ServiceMethodBean的服务类就相当于打上的@Service,可以被Spring的Bean扫描器扫描到。 

下面的例子拥有两个服务方法,它们的version都是1.0: 

UserService.java:使用@ServiceMethodBean 
Java代码   收藏代码
  1. @ServiceMethodBean(version = "1.0") ①  
  2. public class UserService {  
  3.   
  4.     @ServiceMethod(method = "user.add") ②  
  5.     public RopResponse addUser(CreateUserRequest request) {  
  6.        ...  
  7.     }  
  8.   
  9.    @ServiceMethod(method = "user.get", httpAction = HttpAction.GET)③  
  10.     public RopResponse getUser(CreateUserRequest request) {  
  11. ...  
  12. }  
  13. }  

②和③处的服务方法的version都自动设置为1.0,如果UserService 业务类方法显式指定了version属性,将会覆盖@ServiceMethodBean的设置。 
Rop框架在启动时,将创建代表Rop框架上下文的RopContext实例,同时扫描Spring容器中所有的Bean,将标注了@ServiceMethod的Bean方法注册到RopContext的服务方法注册表中。这样,ServiceRouter就可根据RopContext中的服务方法注册表进行请求服务的路由了。


转自:http://stamen.iteye.com/blog/1628498