Java中的RMI使用方法詳解
1、概述
1.1、簡介
RMI 是 Java 提供的一個完善的簡單易用的遠(yuǎn)程方法調(diào)用框架,采用客戶/服務(wù)器通信方式,在服務(wù)器上部署了提供各種服務(wù)的遠(yuǎn)程對象,客戶端請求訪問服務(wù)器上遠(yuǎn)程對象的方法,它要求客戶端與服務(wù)器端都是 Java 程序。
RMI 框架采用代理來負(fù)責(zé)客戶與遠(yuǎn)程對象之間通過 Socket 進(jìn)行通信的細(xì)節(jié)。RMI 框架為遠(yuǎn)程對象分別生成了客戶端代理和服務(wù)器端代理。位于客戶端的代理必被稱為存根(Stub),位于服務(wù)器端的代理類被稱為骨架(Skeleton)。
1.2、原理
當(dāng)客戶端調(diào)用遠(yuǎn)程對象的一個方法時,實(shí)際上是調(diào)用本地存根對象的相應(yīng)方法。存根對象與遠(yuǎn)程對象具有同樣的接口。存根采用一種與平臺無關(guān)的編碼方式,把方法的參數(shù)編碼為字節(jié)序列,這個編碼過程被稱為參數(shù)編組。RMI 主要采用Java 序列化機(jī)制進(jìn)行參數(shù)編組。
存根把以下請求信息發(fā)送給服務(wù)器:
- 被訪問的遠(yuǎn)程對象的名字
- 被調(diào)用的方法的描述
- 編組后的參數(shù)的字節(jié)序列
服務(wù)器端接收到客戶端的請求信息,然后由相應(yīng)的骨架對象來處理這一請求信息,骨架對象執(zhí)行以下操作:
- 反編組參數(shù),即把參數(shù)的字節(jié)序列反編碼為參數(shù)
- 定位要訪問的遠(yuǎn)程對象
- 調(diào)用遠(yuǎn)程對象的相應(yīng)方法
- 獲取方法調(diào)用產(chǎn)生的返回值或者異常,然后對它進(jìn)行編組
- 把編組后的返回值或者異常發(fā)送給客戶
客戶端的存根接收到服務(wù)器發(fā)送過來的編組后的返回值或者異常,再對它進(jìn)行反編組,就得到調(diào)用遠(yuǎn)程方法的返回結(jié)果
JDK5.0 之后,RMI 框架會在運(yùn)行時自動為運(yùn)程對象生成動態(tài)代理類(包括存根和骨架類),從而更徹底地封裝了 RMI 框架的實(shí)現(xiàn)細(xì)節(jié),簡化了 RMI 框架的使用方式。
RMI交互圖

Stub和Skeleton通信過程

