爲了帶你搞懂 RPC,我們手寫了一個 RPC 框架
作者:PPPHUANG
原文:https://www.jianshu.com/p/2e5b07e2fa97
如今分佈式系統大行其道的年代,RPC 有着舉足輕重的地位。風靡的 Duboo、Thrift、gRpc 等框架各領風騷,深入瞭解 RPC 是新手也是老鳥的必修課。你知道 RPC 的實現原理嗎?想動手實現一個簡單的 RPC 框架嗎?本文將通過一個 RPC 項目帶你尋找答案,大量代碼展示,乾貨滿滿,如果你能再鑽研該項目代碼,相信你能收穫到包括不限於 RPC 原理、Java 基礎(註解、反射、同步器、Future、SPI、動態代理)、Javassist 字節碼增強、服務註冊與發現、Netty 網絡通訊、傳輸協議、序列化、包壓縮、TCP 粘包 \ 拆包、長連接複用、心跳檢測、SpringBoot 自動裝載、服務分組、接口版本、客戶端連接池、負載均衡、異步調用等等重點知識。
RPC 定義
遠程服務調用(Remote procedure call)的概念歷史已久,1981 年就已經被提出,最初的目的就是爲了調用遠程方法像調用本地方法一樣簡單,經歷了四十多年的更新與迭代,RPC 的大體思路已經趨於穩定,如今百家爭鳴的 RPC 協議和框架,諸如 Dubbo (阿里)、Thrift(FaceBook)、gRpc(Google)、brpc (百度)等都在不同側重點去解決最初的目的,有的想極致完美,有的追求極致性能,有的偏向極致簡單。
RPC 原理
讓我們回到 RPC 最初的目的,要想實現調用遠程方法像調用本地方法一樣簡單,至少要解決如下問題:
-
如何獲取可用的遠程服務器
-
如何表示數據
-
如何傳遞數據
-
服務端如何確定並調用目標方法
上述四點問題,都能與現在分佈式系統火熱的術語一一對應,如何獲取可用的遠程服務器(服務註冊與發現)、如何表示數據(序列化與反序列化)、如何傳遞數據(網絡通訊)、服務端如何確定並調用目標方法(調用方法映射)。筆者將通過一個簡單 RPC 項目來解決這些問題。
首先來看 RPC 的整體系統架構圖:
圖中服務端啓動時將自己的服務節點信息註冊到註冊中心,客戶端調用遠程方法時會訂閱註冊中心中的可用服務節點信息,拿到可用服務節點之後遠程調用方法,當註冊中心中的可用服務節點發生變化時會通知客戶端,避免客戶端繼續調用已經失效的節點。那客戶端是如何調用遠程方法的呢,來看一下遠程調用示意圖:
-
客戶端模塊代理所有遠程方法的調用
-
將目標服務、目標方法、調用目標方法的參數等必要信息序列化
-
序列化之後的數據包進一步壓縮,壓縮後的數據包通過網絡通信傳輸到目標服務節點
-
服務節點將接受到的數據包進行解壓
-
解壓後的數據包反序列化成目標服務、目標方法、目標方法的調用參數
-
通過服務端代理調用目標方法獲取結果,結果同樣需要序列化、壓縮然後回傳給客戶端
通過以上描述,相信讀者應該大體上了解了 RPC 是如何工作的,接下來看如何使用代碼具體實現上述的流程。鑑於篇幅筆者會選擇重要或者網絡上介紹相對較少的模塊來講述。
RPC 實現細節
服務註冊與發現
系統選用 Zookeeper 作爲註冊中心, ZooKeeper 將數據保存在內存中,性能很高。 在讀
多寫
少的場景中尤其適用,因爲寫
操作會導致所有的服務器間同步狀態。服務註冊與發現是典型的讀
多寫
少的協調服務場景。 Zookeeper 是一個典型的 CP 系統,在服務選舉或者集羣半數機器宕機時是不可用狀態,相對於服務發現中主流的 AP 系統來說,可用性稍低,但是用於理解 RPC 的實現,也是綽綽有餘。
ZooKeeper 節點
-
持久節點 (PERSISENT):一旦創建,除非主動調用刪除操作,否則一直持久化存儲。
-
臨時節點 (EPHEMERAL):與客戶端會話綁定,客戶端會話失效,這個客戶端所創建的所有臨時節點都會被刪除除。
-
節點順序 (SEQUENTIAL):創建子節點時,如果設置 SEQUENTIAL 屬性,則會自動在節點名後追加一個整形數字,上限是整形的最大值;同一目錄下共享順序,例如(/a0000000001,/b0000000002,/c0000000003,/test0000000004)。
服務註冊
在 ZooKeeper 根節點下根據服務名創建持久節點 /rpc/{serviceName}/service
,將該服務的所有服務節點使用臨時節點創建在 /rpc/{serviceName}/service
目錄下,代碼如下(爲方便展示,後續展示代碼與項目代碼相比都做了刪減):
public void exportService(Service serviceResource) {
String name = serviceResource.getName();
String uri = GSON.toJson(serviceResource);
String servicePath = "rpc/" + name + "/service";
//判斷是否已經註冊過servicePath持久化節點,沒有就註冊
if (!zkClient.exists(servicePath)) {
zkClient.createPersistent(servicePath, true);
}
String uriPath = servicePath + "/" + uri;
//當前節點已被註冊則刪除已註冊節點
if (zkClient.exists(uriPath)) {
zkClient.delete(uriPath);
}
//創建一個新的臨時節點,當該節點宕機會話失效時,該臨時節點會被清理
zkClient.createEphemeral(uriPath);
}
註冊效果如圖,本地啓動兩個服務則 service 下有兩個服務節點信息:
存儲的節點信息包括服務名,服務 IP:PORT ,序列化協議,壓縮協議等。
服務發現
客戶端啓動後,不會立即從註冊中心獲取可用服務節點,而是在調用遠程方法時獲取節點信息(懶加載),並放入本地緩存 MAP 中,供後續調用,當註冊中心通知目錄變化時清空服務所有節點緩存,代碼如下:
public List<Service> getServices(String name) {
Map<String, List<Service>> SERVER_MAP = new ConcurrentHashMap<>();
String servicePath = "rpc/" + name + "/service";
List<String> children = zkClient.getChildren(servicePath);
List<Service> serviceList = Optional.ofNullable(children).orElse(new ArrayList<>()).stream().map(str -> {
String deCh = null;
deCh = URLDecoder.decode(str, StandardCharsets.UTF_8.toString());
return gson.fromJson(deCh, Service.class);
}).collect(Collectors.toList());
SERVER_MAP.put(name, serviceList);
return serviceList;
}
public class ZkChildListenerImpl implements IZkChildListener {
/**
* 監聽子節點的刪除和新增事件
*/
@Override
public void handleChildChange(String parentPath, List<String> childList) throws Exception {
//有變動就清空服務所有節點緩存
String[] arr = parentPath.split("/");
SERVER_MAP.remove(arr[2]);
}
}
PS:美團分佈式 ID 生成系統 Leaf 就使用 Zookeeper 的順序節點來註冊 WorkerID ,臨時節點保存節點 IP:PORT 信息。
Client
客戶端調用本地方法一樣調用遠程方法的完美體驗與 Java 動態代理的強大密不可分。
DefaultRpcBaseProcessor
抽象類實現了 ApplicationListener
, onApplicationEvent
方法在 Spring 項目啓動完畢會收到時間通知,獲取 ApplicationContext
上下文之後開始注入服務 injectService
(有依賴服務)或者啓動服務 startServer
(有服務實現)。
injectService
方法會遍歷 ApplicationContext
上下文中的所有 Bean
, Bean
中是否有屬性使用了 InjectService
註解。有的話生成代理類,注入到 Bean
的屬性中。代碼如下:
public abstract class DefaultRpcBaseProcessor implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//Spring啓動完畢會收到Event
if (Objects.isNull(contextRefreshedEvent.getApplicationContext().getParent())) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
//保存spring上下文 後續使用
Container.setSpringContext(applicationContext);
startServer(applicationContext);
injectService(applicationContext);
}
}
private void injectService(ApplicationContext context) {
String[] names = context.getBeanDefinitionNames();
for (String name : names) {
Object bean = context.getBean(name);
Class<?> clazz = bean.getClass();
if (Objects.isNull(clazz)) {
continue;
}
if (AopUtils.isCglibProxy(bean)) {
//aop增強的類生成cglib類,需要Superclass才能獲取定義的字段
clazz = clazz.getSuperclass();
} else if(AopUtils.isJdkDynamicProxy(bean)) {
//動態代理類,可能也需要
clazz = clazz.getSuperclass();
}
Field[] declaredFields = clazz.getDeclaredFields();
//設置InjectService的代理類
for (Field field : declaredFields) {
InjectService injectService = field.getAnnotation(InjectService.class);
if (injectService == null) {
continue;
}
Class<?> fieldClass = field.getType();
Object object = context.getBean(name);
field.setAccessible(true);
field.set(object, clientProxyFactory.getProxy(fieldClass, injectService.group(), injectService.version()));
ServerDiscoveryCache.SERVER_CLASS_NAMES.add(fieldClass.getName());
}
}
}
protected abstract void startServer(ApplicationContext context);
}
調用 ClientProxyFactory
類的 getProxy
,根據服務接口、服務分組、服務版本、是否異步調用來創建該接口的代理類,對該接口的所有方法都會使用創建的代理類來調用。方法調用的實現細節都在 ClientInvocationHandler
中的 invoke
方法,主要內容是,獲取服務節點信息,選擇調用節點,構建 request 對象,最後調用網絡模塊發送請求。
public class ClientProxyFactory {
public <T> T getProxy(Class<T> clazz, String group, String version, boolean async) {
if (async) {
return (T) asyncObjectCache.computeIfAbsent(clazz.getName() + group + version, clz -> Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ClientInvocationHandler(clazz, group, version, async)));
} else {
return (T) objectCache.computeIfAbsent(clazz.getName() + group + version, clz -> Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ClientInvocationHandler(clazz, group, version, async)));
}
}
private class ClientInvocationHandler implements InvocationHandler {
private Class<?> clazz;
private boolean async;
private String group;
private String version;
public ClientInvocationHandler(Class<?> clazz, String group, String version, boolean async) {
this.clazz = clazz;
this.async = async;
this.group = group;
this.version = version;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//1\. 獲得服務信息
String serviceName = clazz.getName();
List<Service> serviceList = getServiceList(serviceName);
Service service = loadBalance.selectOne(serviceList);
//2\. 構建request對象
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setRequestId(UUID.randomUUID().toString());
rpcRequest.setAsync(async);
rpcRequest.setServiceName(service.getName());
rpcRequest.setMethod(method.getName());
rpcRequest.setGroup(group);
rpcRequest.setVersion(version);
rpcRequest.setParameters(args);
rpcRequest.setParametersTypes(method.getParameterTypes());
//3\. 協議編組
RpcProtocolEnum messageProtocol = RpcProtocolEnum.getProtocol(service.getProtocol());
RpcCompressEnum compresser = RpcCompressEnum.getCompress(service.getCompress());
RpcResponse response = netClient.sendRequest(rpcRequest, service, messageProtocol, compresser);
return response.getReturnValue();
}
}
}
網絡傳輸
客戶端封裝調用請求對象之後需要通過網絡將調用信息發送到服務端,在發送請求對象之前還需要經歷序列化、壓縮兩個階段。
序列化與反序列化
序列化與反序列化的核心作用就是對象的保存與重建,方便客戶端與服務端通過字節流傳遞對象,快速對接交互。
-
序列化就是指把 Java 對象轉換爲字節序列的過程。
-
反序列化就是指把字節序列恢復爲 Java 對象的過程。
Java 序列化的方式有很多,諸如 JDK 自帶的 Serializable
、 Protobuf
、 kryo
等,上述三種筆者自測性能最高的是 Kryo
、其次是 Protobuf
。 Json
也不失爲一種簡單且高效的序列化方法,有很多大道至簡的框架採用。序列化接口比較簡單,讀者可以自行查看實現代碼。
public interface MessageProtocol {
byte[] marshallingRequest(RpcRequest request) throws Exception;
RpcRequest unmarshallingRequest(byte[] data) throws Exception;
byte[] marshallingResponse(RpcResponse response) throws Exception;
RpcResponse unmarshallingResponse(byte[] data) throws Exception;
}
壓縮與解壓
網絡通信的成本很高,爲了減小網絡傳輸數據包的體積,將序列化之後的字節碼壓縮不失爲一種很好的選擇。Gzip 壓縮算法比率在 3 到 10 倍左右,可以大大節省服務器的網絡帶寬,各種流行的 web 服務器也都支持 Gzip 壓縮算法。 Java 接入也比較容易,接入代碼可以查看下方接口的實現。
public interface Compresser {
byte[] compress(byte[] bytes);
byte[] decompress(byte[] bytes);
}
網絡通信
萬事俱備只欠東風。將請求對象序列化成字節碼,並且壓縮體積之後,需要使用網絡將字節碼傳輸到服務器。常用網絡傳輸協議有 HTTP 、 TCP 、 WebSocke t 等。HTTP、WebSocket 是應用層協議,TCP 是傳輸層協議。有些追求簡潔、易用的 RPC 框架也有選擇 HTTP 協議的。TCP 傳輸的高可靠性和極致性能是主流 RPC 框架選擇的最主要原因。談到 Java 生態的通信領域,Netty
的領銜地位短時間內無人能及。選用 Netty 作爲網絡通信模塊, TCP 數據流的粘包、拆包不可避免。
粘包、拆包
TCP 傳輸協議是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。爲了最大化傳輸效率。發送方可能將單個較小數據包合併發送,這種情況就需要接收方來拆包處理數據了。
Netty 提供了 3 種類型的解碼器來處理 TCP 粘包 / 拆包問題:
-
定長消息解碼器:
FixedLengthFrameDecoder
。發送方和接收方規定一個固定的消息長度,不夠用空格等字符補全,這樣接收方每次從接受到的字節流中讀取固定長度的字節即可,長度不夠就保留本次接受的數據,再在下一個字節流中獲取剩下數量的字節數據。 -
分隔符解碼器:
LineBasedFrameDecoder
或DelimiterBasedFrameDecoder
。LineBasedFrameDecoder
是行分隔符解碼器,分隔符爲\n
或\r\n
;DelimiterBasedFrameDecoder
是自定義分隔符解碼器,可以定義一個或多個分隔符。接收端在收到的字節流中查找分隔符,然後返回分隔符之前的數據,沒找到就繼續從下一個字節流中查找。 -
數據長度解碼器:
LengthFieldBasedFrameDecoder
。將發送的消息分爲 header 和 body,header 存儲消息的長度(字節數),body 是發送的消息的內容。同時發送方和接收方要協商好這個 header 的字節數,因爲 int 能表示長度,long 也能表示長度。接收方首先從字節流中讀取前 n(header 的字節數)個字節(header),然後根據長度讀取等量的字節,不夠就從下一個數據流中查找。
不想使用內置的解碼器也可自定義解碼器,自定傳輸協議。
網絡通信這部分內容比較複雜,說來話長,代碼易讀,讀者可先自行閱讀代碼。後續有機會細說此節內容。
Server
客戶端通過網絡傳輸將請求對象序列化、壓縮之後的字節碼傳輸到服務端之後,同樣先通過解壓、反序列化將字節碼重建爲請求對象。有了請求對象之後,就可以進行關鍵的方法調用環節了。
public abstract class RequestBaseHandler {
public RpcResponse handleRequest(RpcRequest request) throws Exception {
//1\. 查找目標服務代理對象
ServiceObject serviceObject = serverRegister.getServiceObject(request.getServiceName() + request.getGroup() + request.getVersion());
RpcResponse response = null;
if (serviceObject == null) {
response = new RpcResponse(RpcStatusEnum.NOT_FOUND);
} else {
try {
//2\. 調用對應的方法
response = invoke(serviceObject, request);
} catch (Exception e) {
response = new RpcResponse(RpcStatusEnum.ERROR);
response.setException(e);
}
}
//響應客戶端
response.setRequestId(request.getRequestId());
response.setAsync(request.isAsync());
return response;
}
/**
* 具體代理調用
* @return RpcResponse
*/
public abstract RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception;
}
上述抽象類 RequestBaseHandler
是調用服務方法的抽象實現 handleRequest
通過請求對象的服務名、服務分組、服務版本在 serverRegister.getServiceObject
獲取代理對象。然後調用 invoke
抽象方法來真正通過代理對象調用方法獲得結果。
-
服務的代理對象怎麼產生的?
-
如何通過代理對象調用方法?
生成服務代理對象
帶着上述問題來看 DefaultRpcBaseProcessor
抽象類:
public abstract class DefaultRpcBaseProcessor implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//Spring啓動完畢會收到Event
if (Objects.isNull(contextRefreshedEvent.getApplicationContext().getParent())) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
Container.setSpringContext(applicationContext);
startServer(applicationContext);
injectService(applicationContext);
}
}
private void injectService(ApplicationContext context) {}
protected abstract void startServer(ApplicationContext context);
}
DefaultRpcBaseProcessor
抽象類也有兩個實現類 DefaultRpcReflectProcessor
和 DefaultRpcJavassistProcessor
,來實現關鍵的生成代理對象的 startServer
方法。
服務接口實現類的 Bean
作爲代理對象
public class DefaultRpcReflectProcessor extends DefaultRpcBaseProcessor {
@Override
protected void startServer(ApplicationContext context) {
Map<String, Object> beans = context.getBeansWithAnnotation(RpcService.class);
//判定
if (beans.size() > 0) {
boolean startServerFlag = true;
for (Object obj : beans.values()) {
Class<?> clazz = obj.getClass();
Class<?>[] interfaces = clazz.getInterfaces();
ServiceObject so = null;
/*
* 如果只實現了一個接口就用接口的className作爲服務名
* 如果該類實現了多個接口,則使用註解裏的value作爲服務名
*/
RpcService service = clazz.getAnnotation(RpcService.class);
if (interfaces.length != 1) {
String value = service.value();
so = new ServiceObject(value, Class.forName(value), obj, service.group(), service.version());
} else {
Class<?> supperClass = interfaces[0];
so = new ServiceObject(supperClass.getName(), supperClass, obj, service.group(), service.version());
}
serverRegister.register(so);
}
}
}
}
DefaultRpcReflectProcessor
中獲取到所有有 RpcService
註解的服務接口實現類 Bean
,然後將該 Bean
作爲服務代理對象註冊到 serverRegister
中供上述的反射調用中使用。
使用 Javassist
生成新的代理對象
public class DefaultRpcJavassistProcessor extends DefaultRpcBaseProcessor {
@Override
protected void startServer(ApplicationContext context) {
Map<String, Object> beans = context.getBeansWithAnnotation(RpcService.class);
//判定
if (beans.size() > 0) {
boolean startServerFlag = true;
for (Map.Entry<String, Object> entry : beans.entrySet()) {
String beanName = entry.getKey();
Object obj = entry.getValue();
Class<?> clazz = obj.getClass();
Class<?>[] interfaces = clazz.getInterfaces();
Method[] declaredMethods = clazz.getDeclaredMethods();
ServiceObject so = null;
/*
* 如果只實現了一個接口就用接口的className作爲服務名
* 如果該類實現了多個接口,則使用註解裏的value作爲服務名
*/
RpcService service = clazz.getAnnotation(RpcService.class);
if (interfaces.length != 1) {
String value = service.value();
/*
* bean實現多個接口時,javassist代理類中生成的方法只按照註解指定的服務類來生成
*/
declaredMethods = Class.forName(value).getDeclaredMethods();
Object proxy = ProxyFactory.makeProxy(value, beanName, declaredMethods);
so = new ServiceObject(value, Class.forName(value), proxy, service.group(), service.version());
} else {
Class<?> supperClass = interfaces[0];
Object proxy = ProxyFactory.makeProxy(supperClass.getName(), beanName, declaredMethods);
so = new ServiceObject(supperClass.getName(), supperClass, proxy, service.group(), service.version());
}
serverRegister.register(so);
}
}
}
}
DefaultRpcJavassistProcessor
與 DefaultRpcReflectProcessor
的差異在於後者直接將服務實現類對象 Bean
作爲服務代理對象,而前者通過
ProxyFactory.makeProxy(value, beanName, declaredMethods)
創建了新的代理對象,將新的代理對象註冊到 serverRegister
中供後續調用調用中使用。該方法通過 Javassist
來生成代理類,代碼冗長,建議閱讀源碼。我來通過下面的代碼演示實現的代理類。
首先我們的服務接口是:
public interface HelloService {
String hello(String name);
}
服務的實現類是:
@RpcService
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String name) {
return "a1";
}
}
那最終新生成的代理類是這樣的:
public class HelloService$proxy1649315143476 {
private static cn.ppphuang.rpcspringstarter.service.HelloService serviceProxy =
((org.springframework.context.ApplicationContext)cn.ppphuang.rpcspringstarter.server.Container.getSpringContext()).getBean("helloServiceImpl");
public cn.ppphuang.rpcspringstarter.common.model.RpcResponse hello(cn.ppphuang.rpcspringstarter.common.model.RpcRequest request) throws java.lang.Exception {
java.lang.Object[] params = request.getParameters();
if(params.length == 1
&& (params[0] == null||params[0].getClass().getSimpleName().equalsIgnoreCase("String"))){
java.lang.String arg0 = null;
arg0 = cn.ppphuang.rpcspringstarter.util.ConvertUtil.convertToString(params[0]);
java.lang.String returnValue = serviceProxy.hello(arg0);
return new cn.ppphuang.rpcspringstarter.common.model.RpcResponse(returnValue);
}
}
public cn.ppphuang.rpcspringstarter.common.model.RpcResponse invoke(cn.ppphuang.rpcspringstarter.common.model.RpcRequest request) throws java.lang.Exception {
String methodName = request.getMethod();
if(methodName.equalsIgnoreCase("hello")){
java.lang.Object returnValue = hello(request);
return returnValue;
}
}
}
清理全限定類名:
public class HelloService$proxy1649315143476 {
private static HelloService serviceProxy = ((ApplicationContext)Container.getSpringContext()).getBean("helloServiceImpl");
public RpcResponse hello(RpcRequest request) throws Exception {
Object[] params = request.getParameters();
if(params.length == 1
&& (params[0] == null|| params[0].getClass().getSimpleName().equalsIgnoreCase("String"))){
String arg0 = null;
arg0 = ConvertUtil.convertToString(params[0]);
String returnValue = serviceProxy.hello(arg0);
return new RpcResponse(returnValue);
}
}
public RpcResponse invoke(RpcRequest request) throws Exception {
String methodName = request.getMethod();
if(methodName.equalsIgnoreCase("hello")){
Object returnValue = hello(request);
return returnValue;
}
}
}
-
代理類
HelloService$proxy1649315143476
中有一個服務接口類型HelloService
的靜態屬性serviceProxy
,值就是通過ApplicationContext
上下文獲取到的服務接口實現類HelloServiceImpl
這個Bean
(SpringContext
已經被提前緩存到Container
類中,讀者可以自行查找代碼瞭解)。 -
public RpcResponse invoke(RpcRequest request) throws Exception
該方法判斷調用的方法名是hello
來調用代理類中的hello
方法。 -
public RpcResponse hello(RpcRequest request) throws Exception
該方法通過調用serviceProxy.hello()
的方法獲取結果。
public interface InvokeProxy {
/**
* invoke調用服務接口
*/
RpcResponse invoke(RpcRequest rpcRequest) throws Exception;
}
HelloService$proxy1649315143476
類實現 InvokeProxy
接口(ProxyFactory.makeProxy
代碼中有體現)。InvokeProxy
接口只有一個 invoke
方法。到這裏就能理解通過調用代理對象的 invoke
方法就能間接調用到服務接口實現類 HelloServiceImpl
的對應方法了。
調用代理對象方法
理清代理對象的生成之後,開始調用代理對象的方法。
上文中寫到的抽象類 RequestBaseHandler
有兩個實現類 RequestJavassistHandler
和 RequestReflectHandler
。
Java 反射調用
先看 RequestReflectHandler
:
public class RequestReflectHandler extends RequestBaseHandler {
@Override
public RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception {
Method method = serviceObject.getClazz().getMethod(request.getMethod(), request.getParametersTypes());
Object value = method.invoke(serviceObject.getObj(), request.getParameters());
RpcResponse response = new RpcResponse(RpcStatusEnum.SUCCESS);
response.setReturnValue(value);
return response;
}
}
Object value = method.invoke(serviceObject.getObj(), request.getParameters());
這行代碼都很熟悉,用 Java 框架中最常見的反射來調用代理類中的方法,大部分 RPC 框架也都是這麼來實現的。
通過 Javassists 生成的代理對象 invoke
方法調用
接着看 RequestJavassistHandler
:
public class RequestJavassistHandler extends RequestBaseHandler {
@Override
public RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception {
InvokeProxy invokeProxy = (InvokeProxy) serviceObject.getObj();
return invokeProxy.invoke(request);
}
}
直接將代理對象轉爲 InvokeProxy
,調用 InvokeProxy.invoke()
方法獲得返回值,如果這裏不能理解,回頭再看一下使用 Javassist
生成新的代理對象這個小節吧。
調用代理對象的方法獲取到結果,仍要通過序列化、壓縮後,將字節流數據包通過網絡傳輸到客戶端,客戶端拿到響應的結果再解壓,反序列化得到結果對象。
Javassist
Javassist
是一個開源的分析、編輯和創建 Java 字節碼的類庫。是由東京工業大學的數學和計算機科學系的 Shigeru Chiba(千葉滋)
所創建的。簡單來說就是用源碼級別的 api 去修改字節碼。Duboo
、MyBatis
也都使用了 Javassist
。Duboo 作者也選擇Javassist
作爲 Duboo 的代理工具,可以點擊這裏查看 Duboo 作者也選擇 Javassist
的原因。
Javassist
還能和諧(pojie)Java 編寫的商業軟件,例如抓包工具 Charles
。代碼在這裏,供交流學習。
在使用 Javassist
有踩到如下坑,供大家參考:
-
Javassist
是運行時,沒有JDK
靜態編譯過程,JDK
的很多語法糖都是在靜態編譯過程中處理的,所以需要自行編碼處理,例如自動拆裝箱。int i = 1; Integer ii = i; //javassist 錯誤 JDK會自動裝箱,javassist需要自行編碼處理 int i = 1; Integer ii = new Integer(i); //javassist 正確
-
自定義的類需要使用類的完全限定名,這也是爲什麼生成的代理類中類都是完全限定名。
選擇哪種代理方式
可以通過配置文件 application.properties
修改 hp.rpc.server-proxy-type
的值來選擇代理模式。
性能測試,機器 Macbook Pro M1 8C 16G, 代碼如下:
@Autowired
ClientProxyFactory clientProxyFactory;
@Test
void contextLoads() {
long l1 = System.currentTimeMillis();
HelloService proxy = clientProxyFactory.getProxy(HelloService.class,"group3","version3");
for (int i = 0; i < 1000000; i++) {
String ppphuang = proxy.hello("ppphuang");
}
long l2 = System.currentTimeMillis();
long l3 = l2 - l1;
System.out.println(l3);
}
測試結果(ms):
測試結果差異並不大,Javassist
模式下只是稍快了一點點,幾乎可以忽略不記。與 Duboo 作者博客 6 樓評論的測試結果一致。所以想簡單通用性強用反射模式,也可以通過使用 Javassist
模式來學習更多知識,因爲 Javassist
需要自己兼容很多特殊的狀況,反射調用 JDK 已經幫你兼容完了。
總結
寫到這裏我們瞭解了 RPC 的基本原理、服務註冊與發現、客戶端代理、網絡傳輸、重點介紹了服務端的兩種代理模式,學習 Javassist
如何實現代理。
還有很多東西沒有重點講述甚至沒有提及,例如粘 \ 拆包的處理、自定義數據包協議、Javassist
模式下如何實現方法重載、如何解決一個服務接口類有多個實現、如何解決一個實現類實現了多個服務接口、在 SpringBoot
中如何自動裝載、如何開箱即用、怎麼實現異步調用、怎麼擴展序列化、壓縮算法等等… 有興趣的讀者可以在源碼中尋找答案,或者尋找優化項,當然也可以尋找 bug 。如果讀者能理解整個項目的實現,相信你一定會有所收穫。後續有機會也會再寫文章與大家交流學習。因筆者水平有限,不完善的地方請大家斧正。感謝各位的閱讀,謝謝。
項目地址
項目地址:https://github.com/ppphuang/rpc-spring-starter
測試 DEMO:https://github.com/ppphuang/rpc-spring-starter-demo
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/k7rdAzSAZbbyT6m2lobWWA