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

:D [quote]Rop项目文档前后写了一个多月,在写的过程中发现很多功能不完善的,又回过头了改项目,此种反复多次,现在Rop开源项目的文档已经差不多了,将陆续在iteye此发出,希望对大家有帮助。在Rop项目的开发过程中得到了众多iteye网友的有力帮助,收到了很多很好的整改的意见,在此对他们的奉献和帮助表示诚挚的感谢,这些网友包括但不限于:
[url=http://melin.iteye.com/]melin[/url]
[url=http://kellokitty.iteye.com/]kellokitty[/url]
[url=http://http://xiegengcai.iteye.com/]風一樣的男子[/url]
[url=http://lioliang.iteye.com]lioliang[/url]
[url=http://jychenok.iteye.com/]木木的爱情童话[/url]
[url=http://devotionalist.iteye.com/]devotionalist[/url]
[url=http://ericliang.iteye.com/]ericliang[/url]
:) :idea:
[/quote]

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

[b][size=x-large]传统Web Service请求模型[/size][/b]

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

[b][size=large]SOAP请求模型[/size][/b]


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

[b][size=large]REST请求模型[/size][/b]


但是,很多情况下,开发者往往希望自由地随时随地访问服务,比如,通过一个形如[quote]http://www.xxx.com/weather/{city}[/quote]
的URL就可以访问服务获取响应。把服务看成一个类似于文档、图片式的普通资源,通过一个唯一的URL进行定位和调用――这就是现在方兴未艾的REST Web Service的中心思想。
REST Web Service充分挖掘了HTTP通讯协议的内涵,借助HTTP方法(如GET、POST、PUT、DELETE等)及合理设计的服务URL,让Web Service达到不言自明的效果。

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

[quote]http://api.douban.com/book/subject/isbn/{isbnID}[/quote]

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

[quote]http://api.douban.com/book/subject/isbn/9787508630069[/quote]

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

[list]
[*]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}:删除某篇评论。
[/list]

采用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参数区分,反而让服务的调度变得简单了。

[b][size=x-large]Rop请求模型[/size][/b]

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

[b][size=large]统一服务URL[/size][/b]

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

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

[quote]http://api.xxx.com/router。[/quote]

[b][size=large]系统级参数[/size][/b]

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

[table]
|参数名称 |是否必须 |参数说明|
|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 |是 |签名串,请求参数的签名,服务平台通过它验证请求数据的合法性。|
[/table]

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

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

sampleRopApplicationContext.xml:定义系统级参数名
<rop:sysparams 
format-param-name="messageFormat"
appkey-param-name="app_key"/>


[b][size=large]业务级参数[/size][/b]

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

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

[b][size=x-large]参数数据绑定与验证[/size][/b]

[b][size=large]参数数据绑定[/size][/b]


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

[align=center][img]http://dl.iteye.com/upload/attachment/0072/0907/ce61e60b-0f02-330d-945f-f80a0913f18e.jpg[/img]
图1[/align]


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

下面,我们来了解一下RopRequestContext接口的方法:
[list]
[*]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 中。
[/list]

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

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

RopRequestContext getRopRequestContext();

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

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

UserService.java
@ServiceMethod(method = "user.getSession",needInSession = NeedInSessionType.NO) 
public RopResponse getSession(LogonRequest request) {

//①访问系统级参数
String appKey = request.getRopRequestContext().getAppKey();

//②-1 访问业务级参数:通过类属性
String userName1 = request.getUserName();
//②-2 访问业务级参数:通过RopRequestContext获取
String userName2 = request.getRopRequestContext().getParamValue("userName");

//③获取其它信息
String ip = request.getRopRequestContext().getIp();
}


通过上面的实例,我们可以知道通过RopRequest可以很方便地获取系统级参数、业务级参数及客户端的相关信息。

[b][size=large]参数数据验证[/size][/b]

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

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

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

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

CreateUserRequest.java:使用JSR 303对业务级参数进行校验
public class CreateUserRequest extends AbstractRopRequest {

@Pattern(regexp = "\\w{4,30}")
private String userName;

@IgnoreSign
@Pattern(regexp = "\\w{6,30}")
private String password;

@DecimalMin("1000.00")
@DecimalMax("100000.00")
@NumberFormat(pattern = "#,###.##")
private long salary;


}


对于系统级的参数,Rop本身会负责校验,开发者仅需关注业务级参数的校验即可。当请求的参数违反校验规则后,Rop将把这些错误“翻译成”对应的错误报文。假设salary格式不对,其对应的错误报文为:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error code="33">
<message>非法的参数</message>
<solution>请查看根据服务接口对参数格式的要求</solution>
<subErrors>
<subError code="isv.parameters-mismatch:salary-and-yyy">
<message>传入的参数salary和aaa不匹配,两者有一定的对应关系</message>
</subError>
</subErrors>
</error

关于错误处理模型及报文格式,我们将后续内容中讲解。

[b][size=x-large]XML和JSON参数绑定[/size][/b]

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

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

CreateUserRequest.java:复合属性
package com.rop.sample.request;
import javax.validation.Valid;

public class CreateUserRequest extends AbstractRopRequest {

@Pattern(regexp = "\\w{4,30}")
private String userName;



//① 可绑定XML或JSON的复合属性,必须打上@Valid注解进行数据校验
@Valid
private Address address;
}

Address是一个复合对象属性,它的类结构对应XML的结构:

CreateUserRequest.java:复合属性
package com.rop.sample.request;

import javax.validation.constraints.Pattern;
import javax.xml.bind.annotation.*;
import java.util.List;

// ①使用JSR 222注解,定义了基于属性名进行数据绑定的规则。
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "address")
public class Address {

//②使用JSR 222定义数据绑定规则,使用JSR 303注解定义数据校验规则。
@XmlAttribute
@Pattern(regexp = "\\w{4,30}")
private String zoneCode;

@XmlAttribute
private String doorCode;


// ③使用JSR 222注解指定列表数据的绑定规则
@XmlElementWrapper(name = "streets")
@XmlElement(name = "street")
private List<Street> streets;
}


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请求参数
@Test
public void testServiceXmlRequestAttr() {
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.add("method", "user.add");
form.add("messageFormat", "xml");//①定消息报文格式为XML格式


//②ML格式的参数数据
form.add("address",
"<address zoneCode=\"0001\" doorCode=\"002\">\n" +
" <streets>\n" +
" <street no=\"001\" name=\"street1\"/>\n" +
" <street no=\"002\" name=\"street2\"/>\n" +
" </streets>\n" +
"</address>");

//手工对请求参数进行签名
String sign = RopUtils.sign(form.toSingleValueMap(), "abcdeabcdeabcdeabcdeabcde");
form.add("sign", sign);

//调用服务获取响应报文
String response = restTemplate.postForObject(SERVER_URL, form, String.class);
}


如果有某个请求参数的内容是XML,必须将报文格式设置成xml,如①所示。在②处,address的参数值即是一个XML格式的字符串,它将正确绑定到CreateUserRequest的address属性中。

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

UserServiceRawClient.java:XML请求参数
public void testServiceJsonRequestAttr() {
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.add("method", "user.add");
form.add("messageFormat", "json");//①指定消息格式为JSON格式

//②JSON格式的参数数据
form.add("address",
"{\"zoneCode\":\"0001\",\n" +
" \"doorCode\":\"002\",\n" +
" \"streets\":[{\"no\":\"001\",\"name\":\"street1\"},\n" +
" {\"no\":\"002\",\"name\":\"street2\"}]}");

String sign = RopUtils.sign(form.toSingleValueMap(), "abcdeabcdeabcdeabcdeabcde");
form.add("sign", sign);

String response = restTemplate.postForObject(SERVER_URL, form, String.class);
}


将报文格式设置为json,即可支持JSON格式参数数据的绑定。在默认情况下,Rop不允许同时使用XML和JSON,仅能两者取一:请求和响应报文要么是XML,要么是JSON。


[b][size=x-large]自定义数据转换器[/size][/b]

对于复合结构的参数,我们推荐使用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>接口进行了扩展,定义了一个可以实现双向转换的接口,如下所示:

package com.rop.request;
import org.springframework.core.convert.converter.Converter;
public interface RopConverter<S, T> extends Converter<S, T> {

S unconvert(T target);

Class<S> getSourceClass();
Class<T> getTargetClass();
}


Converter<S, T>接口定义了一个T convert(S source)的方法,RopConverter<S, T>新增了一个S unconvert(T target)的方法,这样就可以实现S和T两者的双向转换了。

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

TelephoneConverter.java:双向类型转换器
package com.rop.sample.request;

import com.rop.request.RopConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

public class TelephoneConverter implements RopConverter<String, Telephone> {

@Override
public Telephone convert(String source) {//①将格式化字符串转换为Telephone
if (StringUtils.hasText(source)) {
String zoneCode = source.substring(0, source.indexOf("-"));
String telephoneCode = source.substring(source.indexOf("-") + 1);
Telephone telephone = new Telephone();
telephone.setZoneCode(zoneCode);
telephone.setTelephoneCode(telephoneCode);
return telephone;
} else {
return null;
}
}

@Override
public String unconvert(Telephone target) {//②将Telephone转换为格式化字符串
StringBuilder sb = new StringBuilder();
sb.append(target.getZoneCode());
sb.append("-");
sb.append(target.getTelephoneCode());
return null;
}

@Override
public Class<String> getSourceClass() {
return String.class;
}

@Override
public Class<Telephone> getTargetClass() {
return Telephone.class;
}
}


接下来的工作是如何将TelephoneConverter注册到Rop中,以便Rop在进行参数数据绑定时利用这个转换器。
Rop的<rop:annotation-driven/>拥有一个formatting-conversion-service属性,可以通过该属性指定一个Spring的FormattingConversionService。在FormattingConversionService中即可注册自定义的Converter,如下所示:

sampleRopApplicationContext.xml:注册自定义类型转换器
<rop:annotation-driven formatting-conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<!--将xxxx-yyy格式化串转换为Telephone对象-->
<bean class="com.rop.sample.request.TelephoneConverter"/>
</set>
</property>
</bean>


CreateUserRequest中拥有一个Telephone的属性:

CreateUserRequest.java
public class CreateUserRequest extends AbstractRopRequest {

@Pattern(regexp = "\\w{4,30}")
private String userName;

@IgnoreSign
@Pattern(regexp = "\\w{6,30}")
private String password;

private Telephone telephone;


}


Telephone类拥有zoneCode和telephoneCode两个属性,Rop在处理Telephone类型的数据绑定时,将自动调用TelephoneConverter进行数据转换。

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

UserServiceClient.java:测试自定义类型转换器
@Test
public void testCustomConverter() {

ropClient.addRopConvertor(new TelephoneConverter());// ①

CreateUserRequest request = new CreateUserRequest();
request.setUserName("tomson");
request.setSalary(2500L);
Telephone telephone = new Telephone();
telephone.setZoneCode("0592");
telephone.setTelephoneCode("12345678");

CompositeResponse response = ropClient.buildClientRequest()
.post(request, CreateUserResponse.class, "user.add", "1.0");

assertNotNull(response);
assertTrue(response.isSuccessful());
assertTrue(response.getSuccessResponse() instanceof CreateUserResponse);
}


在①处,RopClient注册了一个TelephoneConverter实现,当调用post()发送服务请求时,TelephoneConverter就会自动将Telephone对象转换成一个xxx-yyy的格式化串,并以请求报文的方式发送给服务端。而服务端则会利用注册在ConversionService中的TelephoneConverter,将xxx-yyy格式的电话号转为Telephone对象。

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


[b][size=x-large]请求服务映射[/size][/b]

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"),它对应如下的服务请求:

[quote]http://<serverUrl>/<ropServletUri>?method=user.getSession&v=1.0&...[/quote]

来看一个具体的例子:

UserService.java
@Service //①服务类必须是一个Spring的Bean
public class UserService {

//②服务方法对应如下的HTTP请求:?method=user.add&v=1.0&…
@ServiceMethod(method = "user.add", version = "1.0")
public RopResponse addUser(CreateUserRequest request) {
...
}
}


服务开放平台一旦将服务发布出去后,其内部实现可以不断优化和调整,但是服务接口必须保证不变,否则基于服务开发的第三方应用的运行稳定性就得不到保障。如果要调整服务接口定义,必须升级版本,这也是Rop为什么要求方法名一定要和版本同时提供的原因。

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

[list]
[*]@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;
[/list]

@ServiceMethod除了method和version属性外,还拥有多个其它的属性,分别说明如下:
[list]
[*]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将直接中断服务并返回错误的报文。
[/list]


@ServiceMethod拥有众多的可设置属性,它们都和Rop具体的领域性问题相关联,因此,在这里只要知道method和version的属性就可以了,后面会对其它的属性进行深入的讲解。

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

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

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

UserService.java:使用@ServiceMethodBean
@ServiceMethodBean(version = "1.0") ①
public class UserService {

@ServiceMethod(method = "user.add") ②
public RopResponse addUser(CreateUserRequest request) {
...
}

@ServiceMethod(method = "user.get", httpAction = HttpAction.GET)③
public RopResponse getUser(CreateUserRequest request) {
...
}
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值