【遠程調用框架】如何實現一個簡單的RPC框架(三)優化一:利用動態代理改變用戶服務調用方式

【遠程調用框架】如何實現一個簡單的RPC框架(三)優化一:利用動態代理改變用戶服務調用方式

原創 2017年03月29日 09:43:08spring

  • 514

【如何實現一個簡單的RPC框架】系列文章:框架

【遠程調用框架】如何實現一個簡單的RPC框架(一)想法與設計 
【遠程調用框架】如何實現一個簡單的RPC框架(二)實現與使用 
【遠程調用框架】如何實現一個簡單的RPC框架(三)優化一:利用動態代理改變用戶服務調用方式 
【遠程調用框架】如何實現一個簡單的RPC框架(四)優化二:改變底層通訊框架 
【遠程調用框架】如何實現一個簡單的RPC框架(五)優化三:軟負載中心設計與實現 
第一個優化以及第二個優化修改後的工程代碼可下載資源 如何實現一個簡單的RPC框架ide

這篇博客,在(一)(二)的基礎上,對初版本實現的服務框架進行改善,不按期更新,每次更新都會增長一個優化的地方。函數

 

 

一、優化一:利用動態代理改變用戶服務調用方式

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>
  • 1
  • 2
  • 3
  • 4

假設咱們要調用的服務對應的接口爲:ICaculator;當咱們想要調用這個接口的add方法時,須要調用LCRPCConsumerImpl提供的ServiceConsumer方法,該方法的簽名爲:優化

public Object serviceConsumer(String methodName, Object[] params)
  • 1

這意味着,用戶在調用服務全部的方法時,都須要使用LCRPCConsumerImpl提供的ServiceConsumer方法,傳入方法的名稱,以及參數列表,而且在獲得該函數的結果後顯示將Object對象轉換爲該函數的返回類型。例如,但願調用這個接口的add方法:this

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這樣的使用方式,看起來有些麻煩。那麼是否能夠在使用LCRPC依賴進行遠程服務調用時與訪問本地接口沒有區別,用戶在調用方法時,直接使用服務發佈者提供的二方包,直接調用二方包中接口的方法,例如上面的程序是否能夠改爲:spa

MyParamDO myParamDO = new MyParamDO();
myParamDO.setN1(1.0);
myParamDO.setN2(2.0);
MyResultDO result = caculator.add(myParamDO);
  • 1
  • 2
  • 3
  • 4

caculator爲ICaculator類型對象,Spring配置文件中的配置不變。.net

其實,使用動態代理的方式徹底能夠實現上述目的。設計

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);
  • 1
  • 2

此時,調用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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 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>
  • 1
  • 2
  • 3
  • 4

用戶的使用方式以下:

@Resource
ICaculator caculator;
caculator.add(...)
  • 1
  • 2
  • 3

那麼此時咱們如何將動態代理對象傳給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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

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>
  • 1
  • 2
  • 3
  • 4

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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

此時能夠發現,咱們在調用遠程服務的時候,徹底就是利用服務發佈者提供的二方包,調用其中的接口,跟本地調用徹底沒有差異。 
執行主類沒有改變,運行後的結果以下,與初版本相同。

這裏寫圖片描述

相關文章
相關標籤/搜索