1. 程式人生 > >【遠端呼叫框架】如何實現一個簡單的RPC框架(三)優化一:利用動態代理改變使用者服務呼叫方式

【遠端呼叫框架】如何實現一個簡單的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));
}

此時可以發現,我們在呼叫遠端服務的時候,完全就是利用服務釋出者提供的二方包,呼叫其中的介面,跟本地呼叫完全沒有差別。
執行主類沒有改變,執行後的結果如下,與第一版本相同。

這裡寫圖片描述