Java RMI技術詳解與案例分析

威哥爱编程發表於2024-08-05

Java RMI(Remote Method Invocation)是一種允許Java虛擬機器之間進行通訊和互動的技術。它使得遠端Java物件能夠像本地物件一樣被訪問和操作,從而簡化了分散式應用程式的開發。一些應用依然會使用 RMI 來實現通訊和互動,今天的內容我們來聊聊 RMI 的那些事兒。

一、先來了解一下概念

RMI原理

RMI的基本思想是遠端方法呼叫。客戶端呼叫遠端方法時,實際上是傳送一個呼叫請求到伺服器,由伺服器執行該方法,並將結果返回給客戶端。RMI透過存根(Stub)和骨架(Skeleton)類來實現遠端呼叫,存根位於客戶端,而骨架位於伺服器端。

RMI元件

  1. 遠端介面:必須繼承自java.rmi.Remote介面,並宣告丟擲RemoteException
  2. 遠端物件:實現了遠端介面的類。
  3. RMI伺服器:提供遠端物件,並處理客戶端的呼叫請求。
  4. RMI客戶端:發起遠端方法呼叫請求。
  5. 註冊服務(Registry):提供服務註冊與獲取,類似於目錄服務。

資料傳遞

RMI使用Java序列化機制來傳遞資料。客戶端將方法引數序列化後透過網路傳送給伺服器,伺服器反序列化引數並執行遠端方法,然後將結果序列化回傳給客戶端。

RMI案例

以下是一個簡單的RMI案例,包括伺服器和客戶端的實現思路,下文V 將再用程式碼來解釋:

伺服器端

  1. 實現一個遠端介面,例如PersonController,包含一個遠端方法queryName
  2. 建立該介面的具體實現類PersonControllerImpl,並在其中實現遠端方法。
  3. 在伺服器的main方法中,例項化遠端物件,建立RMI登錄檔,並使用Naming.rebind將遠端物件繫結到指定名稱。

客戶端

  1. 透過Naming.lookup方法,使用RMI登錄檔提供的名稱獲取遠端物件的存根。
  2. 呼叫存根上的方法,就像呼叫本地方法一樣,實際上是在呼叫伺服器上的遠端方法。

RMI的侷限性

  • 語言限制:RMI是Java特有的技術,不能直接用於非Java應用程式。
  • 安全性問題:RMI的序列化機制可能帶來安全風險,不建議將1099埠暴露在公網上。
  • 效能和擴充套件性:RMI的效能受網路延遲和頻寬影響,且在高併發情況下可能面臨擴充套件性限制。

RMI的應用場景

RMI適用於需要Java程式之間進行遠端通訊的場景,如分散式銀行系統、遊戲伺服器、股票交易系統和網上商城等。接下來一起看一個簡單的案例使用吧。

二、案例使用

先來搞一個簡單的Java RMI伺服器端和客戶端的實現案例。這個案例中,伺服器端將提供一個名為HelloWorld的遠端服務,客戶端將呼叫這個服務並列印返回的問候語。

伺服器端實現

  1. 定義遠端介面
    伺服器和客戶端都需要這個介面。它必須繼承自java.rmi.Remote介面,並且所有遠端方法都要宣告丟擲RemoteException
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloWorld extends Remote {
    String sayHello() throws RemoteException;
}
  1. 實現遠端介面
    建立一個實現了上述介面的類,並實現遠端方法。
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;

