背景
在平常的業務開發中遇到了兩個場景:
1.由於業務用的rpc框架是thrift,程式碼也是都是用thrift再寫,有一天突然接到個需要前端要用http訪問介面的需求,於是花了幾天時間把所有的thrift介面又用Controller封裝一層。由於跨語言,且對方不使用thrift,就需要你提供Http介面
2.寫完thrift為了自測,需要再寫個TestController驗證程式碼是否正確,整個流程是否跑通,非常麻煩。
這兩個場景大家遇到的比較多,所以要是能一寫完thrift介面就能直接轉換為http介面,那樣就好了。
放眼整個網際網路中,在網際網路快速迭代的大潮下,越來越多的公司選擇nodejs、django、rails這樣的快速指令碼框架來開發web端應用,而對於我們來說公司選擇的後端語言是Java,這就產生了大量的跨語言的呼叫需求。其實對於thrift來說是支援很多語言的,但是給每次給其他語言開發都需要開發對應的客戶端,並且還有很多rpc框架並不是像thrift一樣支援這麼多語言的,所以現在微服務都推出了service mesh(www.servicemesh.cn/),但是這個依然很新,有需要嘗試的其實可以起嘗試一下。http、json是天然合適作為跨語言的標準,各種語言都有成熟的類庫,所以如何把像thrift這種tcp rpc框架轉換成http,對於多語言支援是比較重要的。
RESTful or JSONRPC
RESTful
最開始想的是如何把thrift介面對映成RESTful,因為這個更加符合網際網路http的標準,但是TCP rpc 對比RESTful有根本的區別,RESTful的核心是資源,並且利用Http協議中的各種方法GET,POST,OPTION等等對資源進行操作,如果想把thrift每個介面一一對映上,這個難度有點大,畢竟兩個產生不出來任何關聯,這個時候就需要每個介面進行配置對映,起成本不亞於我重寫一套Controller了,所以RESTful這個方案基本被否決了。
JSONRPC
JSON-RPC是一個無狀態且輕量級的遠端過程呼叫(RPC)協議。它允許執行在基於socket,http等諸多不同訊息傳輸環境的同一程式中。
JSONRPC本質上也是個RPC,定位和thrfit類似,不需要進行過多的協議對映。所以我們選擇了使用JSONRPC,進行Http的轉換。
JSONRPC請求物件
傳送一個請求物件至服務端代表一個rpc呼叫, 一個請求物件包含下列成員:
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
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
對於thrift到http是利用Serlvet加上jsonRpc4j完成關係的對映,如下圖所示:
HTTP URL
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
ThrifSerlvet
我們所有的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方法才能訪問。
JsonRpc4j的修改
對於這個開源專案並沒有直接用他而是對他進行了修改,為什麼會需要進行修改呢?
我們簡單看看下面這個方法
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通過位元組碼獲取引數名字,這樣我們就不需要用註解即可使用傳引數名字的方式。
Swagger
總結
本次主要介紹瞭如何從thrfit轉換為http,還有更多的細節,鑑權,分散式追蹤系統埋點等等需要補充,這種方法實現http可能不是最好的,我覺得最好的還是要實現rest,畢竟rest才是網際網路系統呼叫所認可的,但是通過這種方式瞭解瞭如何從一個協議轉換成另外一個協議,補充了自己在協議轉換這方面的一些空白吧。