1. 程式人生 > >系統間通訊方式之(Java之RMI初步使用詳解)(八)

系統間通訊方式之(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技術框架做效能比較

(說明作者沒有弄清楚Socket框架和RMI框架的關係)。描述RMI底層IO模型的文章更是沒有找到,只找到一篇寥寥幾字的文章居然說RMI和NIO沒有任何關聯

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通訊模型。