原創 2017年03月29日 09:43:08spring
【如何實現一個簡單的RPC框架】系列文章:框架
【遠程調用框架】如何實現一個簡單的RPC框架(一)想法與設計
【遠程調用框架】如何實現一個簡單的RPC框架(二)實現與使用
【遠程調用框架】如何實現一個簡單的RPC框架(三)優化一:利用動態代理改變用戶服務調用方式
【遠程調用框架】如何實現一個簡單的RPC框架(四)優化二:改變底層通訊框架
【遠程調用框架】如何實現一個簡單的RPC框架(五)優化三:軟負載中心設計與實現
第一個優化以及第二個優化修改後的工程代碼可下載資源 如何實現一個簡單的RPC框架ide
這篇博客,在(一)(二)的基礎上,對初版本實現的服務框架進行改善,不按期更新,每次更新都會增長一個優化的地方。函數
改變用戶使用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方法: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);
這樣的使用方式,看起來有些麻煩。那麼是否能夠在使用LCRPC依賴進行遠程服務調用時與訪問本地接口沒有區別,用戶在調用方法時,直接使用服務發佈者提供的二方包,直接調用二方包中接口的方法,例如上面的程序是否能夠改爲:spa
MyParamDO myParamDO = new MyParamDO(); myParamDO.setN1(1.0); myParamDO.setN2(2.0); MyResultDO result = caculator.add(myParamDO);
caculator爲ICaculator類型對象,Spring配置文件中的配置不變。.net
其實,使用動態代理的方式徹底能夠實現上述目的。設計
方法:動態代理
關於動態代理的知識讀者能夠自行查閱網上諸多資料,也能夠閱讀《瘋狂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框架(二)優化)。
@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; } }
<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節內容。
仍是在上一版本客戶端測試代碼的基礎上,咱們仍是要調用服務發佈者發佈的計算器服務。
<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屬性對應接口的動態代理對象。
@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)); }
此時能夠發現,咱們在調用遠程服務的時候,徹底就是利用服務發佈者提供的二方包,調用其中的接口,跟本地調用徹底沒有差異。
執行主類沒有改變,運行後的結果以下,與初版本相同。