Java 中 RMI 的使用

未讀程式碼發表於2021-05-11

RMI 介紹

RMI (Remote Method Invocation) 模型是一種分散式物件應用,使用 RMI 技術可以使一個 JVM 中的物件,呼叫另一個 JVM 中的物件方法並獲取呼叫結果。這裡的另一個 JVM 可以在同一臺計算機也可以是遠端計算機。因此,RMI 意味著需要一個 Server 端和一個 Client 端。

Server 端通常會建立一個物件,並使之可以被遠端訪問。

  • 這個物件被稱為遠端物件。
  • Server 端需要註冊這個物件可以被 Client 遠端訪問。

Client 端呼叫可以被遠端訪問的物件上的方法,Client 端就可以和 Server 端進行通訊並相互傳遞資訊。

說到這裡,是不是發現使用 RMI 在構建一個分散式應用時十分方便,它和 RPC 一樣可以實現分散式應用之間的互相通訊,甚至和現在的微服務思想都十分類似。

RMI 工作原理

正所謂 “知其然知其所以然”,在開始編寫 RMI 程式碼之前,有必要了解一下 RMI 的工作原理,RMI 中 Client 端是和 Server 端是如何通訊的呢?

下圖的可以幫助我們理解RMI 的工作流程。

從圖中可以看到,Client 端有一個被稱 Stub 的東西,有時也會被成為存根,它是 RMI Client 的代理物件,Stub 的主要功能是請求遠端方法時構造一個資訊塊,RMI 協議會把這個資訊塊傳送給 Server 端。

這個資訊塊由幾個部分組成:

  • 遠端物件識別符號。
  • 呼叫的方法描述。
  • 編組後的引數值(RMI協議中使用的是物件序列化)。

既然 Client 端有一個 Stub 可以構造資訊塊傳送給 Server 端,那麼 Server 端必定會有一個接收這個資訊快的物件,稱為 Skeleton

它主要的工作是:

  • 解析資訊快中的呼叫物件識別符號和方法描述,在 Server 端呼叫具體的物件方法。
  • 取得呼叫的返回值或者異常值。
  • 把返回值進行編組,返回給客戶端 Stub.

到這裡,一次從 Client 端對 Server 端的呼叫結果就可以獲取到了。

RMI 開發

通過上面的介紹,知道了 RMI 的概念以及 RMI 的工作原理,下面介紹 RMI 的開發流程。

這裡會通過一個場景進行演示,假設 Client 端需要查詢使用者資訊,而使用者資訊存在於 Server 端,所以在 Server 端開放了 RMI 協議介面供客戶端呼叫查詢。

RMI Server

Server 端主要是構建一個可以被傳輸的類 User,一個可以被遠端訪問的類 UserService,同時這個物件要註冊到 RMI 開放給客戶端使用。

  1. 定義伺服器介面(需要繼承 Remote 類,方法需要丟擲 RemoteException)。

    package com.wdbyte.rmi.server;
    
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    
    
    /**
     * RMI Server
     *
     * @author www.wdbyte.com
     * @date 2021/05/08
     */
    public interface UserService extends Remote {
    
        /**
         * 查詢使用者
         * 
         * @param userId
         * @return
         * @throws RemoteException
         */
        User findUser(String userId) throws RemoteException;
    }
    

    User 物件在步驟 3 中定義。

  2. 實現伺服器介面(需要繼承 UnicastRemoteObject 類,實現定義的介面)。

    package com.wdbyte.rmi.server;
    
    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    
    /**
     * @author www.wdbyte.com
     * @date 2021/05/08
     */
    public class UserServiceImpl extends UnicastRemoteObject implements UserService {
    
        protected UserServiceImpl() throws RemoteException {
        }
    
        @Override
        public User findUser(String userId) throws RemoteException {
            // 載入在查詢
             if ("00001".equals(userId)) {
                User user = new User();
                user.setName("金庸");
                user.setAge(100);
                user.setSkill("寫作");
                return user;
            }
            throw new RemoteException("查無此人");
        }
    }
    
  3. 定義傳輸的物件,傳輸的物件需要實現序列化(Serializable)介面。

    需要傳輸的類一定要實現序列化介面,不然傳輸時會報錯。IDEA 中如何生成 serialVersionUID,在文章末尾也附上了簡單教程。

    package com.wdbyte.rmi.server;
    
    import java.io.Serializable;
    
    /**
     *
     * @author www.wdbyte.com
     * @date 2021/05/08
     */
    public class User implements Serializable {
    
        private static final long serialVersionUID = 6490921832856589236L;
    
        private String name;
        private Integer age;
        private String skill;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        public String getSkill() {
            return skill;
        }
    
        public void setSkill(String skill) {
            this.skill = skill;
        }
        
        @Override
        public String toString() {
            return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", skill='" + skill + '\'' +
                '}';
        }
    }
    
  4. 註冊( rmiregistry)遠端物件,並啟動服務端程式。

    服務端繫結了 UserService 物件作為遠端訪問的物件,啟動時埠設定為 1900。

    package com.wdbyte.rmi.server;
    
    import java.rmi.Naming;
    import java.rmi.registry.LocateRegistry;
    
    /**
     * RMI Server 端
     *
     * @author https://www.wdbyte.com
     * @date 2021/05/08
     */
    public class RmiServer {
    
        public static void main(String[] args) {
            try {
                UserService userService = new UserServiceImpl();
                LocateRegistry.createRegistry(1900);
                Naming.rebind("rmi://localhost:1900/user", userService);
                System.out.println("start server,port is 1900");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

RMI Client

相比 Server 端,Client 端就簡單的多。直接引入可遠端訪問和需要傳輸的類,通過埠和 Server 端繫結的地址,就可以發起一次呼叫。

package com.wdbyte.rmi.client;

import java.rmi.Naming;

import com.wdbyte.rmi.server.User;
import com.wdbyte.rmi.server.UserService;

/**
 * @author https://www.wdbyte.com
 * @date 2021/05/08
 */
public class RmiClient {
    public static void main(String args[]) {
        User answer;
        String userId = "00001";
        try {
            // lookup method to find reference of remote object
            UserService access = (UserService)Naming.lookup("rmi://localhost:1900/user");
            answer = access.findUser(userId);
            System.out.println("query:" + userId);
            System.out.println("result:" + answer);
        } catch (Exception ae) {
            System.out.println(ae);
        }
    }
}

RMI 測試

啟動 Server 端。

start server,port is 1900

啟動 Client 端。

query:00001
result:User{name='金庸', age=100, skill='寫作'}

如果 Client 端傳入不存在的 userId。

java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
	java.rmi.RemoteException: 查無此人

serialVersionUID 的生成

IDEA 中生成 serialVersionUID,開啟設定,如下圖所示勾選。

選中要生成 serialVersionUID 的類,按智慧提示快捷鍵。

參考

[1] https://docs.oracle.com/javase/tutorial/rmi/overview.html

訂閱

可以關注未讀程式碼部落格或者微信搜尋「 未讀程式碼 」。

文章會在部落格和公眾號同步更新。

相關文章