HttpRpc组件(HttpService)介绍
HttpRpc组件(HttpService)介绍
当看到Feign的源码,我就想到我曾经写的HttpService,在springCloud还没有流行的时候,我曾经开发的HttpService的中间件就已经在使用类似的机制来实现远程调用了.
在差不多12年的时候,当时设计的初衷就是不想使用Dubbo,因为dubbo太重量级,如果直接通过http进行远程服务的调用,会更加轻量级.而早在spring1.x的年代,spring就已经推出HttpInvoker,使用Http协议进行远程调用了.当时我们看到HttpInvoker非常不好用,就自己写了HttpService.
所以直到目前为止,我对于springCloud中广为吹牛的ribbon,hytrix等功能,都不是太感冒,中间整了那么多概念,本质上就是通过java来实现了NGINX的功能,非常华而不实.
等到springCloud的热潮过去以后,看看HttpService作为一个轻量级的远程调用,能否还有用武之地.
运行示例
请移步state-demo源码,在mock工程内.
代码集成
maven引入
<dependency>
<groupId>com.9istock.base</groupId>
<artifactId>qilu-http-service</artifactId>
<version>1.0.0</version>
</dependency>接口声明
整体注解使用JSR311整个restful的规范来进行.不需要依赖其他的包.
@Path("httpMock")
public interface HttpServiceMock {
public ApiBaseResponse<String> test1(@QueryParam("requestId") String requestId);
public ApiBaseResponse<String> test2(ModelTest model , MdoelMock mock);
@Consumes(value="application/json")
public ApiBaseResponse<String> test3(Map<String , Object> paramMap);
@Consumes(value="application/json")
public ApiBaseResponse<String> test4(List<ModelTest> modelList);
public ApiBaseResponse<String> test5(@QueryParam("numberList")Integer[] numberList);
}客户端spring配置
这部分的源码在qilu-state的sample工程里面
<bean id="mockService" class="com.istock.base.qiluHttpService.client.HttpProxyFactoryCglib">
<!-- 模拟的接口,一般声明处理,让controller实现 -->
<property name="targetClass" value="com.istock.state.demo.api.MockService"></property>
<!-- 服务端地址,一般配置http://ip:端口/contextPath 后续部门会通过@Path拼接 -->
<property name="endPoint" value="${mock.endpoint}"></property>
<!-- 超时时间,很重要,默认5s,不建议不配置 -->
<property name="timeOut" value="1000"></property>
<!-- 请求的后缀,可以为空 -->
<property name="methodFix" value=".do"></property>
<!-- 请求拦截器,类似于Feign中的RequestInterceptor,可以自定义 -->
<!-- 这边模拟的是一个请求需要签名的玩法,默认的签名算法,是Md5(排序(参数)&key) -->
<property name="requestHandler">
<bean class="com.istock.base.qiluHttpService.client.SignHttpRequestHandler">
<property name="signName" value="senvon"></property>
<property name="signKey" value="12345"></property>
</bean>
</property>
</bean>客户端的调用
当使用到代理对象时,客户端通过接口类中声明的annotation,拼装请求url和请求内容,拼装的内容如上.
@Autowired
private HttpServiceMock mock;客户端使用autowired的方式通过spring注入对象
test1的日志打印
//public ApiBaseResponse<String> test1(@QueryParam("requestId") String requestId);
INFO c.i.b.h.u.HttpSignCalculator - sign calcuate param:requestId=124&12345
INFO c.i.b.h.u.HttpConnectionUtils - ready to send post http request:http://localhost:17070/mock/httpMock/test1.do ,params:,body:{"requestId":"124"} , header:{senvon=1867852F1EB36D63441B100291DB8DDB}
INFO c.i.h.HttpServiceController - ready to test1 ================124@QueryPram的注解,是为了在生成form请求时候,生成a=xxx的a,spring是通过ASM,到编译后的class文件中找到的参数名,这边是通过@QueryParam进行指定的.
test2的日志打印
在test2中,因为传入2个bean,bean的属性会被拆分为requestMap,属性名不能重复
在springMVC的controller中,bean的属性注入,只能注入第一层的属性,不能注入第二层及以下的属性
//public ApiBaseResponse<String> test2(ModelTest model , MdoelMock mock);
INFO c.i.b.h.u.HttpSignCalculator - sign calcuate param:aaa=12314&age=123&bbb=1qza&name=senvon1,qazxs&12345
INFO c.i.b.h.u.HttpConnectionUtils - ready to send post http request:http://localhost:17070/mock/httpMock/test2.do ,params:,body:{"_DEFAULT0":{"aaa":"12314","bbb":"1qza","name":"senvon1,qazxs"},"_DEFAULT1":{"age":123,"name":"senvon1,qazxs"}} , header:{senvon=BC546013CA0F5F7B91DCB634394F78AE}
INFO c.i.h.HttpServiceController - ready to test2 ================com.istock.http.ModelTest@6f75652d[aaa=12314,bbb=1qza,name=senvon1,qazxs],com.istock.http.MdoelMock@27e98006[name=senvon1,qazxs,age=123]springMVC的controller的参数中,不支持各种接口,比如List,Map,Set,但是能支持常见的POJO类的映射注入,注入的方式是将requestMap中对应被注入bean的属性名(只能是第一层属性).
如果出现requestMap中属性名重复,httpService会把重复的属性名打包成一个用","分割的一个属性,比如上述日志中的name属性
test3的日志打印
展示如果参数中出现Map,Set,List的调用方式.
在接口声明中,如果接口方法声明@Consumes(value="application/json"),就代表,当前的请求将会以http+json的方式传输,对应的服务暴露,一定要使用@RequestBody,对参数进行封装,而且只能传入一个参数.
@Consumes(value="application/json")
public ApiBaseResponse<String> test3(Map<String , Object> paramMap);
INFO c.i.b.h.u.HttpSignCalculator - sign calcuate param:age=11&name=s1&value=v1&12345
INFO c.i.b.h.u.HttpConnectionUtils - ready to send post http request:http://localhost:17070/mock/httpMock/test3.do ,params:,body:{"_DEFAULT0":{"name":"senvon","paramMap":{"name":"s1","value":"v1"},"age":11}} , header:{senvon=322AC5E5822DC3B4BEEC99E8E02314E3}
INFO c.i.h.HttpServiceController - ready to test3 ================{name=senvon, paramMap={"name":"s1","value":"v1"}, age=11}test4的日志打印
展示参数中List<Bean>的传输模式.只能传入一个参数.
@Consumes(value="application/json")
public ApiBaseResponse<String> test4(List<ModelTest> modelList);
INFO c.i.b.h.u.HttpSignCalculator - sign calcuate param:aaa=1231111&bbb=abc11111&name=senvon&12345
INFO c.i.b.h.u.HttpConnectionUtils - ready to send post http request:http://localhost:17070/mock/httpMock/test4.do ,params:,body:{"_DEFAULT0":[{"aaa":"123","bbb":"abc","name":"senvon"},{"aaa":"1231111","bbb":"abc11111","name":"senvon"}]} , header:{senvon=451882B8F52839B4F2E92EB75AE72426}
INFO c.i.h.HttpServiceController - ready to test4 ================java.util.ArrayList@4059d06d[size=2]test5的日志打印
数组作为参数的传参方式.
springMVC在注入controller的数组参数时,会自动使用","分割,传入多个参数,但这种传参方式,只能使用原生类型,不支持自定义的bean.
在接口声明中,同样需要注明@QueryParam注明参数名
public ApiBaseResponse<String> test5(@QueryParam("numberList")Integer[] numberList);
INFO c.i.b.h.u.HttpSignCalculator - sign calcuate param:numberList=1,2,111&12345
INFO c.i.b.h.u.HttpConnectionUtils - ready to send post http request:http://localhost:17070/mock/httpMock/test5.do ,params:,body:{"numberList":[1,2,111]} , header:{senvon=86205C8836EE93DD1C1CD51A6379FDBF}
INFO c.i.h.HttpServiceController - ready to test5 ================[Ljava.lang.Integer;@ed1b56[{1,2,111}]客户端的属性说明
| 属性 | 说明 |
|---|---|
| targetClass | 需要被模拟的远程接口,通过spring创建一个虚拟对象,实现该接口,所有的接口都通过http协议把请求发送到服务端 |
| endPoint | 远程服务发布的地址 |
| timeOut | 请求的超时时间,很重要 |
| methodFix | 请求的后缀,可以为空,如果为空,则没有后缀,springMVC可以支持没有后缀的请求 |
| requestHandler | 对请求上下文的拦截处理,httpService提供一个请求签名的方式.一般对于远程访问,远程接口可能会提供部分安全功能,比如签名,加解密,需要通过拦截的手段来进行处理.服务器部分,一般是通过filter或者interceptor进行处理 |
服务端实现
注意,controller实现了接口.
@Controller
@RequestMapping("httpMock")
public class HttpServiceController implements HttpServiceMock{
private Logger logger = LoggerFactory.getLogger(getClass());
@ResponseBody
@RequestMapping("test1")
public ApiBaseResponse<String> test1(String requestId){
logger.info("ready to test1 ================{}" , requestId);
return new ApiBaseResponse<String>(RetStatusEnum.SUCCESS,"ok" , "ok" , "test1 is ok");
}
@ResponseBody
@RequestMapping("test2")
public ApiBaseResponse<String> test2(ModelTest model , MdoelMock mock){
logger.info("ready to test2 ================{},{}" , ToStringBuilder.reflectionToString(model) , ToStringBuilder.reflectionToString(mock));
return new ApiBaseResponse<String>(RetStatusEnum.SUCCESS,"ok" , "ok" , "test2 is ok");
}
@ResponseBody
@RequestMapping("test3")
public ApiBaseResponse<String> test3(@RequestBody Map<String , Object> paramMap){
logger.info("ready to test3 ================{}" , paramMap);
return new ApiBaseResponse<String>(RetStatusEnum.SUCCESS,"ok" , "ok" , "test3 is ok");
}
@ResponseBody
@RequestMapping("test4")
public ApiBaseResponse<String> test4(@RequestBody List<ModelTest> modelList){
logger.info("ready to test4 ================{}" , ToStringBuilder.reflectionToString(modelList));
return new ApiBaseResponse<String>(RetStatusEnum.SUCCESS,"ok" , "ok" , "test4 is ok");
}
@ResponseBody
@RequestMapping("test5")
public ApiBaseResponse<String> test5(Integer[] numberList){
logger.info("ready to test5 ================{}" , ToStringBuilder.reflectionToString(numberList));
return new ApiBaseResponse<String>(RetStatusEnum.SUCCESS,"ok" , "ok" , "test5 is ok");
}
}注意事项
- 对于自定义Bean的类型参数,springMVC只能帮忙注入第一层的属性,而且属性名不能重复,如果重复,spring会认为是数组
- 对于原生类型的参数,一定要使用
@QueryParam或者@FormParam来进行标识,获得参数名 - 对于一定要使用Map,List,Set进行传参的接口,一定要声明
@Consumes(value="application/json"),这种传参方式,使用@RequestBody进行接收,但是只能传递一个参数,不支持多个参数.
