在日常的业务开发中遇到了两个场景:html
1.因为业务用的rpc框架是thrift,代码也是都是用thrift再写,有一天忽然接到个须要前端要用http访问接口的需求,因而花了几天时间把全部的thrift接口又用Controller封装一层。因为跨语言,且对方不使用thrift,就须要你提供Http接口前端
2.写完thrift为了自测,须要再写个TestController验证代码是否正确,整个流程是否跑通,很是麻烦。node
这两个场景你们遇到的比较多,因此要是能一写完thrift接口就能直接转换为http接口,那样就行了。git
放眼整个互联网中,在互联网快速迭代的大潮下,愈来愈多的公司选择nodejs、django、rails这样的快速脚本框架来开发web端应用,而对于咱们来讲公司选择的后端语言是Java,这就产生了大量的跨语言的调用需求。其实对于thrift来讲是支持不少语言的,可是给每次给其余语言开发都须要开发对应的客户端,而且还有不少rpc框架并非像thrift同样支持这么多语言的,因此如今微服务都推出了service mesh(www.servicemesh.cn/),可是这个依然很新,有须要尝试的其实能够起尝试一下。http、json是自然合适做为跨语言的标准,各类语言都有成熟的类库,因此如何把像thrift这种tcp rpc框架转换成http,对于多语言支持是比较重要的。github
最开始想的是如何把thrift接口映射成RESTful,由于这个更加符合互联网http的标准,可是TCP rpc 对比RESTful有根本的区别,RESTful的核心是资源,而且利用Http协议中的各类方法GET,POST,OPTION等等对资源进行操做,若是想把thrift每一个接口一一映射上,这个难度有点大,毕竟两个产生不出来任何关联,这个时候就须要每一个接口进行配置映射,起成本不亚于我重写一套Controller了,因此RESTful这个方案基本被否决了。web
JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议。它容许运行在基于socket,http等诸多不一样消息传输环境的同一进程中。spring
JSONRPC本质上也是个RPC,定位和thrfit相似,不须要进行过多的协议映射。因此咱们选择了使用JSONRPC,进行Http的转换。django
发送一个请求对象至服务端表明一个rpc调用, 一个请求对象包含下列成员:json
jsonrpc后端
指定JSON-RPC协议版本的字符串,必须准确写为“2.0”
method
包含所要调用方法名称的字符串,以rpc开头的方法名,用英文句号(U+002E or ASCII 46)链接的为预留给rpc内部的方法名及扩展名,且不能在其余地方使用。
params
调用方法所须要的结构化参数值,该成员参数能够被省略。
id
已创建客户端的惟一标识id,值必须包含一个字符串、数值或NULL空值。若是不包含该成员则被认定为是一个通知。该值通常不为NULL[1],若为数值则不该该包含小数[2]。
服务端必须回答相同的值若是包含在响应对象。 这个成员用来两个对象之间的关联上下文。
[1] 在请求对象中不建议使用NULL做为id值,由于该规范将使用空值认定为未知id的请求。另外,因为JSON-RPC 1.0 的通知使用了空值,这可能引发处理上的混淆。
[2] 使用小数是不肯定性的,由于许多十进制小数不能精准的表达为二进制小数。
没有包含“id”成员的请求对象为通知, 做为通知的请求对象代表客户端对相应的响应对象并不感兴趣,自己也没有响应对象须要返回给客户端。服务端必须不回复一个通知,包含那些批量请求中的。
因为通知没有返回的响应对象,因此通知不肯定是否被定义。一样,客户端不会意识到任何错误(例如参数缺省,内部错误)。
rpc调用若是存在参数则必须为基本类型或结构化类型的参数值,要么为索引数组,要么为关联数组对象。
索引:参数必须为数组,并包含与服务端预期顺序一致的参数值。
关联名称:参数必须为对象,并包含与服务端相匹配的参数成员名称。没有在预期中的成员名称可能会引发错误。名称必须彻底匹配,包括方法的预期参数名以及大小写。
当发起一个rpc调用时,除通知以外,服务端都必须回复响应。响应表示为一个JSON对象,使用如下成员:
jsonrpc
指定JSON-RPC协议版本的字符串,必须准确写为“2.0”
result
该成员在成功时必须包含。
当调用方法引发错误时必须不包含该成员。
服务端中的被调用方法决定了该成员的值。
error
该成员在失败是必须包含。
当没有引发错误的时必须不包含该成员。
该成员参数值必须为5.1中定义的对象。
id
该成员必须包含。
该成员值必须于请求对象中的id成员值一致。
若在检查请求对象id时错误(例如参数错误或无效请求),则该值必须为空值。
响应对象必须包含result或error成员,但两个成员必须不能同时包含。
当一个rpc调用遇到错误时,返回的响应对象必须包含错误成员参数,而且为带有下列成员参数的对象:
code
使用数值表示该异常的错误类型。 必须为整数。
message
对该错误的简单描述字符串。 该描述应尽可能限定在简短的一句话。
data
包含关于错误附加信息的基本类型或结构化类型。该成员可忽略。 该成员值由服务端定义(例如详细的错误信息,嵌套的错误等)。
jsonRpc4j是一款用Java语言实现的JSONRPC的框架,使用JackSon进行JSON解析。他的github地址为:github.com/briandilley…
在jsonRpc4j中他能够处理HTTP Server (HttpServletRequest \ HttpServletResponse),因此可以帮助咱们很快的构建httpserver,使用JsonRpc4j很简单:
ObjectMapper mapper = new ObjectMapper();
JsonRpcServer skeleton = new JsonRpcServer(mapper, new DemoService(), (Class<?>) service.getClass());
skeleton.handle(req, resp);
复制代码
首先建立一个ObjectMapper,用于JSON的转换的,而后 把须要变成Server的Service放进JsonRpcServer,最后执行这个请求。
对于thrift到http是利用Serlvet加上jsonRpc4j完成关系的映射,以下图所示:
http中关键在于http URL如何制定,这里URL为了简单快速明了,用如下规则:
POST: servlet-url-pattern + thriftServiceInfaceName
首先全部thrift方法公共的路径在Servlet中制定,全部/thrift/*的URL都走ThriftSerlvet
<servlet>
<servlet-name>thriftSerlvet</servlet-name>
<servlet-class>com.thrift.ThriftSerlvet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>thriftSerlvet</servlet-name>
<url-pattern>/thrift/*</url-pattern>
</servlet-mapping>
复制代码
咱们有以下一个thrift
public class CustomerThriftServiceImpl implements customerService.Iface{
@Override
public QueryCustomerResp queryCustomer(QueryRuleReq queryReq) throws TException {
QueryCustomerResp result = new QueryCustomerResp();
try {
result.setCustomr(new Customer());
result.setStatus(ThriftRespStatusHelper.OK);
} catch (Exception e) {
LOGGER.error("查询出现错误{}", e);
result.setStatus(ThriftRespStatusHelper.failure("查询失败"));
}
return result;
}
}
复制代码
因此咱们的URL以下/thrift/customerService
咱们全部的thrift的请求都会通过这个serlvet,而后经过其作jsonRpcServer的路由分发代码以下:
public class ThriftSerlvet extends HttpServlet {
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
private final Map<String, JsonRpcServer> rpcServerMap = new ConcurrentHashMap<>();
private Logger LOGGER = LoggerFactory.getLogger(ThriftSerlvet.class);
public static final String JSON_FILTER_ID = "thriftPropFilter";
@Override
public void init() throws ServletException {
super.init();
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
Map<String, ThriftServerPublisher> publisherMap = rootContext.getBeansOfType(ThriftServerPublisher.class);
if (publisherMap == null || publisherMap.size() == 0) {
return;
}
for (ThriftServerPublisher serverPublisher : publisherMap.values()) {
try {
Field serviceImplField = serverPublisher.getClass().getDeclaredField("serviceImpl");
serviceImplField.setAccessible(true);
Object serveiceImpl = serviceImplField.get(serverPublisher);
addJsonRpcServer(serveiceImpl, serverPublisher.getServiceSimpleName());
} catch (Exception e) {
LOGGER.error("this serverPublisher:{}, get the filed:{} has error", serverPublisher, "serviceImpl", e);
}
}
}
private void addJsonRpcServer(Object serveiceImpl, String serviceSimpleName) {
serviceSimpleName = serviceSimpleName.replaceFirst(String.valueOf(serviceSimpleName.charAt(0)), String.valueOf(serviceSimpleName.charAt(0)).toLowerCase());
LOGGER.info("serverPubliser");
ObjectMapper mapper = new ObjectMapper();
SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
simpleFilterProvider.addFilter(JSON_FILTER_ID, new ThriftPropertiesFilter());
mapper.setFilterProvider(simpleFilterProvider);
mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
@Override
public Object findFilterId(Annotated a) {
return JSON_FILTER_ID;
}
});
JsonRpcServer rpcServer = new JsonRpcServer(mapper, serveiceImpl, serveiceImpl.getClass().getSuperclass());
rpcServer.setInterceptorList(Arrays.asList(new ThriftJsonInterceptor()));
rpcServerMap.put(serviceSimpleName, rpcServer);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
resp.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, "POST");
resp.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, "*");
if (req.getMethod().equalsIgnoreCase("OPTIONS")) {
resp.sendError(200);
} else if (req.getMethod().equalsIgnoreCase("POST")) {
String uri = req.getRequestURI();
String path = req.getServletPath();
String serviceName = uri.substring(path.length(), uri.length()).replaceAll("/", "");
JsonRpcServer rpcServer = rpcServerMap.get(serviceName);
if (rpcServer == null) {
resp.sendError(404);
return;
}
rpcServer.handle(req, resp);
} else {
//方法不被容许
resp.sendError(405);
}
}
}
复制代码
执行过程以下:
1.init:初始化的时候咱们须要把咱们全部的thriftService的Bean从Spring容器中拿出来,而后对每一个Service构建一个不一样的JsonRpcServer,放进Map中等待service方法路由。
这里初始化有几点注意:
ObjectMapper咱们对于输出过滤了以set开头的由于jackSon转换thrift的时候会把thrift本身生成的文件给转换出来。
这里咱们在spring的配置文件中要配置
<aop:aspectj-autoproxy proxy-target-class="true"/>复制代码
显示的要使用cglib,若是不指定这个默认是Jdk的代理,jdk代理的话默认就拿不到本身原本的类了,这里必需要使用cglib代理,这样经过getSuperClass便可得到本身原本的Class。
2.service就比较简单了,咱们先加了容许跨域,而后指定只有POST方法才能访问。
对于这个开源项目并无直接用他而是对他进行了修改,为何会须要进行修改呢?
咱们简单看看下面这个方法
public Person sayHello(Person person, Type type);复制代码
若是咱们想用调用这个服务的话须要传入的json为:
{"jsonrpc": "2.0", "method": "sayHello", "params": \[{"age":"12","name":"lizhao"},{"type":1}\], "id": 1}复制代码
上面这个json,params参数传入的是数组,其实咱们更但愿传的是下面这样,由于对于这种参数须要用名字指定,才能更加可读,减小出错的几率:
{"jsonrpc": "2.0", "method": "sayHello", "params": {"person":{"age":"12","name":"lizhao"},"type":{"type":1}}, "id": 1}复制代码
可是这样传的话会报出找不到方法,jsonrpc4j官方的作法是用注解,将方法修改为:
public Person sayHello(@JsonRpcParam("person")Person person, @JsonRpcParam("type")Type type);复制代码
可是用过thrift的同窗都知道,thrift的不少代码都是根据IDL生成的,这样会致使一个问题,不能使用注解,由于一旦用了注解以后下次生成会直接覆盖。因此这里咱们必需要使用传入的参数的名字才行,具体修改的实现以下:
private List<JsonRpcParam> getAnnotatedParameterNames(Method method) {
List<JsonRpcParam> parameterNames = new ArrayList<>();
List<Parameter> parameters = ReflectionUtil.getParameters(method);
Iterator<Parameter> parameterIterator = parameters.iterator();
List<String> parameterLocalNames = ReflectionUtil.getParameterLocalNames(method);
Iterator<String> parameterLocalNameIterator = parameterLocalNames.iterator();
while (parameterIterator.hasNext() && parameterLocalNameIterator.hasNext()) {
parameterNames.add(getJsonRpcParamType(parameterIterator.next(), parameterLocalNameIterator.next()));
}
return parameterNames;
}
public static List<String> getParameterLocalNames(Method method) {
List<String> parameterNames = new ArrayList<>();
Collections.addAll(parameterNames, PARAMETER\_NAME\_DISCOVERER.getParameterNames(method));
return Collections.unmodifiableList(parameterNames);
}
复制代码
这里主要是使用spring中的ParameterNameDiscoverer经过字节码获取参数名字,这样咱们就不须要用注解便可使用传参数名字的方式。
本次主要介绍了如何从thrfit转换为http,还有更多的细节,鉴权,分布式追踪系统埋点等等须要补充,这种方法实现http可能不是最好的,我以为最好的仍是要实现rest,毕竟rest才是互联网系统调用所承认的,可是经过这种方式了解了如何从一个协议转换成另一个协议,补充了本身在协议转换这方面的一些空白吧。