該文章基於《Spring源碼深度解析》撰寫,感謝郝佳老師的奉獻
RMI的實際作用,是通過暴露對應方法的URL,從而實現高解耦。
RMI服務的流程是通過服務的發佈和服務的調用組成,小Demo如下:
其工程架構如下
其對應文件代碼如下
/*HelloRMIService*/
package RMIService.Impl;
public class HelloRMIService implements RMIService.HelloRMIService{
@Override
public int getAdd(int a, int b) {
return a+b;
}
}
/*RMIService*/
package RMIService;
public interface HelloRMIService {
public int getAdd(int a,int b);
}
/*RMIService*/
package RMIService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class RMIServer {
public static void main(String[] args) throws InterruptedException {
new ClassPathXmlApplicationContext("server.xml");
}
}
/*RMIClient*/
package RMIClient;
import RMIService.HelloRMIService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class RMIClient {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"Client.xml");
HelloRMIService accountService = (HelloRMIService) ctx
.getBean("mobileAccountService");
Integer result = accountService.getAdd(1,2);
System.out.println(result);
}
}
<!--Client-->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="mobileAccountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
<property name="serviceUrl" value="rmi://localhost:8080/MobileAccountService" />
<property name="serviceInterface"
value="RMIService.HelloRMIService" />
</bean>
</beans>
<!--server.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="serviceExporter" class="org.springframework.remoting.rmi.RmiServiceExporter">
<property name="serviceName" value="MobileAccountService" />
<property name="service" ref="accountService" />
<property name="serviceInterface"
value="RMIService.HelloRMIService" />
<property name="registryPort" value="8080" />
<property name="servicePort" value="8088" />
</bean>
<bean id="accountService" class="RMIService.Impl.HelloRMIService" />
</beans>
需要注意的是,我們需要將HelloRMIService.class文件打包爲jar文件後放入lib中。
現在我們開始進行流程分析:
服務器端實現
org.Springframework.remoting.RMI.RMIServiceExporter是RMI服務發佈的關鍵類,該類定義在server.xml中。Spring中的消息開啓很簡單,通過RMIServer類中的
new ClassPathXmlApplicationContext("server.xml");
開啓服務。RMIServiceExporte的類層次結構如下:
RmiServiceExporter實現了幾個比較敏感的接口:BeanClassLoaderAware(保證實現該接口的Bean的初始化時調用其setBeanClassLoader方法)、DisposableBean(銷燬Bean時調用其destory方法)、InitializingBean(初始化Bean時調用其afterPropertiesSet方法),所以RmiServiceExporter的初始化應該位於BeanClassLoaderAware或者InitializingBean中,實際上初始化方法位於InitializingBean的afterPropertiesSet方法,該方法的處理邏輯爲:
(1)驗證Service
(2)處理用戶自定義的SocketFactory屬性,Spring中提供了4個套接字工廠配置分別是(client/server)+SocketFactory,register+(client/server)+SocketFactory,register+(client/server)+SocketFactory用於當創建registry實例時在RMI主機通過serverSocketFactory創建套接字並等待連接,而在服務器端與RMI主機通信時通過clientSocketFacotry創建套接字
(3)根據配置參數獲取Registry
如果RMI註冊主機與發佈服務的機器是同一臺機器,那麼可以直接調用函數LocateRegistry.createRegistry(…)創建Registry實例。如果並不在同一臺機器上,那麼使用LocateRegistry.getRegistry(registryHost,registryPort,clientSocketFactory)進行遠程獲取,如果已經建立了連接,那麼將直接複用連接,除非alwaysCreateRegistry爲true,如果我們自定義了連接工廠——由於clientSocketFactory和serverSocketFactory只能可同時存在或者同時不存在,所以我們只對clientSocketFactory是否爲空進行判斷,如果不存在自定義的連接工廠——直接進行復用檢測,如果不存在或者alwaysCreateRegistry爲true,那麼直接通過LocateRegistry.createRegistry(…)創建新連接。
(4)構造對外發布的實例當外界通過註冊的服務名調用響應方法時,RMI服務會將請求引入此類來處理,如果配置的service屬性對應的類實現了remote接口但是沒有配置serviceInterface接口,那麼直接使用service作爲處理類,否則使用用RMIInvocationWrapper對RMIServiceExporter進行封裝,通過封裝使客戶端與服務器端達成了一致,並且在創建代理時添加了增強攔截器RemoteInvocationTraceIntercaptor,如果直接通過硬編碼,那麼會很不優雅,通過動態代理使代碼具有高擴展性。正是由於封裝的情況,當調用服務時實際上調用的是RMIInvocationWrappe的invoke方法。
(5)發佈實例
客戶端實現
客戶端的入口類爲RMIProxyFactoryBean,其類層次結構如下所示:
該類實現了幾個比較關鍵的接口,InitailizingBean接口(通過afterPropertiesSet進行邏輯初始化),FactoryBean接口(getBean方法實際上返回的是實現類的getObject方法返回的實例),下面重點介紹InitailizingBean接口接口在初始化時的邏輯:
需要注意的是,afterPropertiesSet實際上的處理邏輯位於RmiClientInterceptor類中的prepare方法中,所以此處對該方法進行邏輯分析
(1)通過代理攔截並獲取stub
獲取stub可以通過兩種方式進行:使用自定義的套接字工廠該方法需要在構造Registry實例時將自定義套接字工廠傳入,並使用lookup來獲取對應stub,直接使用RMI提供的標準方法Naming.lookup(getServiceUrl()),該方法中將會使用registryClientSocketFactory(實現了RMIClientSocketFactory接口)用於控制用於連接的socket的各種參數然後連接RMI服務器
(2)增強器用於遠程連接
由於RmiProxyFactoryBean間接實現了MethodInterceptor,那麼當客戶端調用某個方法時,會首先調用invoke方法進行增強,invoke方法的邏輯如下:
(1)獲取服務器中對應註冊的remote對象,通過序列化傳輸,在通過getstub方法獲取stub時,會首先檢查有沒有stub的緩存
(2)遠程方法調用分爲兩種情況,第一種情況是:獲取的stub是RMIInvocationHandler類型的,這就意味着客戶端和服務端都是通過Spring框架搭建的,處理方式爲將處理方式委託給doInvoke方法,第二種情況是:獲取的stub不是RMIInvocationHandler類型的,那麼這也意味着我們將通過反射的方法激活stub中的遠程調用