【遠端呼叫框架】如何實現一個簡單的RPC框架(三)優化一:利用動態代理改變使用者服務呼叫方式
【如何實現一個簡單的RPC框架】系列文章:
這篇部落格,在(一)(二)的基礎上,對第一版本實現的服務框架進行改善,不定期更新,每次更新都會增加一個優化的地方。
1、優化一:利用動態代理改變使用者服務呼叫方式
1.1 目的
改變使用者使用LCRPC進行服務呼叫的方式,使得使用者像訪問本地介面一樣訪問遠端服務。
在第一個版本的服務框架開發完成後,如果使用者希望遠端呼叫一個服務的某一個方法,為了得到正確結果,那麼他必須要掌握的資訊包括:方法的名稱、方法的引數型別及個數、方法的返回值,以及服務釋出者提供的二方包和LCRPC依賴。使用時,需要在spring配置檔案中進行類似下面的配置:
<bean id="caculator" class="whu.edu.lcrpc.server.impl.LCRPCConsumerImpl">
<property name="interfaceName" value="whu.edu.lcrpc.service.ICaculator" ></property>
<property name="version" value="0.1"></property>
</bean>
假設我們要呼叫的服務對應的介面為:ICaculator;當我們想要呼叫這個介面的add方法時,需要呼叫LCRPCConsumerImpl提供的ServiceConsumer方法,該方法的簽名為:
public Object serviceConsumer(String methodName, Object[] params)
這意味著,使用者在呼叫服務所有的方法時,都需要使用LCRPCConsumerImpl提供的ServiceConsumer方法,傳入方法的名稱,以及引數列表,並且在得到該函式的結果後顯示將Object物件轉換為該函式的返回型別。例如,希望呼叫這個介面的add方法:
List<Object> params = new ArrayList<>();
MyParamDO myParamDO = new MyParamDO();
myParamDO.setN1(1.0 );
myParamDO.setN2(2.0);
params.add(myParamDO);
MyResultDO result = (MyResultDO) rpcConsumer.serviceConsumer("multiply",params);
這樣的使用方式,看起來有些麻煩。那麼是否可以在使用LCRPC依賴進行遠端服務呼叫時與訪問本地介面沒有區別,使用者在呼叫方法時,直接使用服務釋出者提供的二方包,直接呼叫二方包中介面的方法,例如上面的程式是否可以改成:
MyParamDO myParamDO = new MyParamDO();
myParamDO.setN1(1.0);
myParamDO.setN2(2.0);
MyResultDO result = caculator.add(myParamDO);
caculator為ICaculator型別物件,Spring配置檔案中的配置不變。
其實,使用動態代理的方式完全可以實現上述目的。
1.2 方法
方法:動態代理
關於動態代理的知識讀者可以自行查閱網上諸多資料,也可以閱讀《瘋狂Java講義》第18章對動態代理的介紹。
使用JDK為我們提供的Proxy和InvocationHandler建立動態代理。主要步驟包括:
step 1. 實現介面InvocationHandler,實現方法invoke,執行代理物件所有方法執行時將會替換成執行此invoke方法,因此我們可以將真正的操作在該函式中實現(例如本服務框架中:拼裝請求引數序列化後傳送給服務端,得到結果後解析,即遠端服務呼叫的過程)。
step2. 利用Proxy的new ProxyInstance生成動態代理物件。例如:
InvocationHandler handler = new MyInvocationhandler(...);
Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[]{Foo.calss},hanlder);
此時,呼叫f的所有方法執行的均是handler中的invoke方法。
瞭解了實現動態代理的思路後,我們可以對我們自己編寫的RPC服務框架進行改善了(第一個版本請參考部落格【遠端呼叫框架】如何實現一個簡單的RPC框架(二)優化)。
- step 1. 編寫類MyinvocationHandler,實現介面InvocationHandler,且該實現類需要包括兩個屬性變數:interFaceName(所要代理的介面的全限定名)、version(服務版本號)。實現方法invoke,在該方法中獲取方法的名稱、引數列表,在此基礎上拼裝request請求物件,傳送給服務端,接收響應,反序列化後返回。其實就是複用我們第一個版本中的程式碼。該類程式碼如下:
@Data
public class MyInvocationHandler implements InvocationHandler {
private String interfaceName;//介面的全限定名
private String version;//服務版本號
private IConsumerService consumerService;//初始化客戶端輔助類
public MyInvocationHandler(String interfaceName, String version){
this.interfaceName = interfaceName;
this.version = version;
consumerService = new ConsumerServiceImpl();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法的名稱
String methodName = method.getName();
//方法的返回型別
Class returnType = method.getReturnType();
//若服務唯一標識沒有提供,則丟擲異常
if (interfaceName == null || interfaceName.length() == 0
|| version == null || version.length() == 0)
throw new LCRPCServiceIDIsIllegal();
//step1. 根據服務的唯一標識獲取該服務的ip地址列表
String serviceID = interfaceName + "_" + version;
Set<String> ips = consumerService.getServiceIPsByID(serviceID);
if (ips == null || ips.size() == 0)
throw new LCRPCServiceNotFound();
//step2. 路由,獲取該服務的地址,路由的結果會返回至少一個地址,所以這裡不需要丟擲異常
String serviceAddress = consumerService.getIP(serviceID,methodName,args,ips);
//step3. 根據傳入的引數,拼裝Request物件,這裡一定會返回一個合法的request物件,所以不需要丟擲異常
LCRPCRequestDO requestDO = consumerService.getRequestDO(interfaceName,version,methodName,args);
//step3. 傳入Request物件,序列化並傳入服務端,拿到響應後,反序列化為object物件
Object result = null;
try {
result = consumerService.sendData(serviceAddress,requestDO);
}catch (Exception e){
//在服務呼叫的過程種出現問題
throw new LCRPCRemoteCallException(e.getMessage());
}
if (result == null)throw new LCRPCRemoteCallException(Constant.SERVICEUNKNOWNEXCEPTION);
//step4. 返回object物件
return result;
}
}
- step 2. 我們希望在使用時spring配置檔案中的配置不變,依舊是(把bean的id值變了一下):
<bean id="caculator" class="whu.edu.lcrpc.server.impl.LCRPCConsumerImpl">
<property name="interfaceName" value="whu.edu.lcrpc.service.ICaculator" ></property>
<property name="version" value="0.1"></property>
</bean>
使用者的使用方式如下:
@Resource
ICaculator caculator;
caculator.add(...)
那麼此時我們如何將動態代理物件傳給caculator,並且spring配置檔案中bean的class值配置的是LCRPCCounsumerImpl,如何在spring生成bean的時候,生成的是響應介面的動態代理物件?而後將該動態代理物件傳給caculator,使得使用者可以直接呼叫caculator中的方法,而實際上是呼叫的動態代理中的方法。
Spring的FactoryBean介面,幫我們實現了該要求。當某一個類實現了FactoryBean介面的時候,spring在建立該型別的bean時可以生成其他型別的物件返回。可以參考部落格【Spring:FactoryBean介面】實現FactoryBean介面,Spring在初始化bean時有何不同。利用這一點,我們讓LCRPCConsumerImpl實現FactoryBean介面,並在重寫的getObject方法中生成相應介面的動態代理物件返回。修改後LCRPCConsumerImpl增加程式碼如下:
@Override
public Object getObject() throws Exception {
//返回介面interfaceName的動態代理類
return getProxy();
}
@Override
public Class<?> getObjectType() {
try {
return Class.forName(interfaceName).getClass();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
@Override
public boolean isSingleton() {
return false;
}
private Object getProxy() throws ClassNotFoundException {
Class clz = Class.forName(interfaceName);
return Proxy.newProxyInstance(clz.getClassLoader(),new Class[]{clz},new MyInvocationHandler(interfaceName,version));
}
getProxy方法利用Proxy和我們自己實現的MyInvocationHandler類返回了某個介面的動態代理物件。
至此,我們在LCRPC服務框架部分的改造已經完成了。使用者現在可以在客戶端像呼叫本地介面一樣,訪問某一個遠端服務了。關於具體的使用請參考1.3節內容。
1.3 使用
還是在上一版本客戶端測試程式碼的基礎上,我們還是要呼叫服務釋出者釋出的計算器服務。
- (1)如果使用者希望呼叫介面ICaculator對應的服務,則spring的配置檔案如下:
<bean id="caculator" class="whu.edu.lcrpc.server.impl.LCRPCConsumerImpl">
<property name="interfaceName" value="whu.edu.lcrpc.service.ICaculator" ></property>
<property name="version" value="0.1"></property>
</bean>
class值為LCRPCConsumerImpl,但是spring返回的bean的型別為interfaceName屬性對應介面的動態代理物件。
- (2)ConsumerTest類修改為:
@Resource
ICaculator caculator;
public void add(){
System.out.println("add:" + caculator.add(1,2));
}
public void minus(){
System.out.println("minus:" + caculator.minus(1,2));
}
public void multiply(){
MyParamDO p1 = new MyParamDO();
p1.setN1(1);
p1.setN2(2);
System.out.println("multiply:" + caculator.multiply(p1));
}
public void divide(){
MyParamDO p1 = new MyParamDO();
p1.setN1(1);
p1.setN2(2);
System.out.println("divide:" + caculator.divide(p1));
}
此時可以發現,我們在呼叫遠端服務的時候,完全就是利用服務釋出者提供的二方包,呼叫其中的介面,跟本地呼叫完全沒有差別。
執行主類沒有改變,執行後的結果如下,與第一版本相同。