public class HelloWorldImpl extends UnicastRemoteObject implements HelloWorld {
    protected HelloWorldImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHello() throws RemoteException {
        return "Hello, World!";
    }
}
  1. 設定RMI伺服器
    建立一個主類來設定RMI伺服器,繫結遠端物件到RMI登錄檔。
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class HelloWorldServer {
    public static void main(String[] args) {
        try {
            // 建立遠端物件
            HelloWorld helloWorld = new HelloWorldImpl();
            // 獲取RMI登錄檔的引用,並在指定埠上建立或獲取登錄檔例項
            LocateRegistry.createRegistry(1099);
            // 將遠端物件繫結到RMI登錄檔中,客戶端可以透過這個名字訪問遠端物件
            Naming.bind("rmi://localhost/HelloWorld", helloWorld);
            System.out.println("HelloWorld RMI object bound");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

客戶端實現

  1. 呼叫遠端服務
    客戶端使用RMI登錄檔的名字來查詢遠端物件,並呼叫其方法。
import java.rmi.Naming;
import java.rmi.RemoteException;

public class HelloWorldClient {
    public static void main(String[] args) {
        try {
            // 使用RMI登錄檔的名字查詢遠端物件
            HelloWorld helloWorld = (HelloWorld) Naming.lookup("rmi://localhost/HelloWorld");
            // 呼叫遠端方法
            String response = helloWorld.sayHello();
            System.out.println("Response: " + response);
        } catch (Exception e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

來詳細解釋吧

  • 遠端介面 (HelloWorld): 這是伺服器和客戶端之間通訊的協議。它定義了可以被遠端呼叫的方法。
  • 遠端物件實現 (HelloWorldImpl): 這是遠端介面的一個實現。RMI呼叫實際上會呼叫這個實現中的方法。
  • 伺服器 (HelloWorldServer): 負責建立遠端物件的例項,並將這個例項繫結到RMI登錄檔中。這樣客戶端就可以透過登錄檔的名字來訪問這個物件。
  • 客戶端 (HelloWorldClient): 使用RMI登錄檔的名字來查詢伺服器上的遠端物件,並呼叫其方法。

接下來就可以編譯所有類檔案,執行伺服器端程式,確保RMI登錄檔已經啟動(在某些Java版本中會自動啟動),再執行客戶端程式,搞定。注意一下哈,由於RMI使用Java序列化機制,因此客戶端和伺服器的類路徑必須一致或相容。

三、RMI 在分散式銀行系統中的應用

接下來V哥要介紹業務場景下的應用了,拿在分散式銀行系統中來說,我們可以使用RMI來實現不同銀行分行之間的通訊,例如,實現賬戶資訊的查詢、轉賬等操作。以下是一個簡化的示例,其中包括兩個基本操作:查詢賬戶餘額和執行轉賬,按步驟一步一步來吧。

步驟1: 定義遠端介面

首先,定義一個遠端介面BankService,它將被各個分行實現以提供銀行服務。

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface BankService extends Remote {
    double getAccountBalance(String accountNumber) throws RemoteException;
    boolean transferFunds(String fromAccount, String toAccount, double amount) throws RemoteException;
}

步驟2: 實現遠端介面

接下來,實現這個介面來建立遠端物件,這個物件將提供實際的銀行服務。

import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;

public class BankServiceImpl extends UnicastRemoteObject implements BankService {
    private Map<String, Double> accounts = new HashMap<>();

    protected BankServiceImpl() throws RemoteException {
        super();
        // 初始化一些賬戶資訊
        accounts.put("123456789", 5000.00);
        accounts.put("987654321", 1000.00);
    }

    @Override
    public double getAccountBalance(String accountNumber) throws RemoteException {
        return accounts.getOrDefault(accountNumber, 0.00);
    }

    @Override
    public boolean transferFunds(String fromAccount, String toAccount, double amount) throws RemoteException {
        if (accounts.containsKey(fromAccount) && accounts.get(fromAccount) >= amount) {
            accounts.put(fromAccount, accounts.get(fromAccount) - amount);
            accounts.merge(toAccount, amount, Double::sum);
            return true;
        }
        return false;
    }
}

步驟3: 設定RMI伺服器

伺服器端將建立BankService的遠端物件例項,並將其繫結到RMI登錄檔中。

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class BankServer {
    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099); // 建立RMI登錄檔
            BankService bankService = new BankServiceImpl();
            Naming.rebind("//localhost/BankService", bankService); // 繫結遠端物件
            System.out.println("BankService is ready for use.");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

步驟4: 實現RMI客戶端

客戶端將使用RMI登錄檔的名字來查詢遠端物件,並呼叫其方法。

import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class BankClient {
    public static void main(String[] args) {
        try {
            BankService bankService = (BankService) Naming.lookup("//localhost/BankService");
            System.out.println("Account balance: " + bankService.getAccountBalance("123456789"));
            
            // 執行轉賬操作
            boolean isTransferSuccess = bankService.transferFunds("123456789", "987654321", 200.00);
            if (isTransferSuccess) {
                System.out.println("Transfer successful.");
            } else {
                System.out.println("Transfer failed.");
            }
            
            // 再次查詢餘額
            System.out.println("New account balance: " + bankService.getAccountBalance("123456789"));
        } catch (RemoteException | NotBoundException e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

來詳細解釋一下

  • 遠端介面 (BankService): 定義了兩個方法:getAccountBalance用於查詢賬戶餘額,transferFunds用於執行轉賬操作。
  • 遠端物件實現 (BankServiceImpl): 實現了BankService介面。它使用一個HashMap來模擬賬戶和餘額資訊。
  • 伺服器 (BankServer): 設定了RMI伺服器,將BankService的實現繫結到RMI登錄檔中,供客戶端訪問。
  • 客戶端 (BankClient): 查詢RMI登錄檔中的BankService服務,並呼叫其方法來查詢餘額和執行轉賬。

擼完程式碼後,編譯所有類檔案,執行伺服器端程式BankServer,再執行客戶端程式BankClient,測試效果吧。

最後

最後V哥要提醒一下,在實際的銀行系統中,當然還需要考慮安全性、事務性、永續性以及錯誤處理等多方面的因素,RMI的網路通訊也需要在安全的網路環境下進行,以防止資料洩露或被篡改。你在應用中是怎麼使用 RMI 的,歡迎關注威哥愛程式設計,一起交流一下哈。

相關文章