一:
上一篇讲解了一下什么是rop以及如何搭建rop框架,本章节讲解,如何使用rop进行服务的调用以及参数的传递
同springMvc相似 rop也是通过在web.xml中配置拦截来声明该url链接启用rop框架。至于如何调用controller层内的方法,rop有专门的参数用来指定,这个下边会讲到。示例:
web.xml
<!-- rop servlet -->
<servlet>
<servlet-name>cop</servlet-name>
<servlet-class>
com.rop.RopServlet
</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>cop</servlet-name>
<url-pattern>/router</url-pattern>
</servlet-mapping>
举例来说。服务器 URL 为 api.xxx.com,而 RopServlet 的映射 URI 为/router,则服务统一 URL 为:
http://api.xxx.com/router
二:rop如何进行参数的传递
上一节中已经说过rop是一个服务型框架,如果你经常调用第三方平台的接口,我相信这点不用解释(这里不细讲,侧重点不同,如何有想细问的可以发表回复,我们可以单独交流)。
rop的参数分为两种
第一种是系统级参数,所谓的系统级参数是指平台开放的,比如调用支付宝接口 微信接口的时候,需要传递接口调用的一些必须参数,比如密钥,用户名等,用来验证身份的合法性,rop的安全验证同样是要求接口调用者传递必须的参数完成接口服务的调用。rop共有七个系统级参数如下:
系统级参数在服务接口调用的时候进行传递,注意参数是否为必填项
业务级参数
是由业务逻辑需要自行定义的,每个服务 API 都可以定义若干
个自己的业务级参数。 Rop 根据参数名和 RopRequest 类属性名相等的契约, 将业务级参数
绑定到 RopRequest 中
列如:
package com.yihg.api.request;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import com.rop.AbstractRopRequest;
import com.rop.annotation.IgnoreSign;
public class LoginRequest extends AbstractRopRequest {
//④
@IgnoreSign
private Integer id;
//手机号①
@NotNull
@Pattern(regexp=".{11}")
private String phoneNumber;
//密码①
@NotNull
private String password;
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setId(int id){
this.id = id;
}
public int getId(){
return id;
}
}
服务调用测试类
@Test
public void test_view_travelHistory() {
MultiValueMap<String, String> paramValues = new LinkedMultiValueMap<String, String>();
// 系统级参数②
paramValues.add("method", "view.TravelHistory");
paramValues.add("appKey", APP_KEY);
paramValues.add("appSecret", APP_SECRET);
paramValues.add("v", "1.0");
// paramValues.add("loginCode", "258@qq.com");
//①
paramValues.add("phoneNumber", "1");
paramValues.add("password", format);
//③
String sign = CopUtils.sign(paramValues.toSingleValueMap(), APP_SECRET);
paramValues.add("sign", sign);
//⑤
paramValues.add("id","1");
Date d = new Date();
long longtime = d.getTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String format = sdf.format(longtime);
String buildGetUrl = CopUtils.buildGetUrl(
paramValues.toSingleValueMap(), SERVER_URL);
String responseContent = new RestTemplate().getForObject(buildGetUrl,
String.class, paramValues);
System.out.println(responseContent);
}
上述测试示列①中指定的参数名称和LoginRequest 类中的名称相同,rop就会自动进行数据的绑定
② 是系统级参数的传递方式,③对参数进行签名 这里我们提到一点,系统级参数都是参与签名的,咱们的业务级参数可以通过在参数名称上添加@IgnoreSign 进行标注是否参与签名,如有标注则不参与签名,列如
④ id 则不参与签名,注意如果id不参与签名时,在进行参数拼接时将id放于签名后拼接,放在⑤的位置,或者在类上标注@IgnoreSign 指明该类中的所有字段都不参与签名。那么rop具体是如何进行参数转换的呢?
参数数据绑定
当客户端调用服务平台某个服务时,其实质是向服务平台的 URL 发送若干个请求参
数(包括系统级和业务级的参数) 。Rop 框架在接收到这些请求参数后,就会将其绑定到
RopRequest 请求对象中,服务方法可通过这个 RopRequest 对象获取请求参数信息
首先,客户端的服务请求通过 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
@ServiceMethod(method = "user.getSession",needInSession = NeedInSessionType.NO)
public Object 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 可以很方便地获取系统级参数、业
务级参数及客户端的相关信息
参数数据校验
由于应用客户端和服务平台都是服务报文进行通信的,所有的请求参数都以字符串的
形式传送过来。为了保证服务得到正确执行,必须事先对请求参数进行数据合法性验证,
只有在服务请求所有参数都符合约定的情况下,服务平台才执行具体的服务操作,否则直
接驳回请求,返回错误的报文。
数据校验从责任主体上看,可分为客户端校验和服务端校验两种。对于一个封闭式的
应用软件来说,由于服务端和客户端都是一体化开发的,为了减少开发工作量,有时仅需
要进行客户端校验就可以了。但是,服务开放平台的潜在调用者是不受限的,应用开发者
可基于服务平台开发出众多丰富多彩的应用,在这种场景下,服务端校验是必不可少的。
服务请求参数的校验是开放平台的一项重要的基础功能,Rop 和 Spring MVC 一样使用 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>
XML 和 JSON 参数绑定
如果某个请求参数的值是一个 XML 或 JSON 串,能否正确地进行绑定呢?Rop 框架
支持将 XML 或 JSON 格式的参数值透明地绑定到 RopRequest 的复合属性中
package com.rop.sample.request;
import javax.validation.Valid;
…
public class CreateUserRequest extends AbstractRopRequest {
@Pattern(regexp = "\\w{4,30}")
private String userName;
…
@Valid ①
private Address address;
}
Address 是一个复合对象属性,它的类结构对应 XML 的结构:
package com.rop.sample.request;
import javax.validation.constraints.Pattern;
import javax.xml.bind.annotation.*;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD) ①
@XmlRootElement(name = "address")
public class Address {
@XmlAttribute ②
@Pattern(regexp = "\\w{4,30}")
private String zoneCode;
@XmlAttribute
private String doorCode;
@XmlElementWrapper(name = "streets") ③
@XmlElement(name = "street")
private List<Street> streets;
}
SR 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 参数数据绑定的场景:
@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");①
…
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 参数
提供数据:
public void testServiceJsonRequestAttr() {
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.add("method", "user.add");
form.add("messageFormat", "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。
自定义数据转换器
对于复合结构的参数,我们推荐使用 XML 或 JSON 的格式指定参数内容。除此以外,
Rop 允许您通过注册自定义转换器支持自定义格式的参数。 Spring 3.0 新增了一个类型转换
的 核 心 框 架 , 可 以 实 现 任 意 两 个 类 型 对 象 数 据 的 转 换 , 即
org.springframework.core.convert.ConversionService,FormattingConversionService 扩展于
ConversionService,添加了格式化数据的功能。Spring 的数据类型转换体系是高度可扩展
的,Rop 就是基于 Spring 的类型转换体系实施参数数据绑定的工作,因此,Rop 允许开发
者定义自己的类型转换器。
在 Spring 的类型转换服务体系中,转换器是由 Converter
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 对象
的双向转换器:
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) {①
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) {②
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
<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 的属性:
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 的方
法:
@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 的更多内容,请参见 10.12 小节。
请求服务映射
Spring MVC 通过@RequestMapping 注解实现 HTTP 请求到处理方法的映射。类似的,
Rop 使用@ServiceMethod 注解实现 HTTP 请求到服务处理方法的映射。@ServiceMethod
只能对 Bean 的方法进行标注,且该方法的签名是受限的:拥有一个 RopRequest 的入参和
一个返回对象。
@ServiceMethod 的 method 和 version 属性值是必须的,method 代码服务方法名,而
version 表 示 版 本 号 。 如 代 码 清 单 1 的 getSession() 服 务 方 法 对 应 的 注 解 是
@ServiceMethod(method = “user.getSession”, version = “1.0”),它对应如下的服务请求:
http://《serverUrl》/《ropServletUri》?method=user.getSession&v=1.0&…。
@Service ①
public class UserService {
@ServiceMethod(method = "user.add", version = "1.0") ②
public Object addUser(CreateUserRequest request) {
...
}
}
服务开放平台一旦将服务发布出去后,其内部实现可以不断优化和调整,但是服务接
口必须保证不变,否则基于服务开发的第三方应用的运行稳定性就得不到保障。如果要调
整服务接口定义,必须升级版本,这也是 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 扫描器扫
描到。
@ServiceMethodBean(version = "1.0") ①
public class UserService {
@ServiceMethod(method = "user.add") ②
public Object addUser(CreateUserRequest request) {
...
}
@ServiceMethod(method = "user.get", httpAction = HttpAction.GET)③
public Object getUser(CreateUserRequest request) {
...
}
}
②和③处的服务方法的 version 都自动设置为 1.0,如果 UserService 业务类方法显式
指定了 version 属性,将会覆盖@ServiceMethodBean 的设置。
Rop 框架在启动时, 将创建代表 Rop 框架上下文的 RopContext 实例, 同时扫描 Spring
容器中所有的 Bean,将标注了@ServiceMethod 的 Bean 方法注册到 RopContext 的服务方
法注册表中。 这样, ServiceRouter 就可根据 RopContext 中的服务方法注册表进行请求服务
的路由了