系統間通訊方式之(Java之RMI初步使用詳解)(八)
1、概述
在概述了資料描述格式的基本知識、IO通訊模型的基本知識後。我們終於可以進入這個系列博文的重點:系統間通訊管理。在這個章節我將通過對RMI的詳細介紹,引出一個重要的系統間通訊的管理規範RPC,並且繼續討論一些RPC的實現;再通過分析PRC的技術特點,引出另一種系統間通訊的管理規範ESB,並介紹ESB的一些具體實現。最後我們介紹SOA:面向服務的軟體架構。
2、RMI基本使用
RMI(Remote Method Invocation,遠端方法呼叫),是JAVA早在JDK 1.1中提供的JVM與JVM之間進行 物件方法呼叫的技術框架的實現(在JDK的後續版本中,又進行了改進)。通過RMI技術,某一個本地的JVM可以呼叫存在於另外一個JVM中的物件方法,就好像它僅僅是在呼叫本地JVM中某個物件方法一樣。例如RMI客戶端中的如下呼叫:
List< UserInfo > users = remoteServiceInterface.queryAllUserinfo();
看似remoteServiceInterface物件和普通的物件沒有區別,但實際上remoteServiceInterface物件的具體方法實現卻不在本地的JVM中,而是在某個遠端的JVM中(這個遠端的JVM可以是RMI客戶端同屬於一臺物理機,也可以屬於不同的物理機)
我在寫這篇部落格的時候,查閱了一些網路資料。發現將RMI講透徹的文章很少。有不少的文章提出“RMI技術已經過時”的觀點,“效能不好”的觀點等。更有甚者甚者將JAVA 原生的Socket框架和RMI技術框架做效能比較
1-1、RMI使用場景
RMI是基於JAVA語言的,也就是說在RMI技術框架的描述中,只有Server端使用的是JAVA語言並且Client端也是用的JAVA語言,才能使用RMI技術(目前在codeproject.com中有一個開源專案名字叫做“RMI for C++”,可以實現JAVA To C++的RMI呼叫。但是這是一個第三方的實現,並不是java的標準RMI框架定義,所以並不在我們的討論範圍中)。
RMI適用於兩個系統都主要使用JAVA語言進行構造,不需要考慮跨語言支援的情況。並且對兩個JAVA系統的通訊速度有要求的情況。
RMI 是一個良好的、特殊的RPC實現:使用JRMP協議承載資料描述,可以使用BIO和NIO兩種IO通訊模型。RMI框架是可以在大規模集群系統中使用的,當然是不是使用RMI技術,還要看您的產品的技術背景、團隊的技術背景、公司的業務背景甚至客戶的非技術背景等。(但如果您說您自己寫的分散式系統性能優於RMI,那說明我膚淺了:原來在我國像Bill Joy、Ann Wollrath這樣的大師竟然是一抓一大把!!!)
1-2、RMI框架的基本組成
雖然RMI早在JDK.1.1版本中就開放了。但是在JDK1.5的版本中RMI又進行改進。所以我們後續的程式碼示例和原理講解都基於最新的RMI框架特性。
要定義和使用一套基於RMI框架工作的系統,您至少需要做一下幾個工作:
1、定義RMI Remote介面
2、實現這個RMI Remote介面
3、生成Stub(樁)和 Skeleton(骨架)。這一步的具體操作視不同的JDK版本而有所不同(例如JDK1.5後,Skeleton不需要手動);“RMI登錄檔”的工作方式也會影響“Stub是否需要命令列生成”這個問題。
4、向“RMI登錄檔”註冊在第2步我們實現的RMI Remote介面。
5、建立一個Remote客戶端,通過java“命名服務”在“RMI登錄檔”所在的IP:PORT尋找註冊好的RMI服務。
6、Remote客戶端向呼叫存在於本地JVM中物件那樣,呼叫存在於遠端JVM上的RMI介面。
下圖描述了上述幾個概念名稱間的關係,呈現了JDK.5中RMI框架其中一種執行方式(注意,是其中一種工作方式。也就是說RMI框架不一定都是這種執行方式,後文中我們還將描述另外一種RMI的工作方式):
1-3、程式碼示例一
在這個程式碼中,我們將使用“本地RMI登錄檔”(LocateRegistry),讓RMI服務的具體提供者和RMI登錄檔工作在同一個JVM上,向您介紹最基本的RMI服務的定義、編寫、註冊和呼叫過程:
首先我們必須定義RMI 服務介面,程式碼如下:
package testRMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
import testRMI.entity.UserInfo;
public interface RemoteServiceInterface extends Remote {
/**
* 這個RMI介面負責查詢目前已經註冊的所有使用者資訊
*/
public List<UserInfo> queryAllUserinfo() throws RemoteException;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
很簡單的程式碼,應該不用多解釋什麼了。這個定義的介面方法如果放在某個業務系統A中,您可以理解是查詢這個系統A中所有可用的使用者資料。注意這個介面所繼承的java.rmi.Remote介面,是“RMI服務介面”定義的特點。
那麼有介面定義了,自然就要實現這個介面:
package testRMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.List;
import testRMI.entity.UserInfo;
/**
* RMI 服務介面RemoteServiceInterface的具體實現<br>
* 請注意這裡繼承的是UnicastRemoteObject父類。
* 繼承於這個父類,表示這個Remote Object是“存在於本地”的RMI服務實現
* (這句話後文會解釋)
* @author yinwenjie
*
*/
public class RemoteUnicastServiceImpl extends UnicastRemoteObject implements RemoteServiceInterface {
/**
* 注意Remote Object沒有預設建構函式
* @throws RemoteException
*/
protected RemoteUnicastServiceImpl() throws RemoteException {
super();
}
private static final long serialVersionUID = 6797720945876437472L;
/* (non-Javadoc)
* @see testRMI.RemoteServiceInterface#queryAllUserinfo()
*/
@Override
public List<UserInfo> queryAllUserinfo() throws RemoteException {
List<UserInfo> users = new ArrayList<UserInfo>();
UserInfo user1 = new UserInfo();
user1.setUserAge(21);
user1.setUserDesc("userDesc1");
user1.setUserName("userName1");
user1.setUserSex(true);
users.add(user1);
UserInfo user2 = new UserInfo();
user2.setUserAge(21);
user2.setUserDesc("userDesc2");
user2.setUserName("userName2");
user2.setUserSex(false);
users.add(user2);
return users;
}
}
- 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
- 51
還有我們定義的Userinfo資訊,就是一個普通的POJO物件:
package testRMI.entity;
import java.io.Serializable;
import java.rmi.RemoteException;
public class UserInfo implements Serializable {
/**
*
*/
private static final long serialVersionUID = -377525163661420263L;
private String userName;
private String userDesc;
private Integer userAge;
private Boolean userSex;
public UserInfo() throws RemoteException {
}
/**
* @return the userName
*/
public String getUserName() {
return userName;
}
/**
* @param userName the userName to set
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* @return the userDesc
*/
public String getUserDesc() {
return userDesc;
}
/**
* @param userDesc the userDesc to set
*/
public void setUserDesc(String userDesc) {
this.userDesc = userDesc;
}
/**
* @return the userAge
*/
public Integer getUserAge() {
return userAge;
}
/**
* @param userAge the userAge to set
*/
public void setUserAge(Integer userAge) {
this.userAge = userAge;
}
/**
* @return the userSex
*/
public Boolean getUserSex() {
return userSex;
}
/**
* @param userSex the userSex to set
*/
public void setUserSex(Boolean userSex) {
this.userSex = userSex;
}
}
- 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
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
RMI Server 的介面定義和RMI Server的實現都有了,那麼編寫程式碼的最後一步是將這個RMI Server註冊到“RMI 登錄檔”中執行。這樣 RMI的客戶端就可以呼叫這個 RMI Server了。下面的程式碼是將RMI Server註冊到“本地RMI 登錄檔”中:
package testRMI;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RemoteUnicastMain {
public static void main(String[] args) throws Exception {
/*
* Locate registry,您可以理解成RMI服務登錄檔,或者是RMI服務位置倉庫。
* 主要的作用是維護一個“可以正常提供RMI具體服務的所在位置”。
* 每一個具體的RMI服務提供者,都會講自己的Stub註冊到Locate registry中,以表示自己“可以提供服務”
*
* 有兩種方式可以管理Locate registry,一種是通過作業系統的命令列啟動登錄檔;
* 另一種是在程式碼中使用LocateRegistry類。
*
* LocateRegistry類中有一個createRegistry方法,可以在這臺物理機上建立一個“本地RMI登錄檔”
* */
LocateRegistry.createRegistry(1099);
// 以下是向LocateRegistry註冊(繫結/重繫結)RMI Server實現。
RemoteUnicastServiceImpl remoteService = new RemoteUnicastServiceImpl();
// 通過java 名字服務技術,可以講具體的RMI Server實現繫結一個訪問路徑。註冊到LocateRegistry中
Naming.rebind("rmi://127.0.0.1:1099/queryAllUserinfo", remoteService);
/*
* 在“已經擁有某個可訪問的遠端RMI登錄檔”的情況下。
* 下面這句程式碼就是向遠端登錄檔註冊RMI Server,
* 當然遠端RMI登錄檔的JVM-classpath中一定要有這個Server的Stub存在
*
* (執行在另外一個JVM上的RMI登錄檔,可能是同一臺物理機也可能不是同一臺物理機)
* Naming.rebind("rmi://192.168.61.1:1099/queryAllUserinfo", remoteService);
* */
}
}
- 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
這樣我們後續編寫的Client端就可以呼叫這個RMI Server了。但是在給出Client端的程式碼前,關於前面幾個類的程式碼還要進行一些細節的說明:
-
由於我們使用LocateRegistry建立了一個“本地RMI登錄檔”,所以不需要使用rmic命令生成Stub了(注意是“不需要手工生成”而不是“不需要”了),這是因為RMI Sever真實服務的JVM和RMI 登錄檔的JVM是同一個JVM。
-
那麼RMI Sever真實服務的JVM和RMI登錄檔的JVM可以是兩個不同的JVM嗎?當然可以。而且這才是RMI框架靈活性、健壯性的提現。
-
請注意RemoteUnicastServiceImpl的定義,它繼承了UnicastRemoteObject。一般來說RMI Server的實現可以繼承兩種父類:UnicastRemoteObject和Activatable(下篇文章就會講到Activatable)。
-
前者的意義是,RMI Server真實的服務提供者將工作在“本地JVM”上;後者的意義是,RMI Server的真是的服務提供者,不是在“本地JVM”上執行,而是可以通過“RMI Remote Server 啟用”技術,被序列化到“遠端JVM”(即遠端RMI登錄檔所在的JVM上),並適時被“遠端JVM”載入執行。
-
再注意一下“Naming.rebind”和“Naming.bind”的區別。前置是指“重繫結”,如果“重繫結”時“RMI 登錄檔”已經有了這個服務name的存在,則之前所繫結的Remote Object將會被替換;而後者在執行時如果“繫結”時“RMI登錄檔”已經有這個服務name的存在,則系統會丟擲錯誤。所以除非您有特別的業務要求,那麼建議使用rebind方法進行Remote Object繫結。
-
還要注意registry.rebind和Naming.rebind繫結的區別。前者是使用RMI登錄檔繫結,所以不需要寫完整的RMI URL了;後者是通過java的名稱服務進行繫結,由於名稱服務不止為RMI框架提供查詢服務,所以在繫結是要書寫完成的RMI URL。
下面的程式碼是RMI Client的程式碼:
package testRMI;
import java.rmi.Naming;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import testRMI.entity.UserInfo;
/**
* 客戶端呼叫RMI測試
* @author yinwenjie
*
*/
public class RemoteClient {
static {
BasicConfigurator.configure();
}
/**
* 日誌
*/
private static final Log LOGGER = LogFactory.getLog(RemoteClient.class);
public static void main(String[] args) throws Exception {
// 您看,這裡使用的是java名稱服務技術進行的RMI介面查詢。
RemoteServiceInterface remoteServiceInterface = (RemoteServiceInterface)Naming.lookup("rmi://192.168.61.1/queryAllUserinfo");
List<UserInfo> users = remoteServiceInterface.queryAllUserinfo();
RemoteClient.LOGGER.info("users.size() = " +users.size());
}
}
- 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
那麼怎麼來執行這段程式碼呢?如果您使用的是eclipse編寫了您第一個RMI Server和RMI Client,並且您使用的是“本地RMI 登錄檔”。那麼您不需要做任何的配置、指令碼指定等工作(包括不需要專門設定JRE許可權、不需要專門指定classpath、不需要專門生成Stub和Skeleton),就可以看到RMI的執行和呼叫效果了:
下圖為RemoteUnicastMain的效果RMI 服務註冊和執行效果:
可以看到,RemoteUnicastMain中的程式碼執行完成後整個應用程式沒有退出。如下圖:
這是因為這個應用程式要承擔“真實的RMI Server實現”的服務呼叫。如果它退出,RMI 登錄檔就無法請求真實的服務實現了。
我們再來看下圖,RemoteClient呼叫RMI 服務的效果:
很明顯控制檯將返回
0 [main] INFO testRMI.RemoteClient - users.size() = 2
1-4、程式碼示例二
好吧,文章寫到這裡我不得不承認我在誤導大家。因為上面的程式碼既沒有涉及到Stub的問題,也沒有涉及到RMI登錄檔的講解。那麼在示例程式碼一的時候,我們講到了RMI登錄檔和RMI Server 實現是可以分成兩個JVM執行的;我們還講到Stub是需要手動生成的。那麼這個該怎麼做呢?
- 首先我們需要改寫RemoteUnicastMain類,將RemoteUnicastMain中使用LocateRegistry類建立“本地RMI登錄檔”的程式碼去掉:
package testRMI;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RemoteRegistryUnicastMain {
public static void main(String[] args) throws Exception {
/*
* 我們通過LocateRegistry的get方法,尋找一個存在於遠端JVM上的RMI登錄檔
* */
Registry registry = LocateRegistry.getRegistry("192.168.61.1", 1099);
// 以下是向遠端RMI登錄檔(繫結/重繫結)RMI Server的Stub。
// 同樣的遠端RMI登錄檔的JVM-classpath下,一定要有這個RMI Server的Stub
RemoteUnicastServiceImpl remoteService = new RemoteUnicastServiceImpl();
/*
* 在不寫LocateRegistry.createRegistry(1099);的情況下。
* 下面這句程式碼就是註冊 遠端RMI登錄檔 (執行在另外一個JVM上的RMI登錄檔,
* 可能是同一臺物理機也可能不是同一臺物理機)
*
* 註冊的RMI登錄檔存在於192.168.61.1這個IP上
*
* 使用登錄檔registry進行繫結或者重繫結時,不需要寫完整的RMI URL
* */
registry.rebind("queryAllUserinfo" , remoteService);
}
}
- 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
- 然後我們要為RMI Remote Server的實現RemoteUnicastServiceImpl手動生成Stub(RMI中稱之為樁);為什麼要生成呢?因為RMI Remote Server的實現和RMI登錄檔將工作在兩個獨立的JVM上,RMI登錄檔需要知道Server實現的基本資訊(包括類方法資訊、類的引用情況等),這些資訊就是定義在Stub類中的。下面我們在windows環境下生成Stub(linux環境下過程基本相同):
rmic -classpath E:\testworkspace\testBSocket\target\classes testRMI.RemoteServiceImpl
上面的Dos視窗程式碼講解一下:
rmic命令:就是rmic命令了,這個命令專門用來生成Stub和Skeleton(JDK1.5+不會生成Skeleton了)
-classpath:classpath引數。指定class目錄的位置。這個引數和您安裝JDK時,在環境變數中設定的CLASSPATH引數含義是一樣的。只是在我的環境中,工程編譯的路徑是E:\testworkspace\testBSocket\target\classes,這個路徑沒有設定設定在環境變數中,所以在生成Stub需要專門指定(否則rmic沒法識別到哪個根路徑識別class)
-testRMI.RemoteServiceImpl:要生成Stub的RMI Server服務實現類。這個類一定要實現java.rmi.Remote介面。
在執行完成後,對應的class目錄下您將可以看到生成好的Stub class。RemoteUnicastServiceImpl_Stub.class就是剛才生成的Stub class。這個Stub class和RemoteServiceInterface需要放到“RMI 登錄檔”執行JVM的classpath下面。
- 接下來我們啟動遠端“RMI 登錄檔”服務:
//設定classpath
set CLASSPATH=%CLASSPATH%;E:
\testworkspace\testBSocket\target\classes//linux下的話,就這麼命令
export CLASSPATH=$CLASSPATH:/usr/java/classpath//啟動登錄檔應用程式
rmiregistry -p 1099
如果不指定“-p”埠引數,那麼預設的埠就是1099。
- 再接下來使用修改後的RemoteUnicastMain,將RMI Remote Server註冊到遠端“RMI登錄檔”中:
現在這個RMI Remote Server就被註冊到遠端“RMI登錄檔”上了。但是RemoteRegistryUnicastMain的執行效果和之前RemoteUnicastMain的執行效果是一樣的。執行到bind/rebind語句時,應用程式也沒有退出。原因和示例程式碼一中的原因是一樣的。
- 最後指定Client的呼叫,呼叫RMI URL的IP地址需要更改一下:
package testRMI;
import java.rmi.Naming;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
import testRMI.entity.UserInfo;
/**
* 客戶端呼叫RMI測試
* @author yinwenjie
*
*/
public class RemoteClient {
static {
BasicConfigurator.configure();
}
/**
* 日誌
*/
private static final Log LOGGER = LogFactory.getLog(RemoteClient.class);
public static void main(String[] args) throws Exception {
RemoteServiceInterface remoteServiceInterface = (RemoteServiceInterface)Naming.lookup("rmi://192.168.61.1:1099/queryAllUserinfo");
List<UserInfo> users = remoteServiceInterface.queryAllUserinfo();
RemoteClient.LOGGER.info("users.size() = " +users.size());
}
}
- 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
3、JAVA RMI 原理
下篇文章開始,我們繼續講解JAVA RMI中工作原理。並且詳細分析RMI框架底層的IO通訊模型。