方法調(diào)用從客戶對象-經(jīng)-存根(stub)、遠(yuǎn)程引用層(Remote Reference Layer)和傳輸層(Transport Layer)向下,傳遞給主機(jī),然后再次經(jīng)傳輸層,向上穿過遠(yuǎn)程調(diào)用層和骨干網(wǎng)(Skeleton),到達(dá)服務(wù)器對象。
- 存根:扮演著遠(yuǎn)程服務(wù)器對象的代理的角色,使該對象可被客戶激活。
- 遠(yuǎn)程調(diào)用層:處理語義、管理單一或多重對象的通信,決定調(diào)用是應(yīng)發(fā)往一個服務(wù)器還是多個。
- 傳輸層:管理實(shí)際的連接,并且追蹤可以接受方法調(diào)用的遠(yuǎn)程對象。
- 骨干網(wǎng):完成對服務(wù)器對象實(shí)際的方法調(diào)用,并獲取返回值。返回值向下經(jīng)遠(yuǎn)程引用層、服務(wù)器端的傳輸層傳遞回客戶端,再向上經(jīng)傳輸層和遠(yuǎn)程調(diào)用層返回。最后,存根獲得返回值。
1.3、組成
RMI由3個部分構(gòu)成:
第一個是rmiregistry(JDK提供的一個可以獨(dú)立運(yùn)行的程序,在bin目錄下)
第二個是server端的程序,對外提供遠(yuǎn)程對象
第三個是client端的程序,想要調(diào)用遠(yuǎn)程對象的方法。
首先,先啟動rmiregistry服務(wù),啟動時可以指定服務(wù)監(jiān)聽的端口,也可以使用默認(rèn)的端口(1099)。其次,server端在本地先實(shí)例化一個提供服務(wù)的實(shí)現(xiàn)類,然后通過RMI提供的Naming/Context/Registry(下面實(shí)例用的Registry)等類的bind或rebind方法將剛才實(shí)例化好的實(shí)現(xiàn)類注冊到rmiregistry上并對外暴露一個名稱。最后,client端通過本地的接口和一個已知的名稱(即rmiregistry暴露出的名稱)再使用RMI提供的Naming/Context/Registry等類的lookup方法從RMIService那拿到實(shí)現(xiàn)類。這樣雖然本地沒有這個類的實(shí)現(xiàn)類,但所有的方法都在接口里了,便可以實(shí)現(xiàn)遠(yuǎn)程調(diào)用對象的方法了。
1.4、數(shù)據(jù)傳遞
Java程序中引用類型(不包括基本類型)的參數(shù)傳遞是按引用傳遞的,對于在同一個虛擬機(jī)中的傳遞時是沒有問題的,因?yàn)榈膮?shù)的引用對應(yīng)的是同一個內(nèi)存空間,在分布式系統(tǒng)中,由于對象不存在于同一個內(nèi)存空間,虛擬機(jī)A的對象引用對于虛擬機(jī)B沒有任何意義,那么怎么解決這個問題呢?
第一種:
將引用傳遞更改為值傳遞,也就是將對象序列化為字節(jié),然后使用該字節(jié)的副本在客戶端和服務(wù)器之間傳遞,而且一個虛擬機(jī)中對該值的修改不會影響到其他主機(jī)中的數(shù)據(jù);但是對象的序列化也有一個問題,就是對象的嵌套引用就會造成序列化的嵌套,這必然會導(dǎo)致數(shù)據(jù)量的激增,因此我們需要有選擇進(jìn)行序列化。
在Java中一個對象如果能夠被序列化,需要滿足下面兩個條件之一:
–是Java的基本類型;
–實(shí)現(xiàn)java.io.Serializable接口(String類即實(shí)現(xiàn)了該接口);
對于容器類,如果其中的對象是可以序列化的,那么該容器也是可以序列化的;
可序列化的子類也是可以序列化的;
第二種:
使用引用傳遞,每當(dāng)遠(yuǎn)程主機(jī)調(diào)用本地主機(jī)方法時,該調(diào)用還要通過本地主機(jī)查詢該引用對應(yīng)的對象,在任何一臺機(jī)器上的改變都會影響原始主機(jī)上的數(shù)據(jù),因?yàn)檫@個對象是共享的;
RMI中的參數(shù)傳遞和結(jié)果返回可以使用的三種機(jī)制(取決于數(shù)據(jù)類型):
簡單類型:按值傳遞,直接傳遞數(shù)據(jù)拷貝;?
遠(yuǎn)程對象引用(實(shí)現(xiàn)了Remote接口):以遠(yuǎn)程對象的引用傳遞;?
遠(yuǎn)程對象引用(未實(shí)現(xiàn)Remote接口):按值傳遞,通過序列化對象傳遞副本,本身不允許序列化的對象不允許傳遞給遠(yuǎn)程方法;
在調(diào)用遠(yuǎn)程對象的方法之前需要一個遠(yuǎn)程對象的引用,如何獲得這個遠(yuǎn)程對象的引用在RMI中是一個關(guān)鍵的問題,如果將遠(yuǎn)程對象的發(fā)現(xiàn)類比于IP地址的發(fā)現(xiàn)可能比較好理解一些。
平常我們上網(wǎng)是通過域名來定位一個網(wǎng)站,實(shí)際上網(wǎng)絡(luò)是通過IP地址來定位網(wǎng)站,因此其中就存在一個映射的過程,域名系統(tǒng)(DNS)就是為了這個目的出現(xiàn)的,在域名系統(tǒng)中通過域名來查找對應(yīng)的IP地址來訪問對應(yīng)的服務(wù)器。
對應(yīng)的,IP地址在這里就相當(dāng)于遠(yuǎn)程對象的引用,而DNS則相當(dāng)于一個注冊表(Registry)
而域名在RMI中就相當(dāng)于遠(yuǎn)程對象的標(biāo)識符,客戶端通過提供遠(yuǎn)程對象的標(biāo)識符訪問注冊表,來得到遠(yuǎn)程對象的引用。這個標(biāo)識符是類似URL地址格式的,它要滿足的規(guī)范如下:
該名稱是URL形式的,類似于http的URL,schema是rmi;
格式類似于rmi://host:port/name,host指明注冊表運(yùn)行的注解,port表明接收調(diào)用的端口,name是一個標(biāo)識該對象的簡單名稱
主機(jī)和端口都是可選的,如果省略主機(jī),則默認(rèn)運(yùn)行在本地;如果端口也省略,則默認(rèn)端口是1099;
2、示例

2.1、創(chuàng)建接口
創(chuàng)建一個接口Hello,該接口需要繼承Remote接口,接口所定義的方法需要拋出RemoteException異常:
//遠(yuǎn)程接口,該接口需要繼承Remote接口,并且接口中的方法全都要拋出RemoteException異常
public interface Hello extends Remote {
public String welcome(String name) throws RemoteException;
}2.2、實(shí)現(xiàn)接口類
基于上面定義的接口實(shí)現(xiàn)一個類Helloimpl,該實(shí)現(xiàn)類需要繼承UnicastRemoteObject類,同樣重載的方法需要拋出RemoteException異常:
/**
* 遠(yuǎn)程接口實(shí)現(xiàn)類,必須繼承UnicastRemoteObject
* (繼承RemoteServer->繼承RemoteObject->實(shí)現(xiàn)Remote,Serializable),
* 只有繼承UnicastRemoteObject類,才表明其可以作為遠(yuǎn)程對象,被注冊到注冊表中供客戶端遠(yuǎn)程調(diào)用
* (補(bǔ)充:客戶端lookup找到的對象,只是該遠(yuǎn)程對象的Stub(存根對象),
* 而服務(wù)端的對象有一個對應(yīng)的骨架Skeleton(用于接收客戶端stub的請求,以及調(diào)用真實(shí)的對象)對應(yīng),
* Stub是遠(yuǎn)程對象的客戶端代理,Skeleton是遠(yuǎn)程對象的服務(wù)端代理,
* 他們之間協(xié)作完成客戶端與服務(wù)器之間的方法調(diào)用時的通信。)
*/
public class HelloImpl extends UnicastRemoteObject implements Hello {
//因?yàn)閁nicastRemoteObject的構(gòu)造方法拋出了RemoteException異常,
//因此這里默認(rèn)的構(gòu)造方法必須寫,也必須聲明拋出RemoteException異常
public HelloImpl() throws RemoteException {
}
@Override
public String welcome(String name) throws RemoteException {
return "Hello " + name;
}
}如果一個遠(yuǎn)程類已經(jīng)繼承了其他類,無法再繼承 UnicastRemoteObiect 類,那么可以在構(gòu)造方法中調(diào)用 UnicastRemoteObject 類的靜態(tài) expotObject 方法,同樣,遠(yuǎn)程類的構(gòu)造方法也必須聲明拋出 RemoteException
public class HelloImpl2 implements Hello {
@Override
public String welcome(String name) throws RemoteException {
return "Hello " + name;
}
public HelloImpl2() throws RemoteException{
//參數(shù) port 指定監(jiān)聽的端口,如果取值為0,就表示監(jiān)聽任意一個匿名端口
UnicastRemoteObject.exportObject(this, 0);
}
}2.3、創(chuàng)建服務(wù)端
服務(wù)端創(chuàng)建了一個注冊表,并注冊了客戶端需要的對象:
public class Server {
public static void main(String[] args) throws RemoteException {
//創(chuàng)建對象
Hello hello = new HelloImpl();
// 本地主機(jī)上的遠(yuǎn)程對象注冊表Registry的實(shí)例,
// 并指定端口,這一步必不可少(Java默認(rèn)端口是1099)
Registry registry = LocateRegistry.createRegistry(1099);
//綁定對象到注冊表,并給它取名為hello
registry.rebind("hello", hello);
}
}向注冊器注冊遠(yuǎn)程對象有三種方式:
//創(chuàng)建遠(yuǎn)程對象
HelloService service1 = new HelloServiceImpl("service1");
//方式1:調(diào)用 java.i.registry.Registy 接口的 bind 或 rebind 方法
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("HelloService1", service1);
//方式2:調(diào)用命名服務(wù)類 java.rmi.Naming 的 bind 或 rebind 方法
Naming.rebind("HelloService1", service1);
//方式3:調(diào)用 JNDI API 的 javax.naming.Context 接口的 bind 或rebind 方法
Context namingContext = new InitialContext();
namingContext.rebind("rmi:HelloService1", service1);2.4、客戶端調(diào)用遠(yuǎn)程對象
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
//獲取到注冊表的代理
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
//利用注冊表的代理去查詢遠(yuǎn)程注冊表中名為hello的對象
Hello hello = (Hello) registry.lookup("hello");
//調(diào)用遠(yuǎn)程方法
System.out.println(hello.welcome("tom"));
}
}2.5、運(yùn)行結(jié)果
先運(yùn)行服務(wù)端,再運(yùn)行客戶端:

3、其它
3.1、遠(yuǎn)程方法中的參數(shù)與返回值傳遞
當(dāng)客戶端調(diào)用服務(wù)器端的遠(yuǎn)程對象的方法時,客戶端會向服務(wù)器端傳遞參數(shù),服務(wù)器端則會向客戶端傳遞返回值。RMI 規(guī)范對參數(shù)以及返回值的傳遞的規(guī)定如下所述:
- 只有基本類型的數(shù)據(jù)、遠(yuǎn)程對象以及可序列化的對象才可以被作為參數(shù)或者返回值進(jìn)行傳遞
- 如果參數(shù)或返回值是一個遠(yuǎn)程對象,那么把它的存根對象傳遞到接收方。也就是說接收方得到的是遠(yuǎn)程對象的存根對象
- 如果參數(shù)或返回值是可序列化對象,那么直接傳遞該對象的序列化數(shù)據(jù)。也就是說接收方得到的是發(fā)送方的可序列化對象的復(fù)制品
- 如果參數(shù)或返回值是基本類型的數(shù)據(jù),那么直接傳遞該數(shù)據(jù)的序列化數(shù)據(jù)。也就是說,接收方得到的是發(fā)送方的基本類型的數(shù)據(jù)的復(fù)制品
3.2、分布式垃圾收集
在 Java 虛擬機(jī)中,對于一個本地對象,只要不被本地 Java 虛擬機(jī)內(nèi)的任何變量引用,它就會結(jié)束生命周期,可以被垃圾回收器回收。而對于一個遠(yuǎn)程對象,不僅會被本地 Java 虛擬機(jī)內(nèi)的變量引用,還會被遠(yuǎn)程引用
服務(wù)器端的一個遠(yuǎn)程對象受到三種引用:
- 服務(wù)器端的一個本地對象持有它的本地引用
- 這個遠(yuǎn)程對象已經(jīng)被注冊到 RMI 注冊器,可以理解為,RMI 注冊器持有它的引用
- 客戶端獲得了這個遠(yuǎn)程對象的存根對象,可以理解為,客戶端持有它的遠(yuǎn)程引用
RMI 框架采用分布式垃圾收集機(jī)制來管理遠(yuǎn)程對象的生命周期,當(dāng)一個遠(yuǎn)程對象不受到任何本地引用和遠(yuǎn)程引用時,這個遠(yuǎn)程對象才會結(jié)束生命周期,并且可以被本地 Java 虛擬機(jī)的垃圾回收器回收。
服務(wù)器端如何知道客戶端持有一個遠(yuǎn)程對象的遠(yuǎn)程引用呢?當(dāng)客戶端獲得了一個服務(wù)器端的遠(yuǎn)程對象的存根后,就會向服務(wù)器發(fā)送一條租約通知,告訴服務(wù)器自己持有這個遠(yuǎn)程對象的引用了??蛻舳藢@個遠(yuǎn)程對象有一個租約期限,默認(rèn)值為 600000ms。當(dāng)至達(dá)了租約期限的一半時間,客戶如果還持有遠(yuǎn)程引用,就會再次向服務(wù)器發(fā)送租約通知??蛻舳瞬粩嘣诮o定的時間間隔中向服務(wù)器發(fā)送租約通知,從而使腸務(wù)器知道客戶端一直持有遠(yuǎn)程對象的引用。如果在租約到期后,服務(wù)器端沒有繼續(xù)收到客戶端的新的租約通知,服務(wù)器端就會認(rèn)為這個客戶已經(jīng)不再持有遠(yuǎn)程對象的引用了
3.3、動態(tài)加載
遠(yuǎn)程對象一般分布在服務(wù)器端,當(dāng)客戶端試圖調(diào)用遠(yuǎn)程對象的方法時,如果在客戶端還不存在遠(yuǎn)程對象所依賴的類文件,比如遠(yuǎn)程方法的參數(shù)和返回值對應(yīng)的類文件,客戶就會從 java.rmi.server.codebase 系統(tǒng)屬性指定的位貿(mào)動態(tài)加載該類文件
同樣,當(dāng)服務(wù)器端訪問客戶端的遠(yuǎn)程對象時,如果服務(wù)器端不存在相關(guān)的類文件,腐務(wù)器就會從 java.rmi.server.codebase 屬性指定的位置動態(tài)加載它們
此外,當(dāng)服務(wù)器向 RMI 注冊器注冊遠(yuǎn)程對象時,注冊器也會從 java.rmi.server.codebase 屬性指定的位置動態(tài)加載相關(guān)的遠(yuǎn)程接口的類文件
前面的例子都是在同一個 classpath 下運(yùn)行服務(wù)器程序以及客戶程序的,這些程序都能從本地 classpath 中找到相應(yīng)的類文件,因此無須從 java.rmi.server.codebase 屬性指定的位置動態(tài)加載類。而在實(shí)際應(yīng)用中,客戶程序與服務(wù)器程序運(yùn)行在不同的主機(jī)上,因此當(dāng)客戶端調(diào)用服務(wù)器端的遠(yuǎn)程對象的方法時,有可能需要從遠(yuǎn)程文件系統(tǒng)加載類文件。同樣,當(dāng)服務(wù)器端調(diào)用客戶端的遠(yuǎn)程對象的方法時,也有可能從遠(yuǎn)程文件系統(tǒng)加載類文件
我們可以且把這些需要被加載的類的文件都集中放在網(wǎng)絡(luò)上的同一地方,啟動時將java.rmi.server.codebase 設(shè)置為指定位置,從而實(shí)現(xiàn)動態(tài)加載
start java -Djava.rmi.server.codebase=http://www.javathinker.net/download/
到此這篇關(guān)于Java中的RMI使用方法詳解的文章就介紹到這了,更多相關(guān)Java RMI使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一篇文章教你將JAVA的RabbitMQz與SpringBoot整合
這篇文章主要介紹了如何將JAVA的RabbitMQz與SpringBoot整合,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2021-09-09
SpringSecurity多表多端賬戶登錄的實(shí)現(xiàn)
本文主要介紹了SpringSecurity多表多端賬戶登錄的實(shí)現(xiàn)2024-05-05
通過Spring層面進(jìn)行事務(wù)回滾的實(shí)現(xiàn)
本文主要介紹了通過Spring層面進(jìn)行事務(wù)回滾的實(shí)現(xiàn),包括聲明式事務(wù)和編程式事務(wù),具有一定的參考價值,感興趣的可以了解一下2025-04-04
IDEA搭建SpringBoot多模塊聚合工程過程詳解(多模塊聚合工程)
這篇文章主要是介紹一下,如何在IDEA開發(fā)工具下,搭建一個基于SpringBoot的多模塊聚合工程項目,本篇文章,將項目模塊細(xì)分為幾個子工程模塊,在文中給大家詳細(xì)介紹過,對IDEA搭建SpringBoot多模塊聚合工程感興趣的朋友一起看看吧2022-04-04
有關(guān)ServletConfig與ServletContext的訪問
下面小編就為大家?guī)硪黄嘘P(guān)ServletConfig與ServletContext的訪問。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01
IntelliJ IDEA彈出“IntelliJ IDEA License Activation”的處理方法
這篇文章主要介紹了IntelliJ IDEA彈出“IntelliJ IDEA License Activation”的處理方法,本文給出解決方法,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09

