首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java RMI 遠程方法調用完整實戰指南:從接口定義到服務部署

     編輯:Java 程式設計入門

前言:RMI 是 Java 原生的分布式物件模型

Java RMI (Remote Method Invocation) 讓執行在不同 JVM (甚至不同機器) 的物件能像本地呼叫一樣透明地調用遠程物件方法。雖然 gRPC、Dubbo、Spring Cloud 等現代 RPC 框架功能更強,但 RMI 作為 **Java 標準庫零依賴** 的分布式基礎設施,理解其核心架構 (Stub/Skeleton、Registry、序列化、代碼下載) 對掌握分布式系統本質、排查微服務通訊問題、維護老舊系統具有不可替代的價值。本文完整演示 RMI 開發全流程:遠程接口、實現類、服務端啟動、客戶端調用、安全策略、常見錯誤排查。

一、RMI 核心架構組件

組件角色關鍵類/介面
遠程接口定義可遠程調用的方法簽名java.rmi.Remote 標記介面
遠程物件實現實現業務邏輯,繼承 UnicastRemoteObjectUnicastRemoteObject, RemoteServer
Stub (客戶端代理)本地代理,封裝網路通訊、序列化動態生成 (JDK 5+) 或 rmic 靜態生成
Registry (命名服務)綁定名稱→遠程物件引用,默認埠 1099LocateRegistry, Registry, Naming
序列化機制參數/返回值/異常跨 JVM 傳輸java.io.Serializable, Externalizable

二、完整開發步驟與代碼實現

2.1 遠程接口


import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface UserService extends Remote {
    // 所有方法必須拋出 RemoteException
    User getUserById(long id) throws RemoteException;
    List getAllUsers() throws RemoteException;
    boolean addUser(User user) throws RemoteException;
    boolean updateUser(User user) throws RemoteException;
    boolean deleteUser(long id) throws RemoteException;
}

2.2 可序列化資料物件


import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L; // 必須顯式聲明
    private long id;
    private String name;
    private int age;
    // 建構子、getter/setter、toString...
}

2.3 遠程物件實現


import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class UserServiceImpl extends UnicastRemoteObject implements UserService {

    private final Map users = new ConcurrentHashMap<>();
    private final AtomicLong idGen = new AtomicLong(1);

    // 建構子必須拋出 RemoteException
    public UserServiceImpl() throws RemoteException {
        super(); // 呼叫父類建構子,導出物件 (監聽匿名埠)
    }

    @Override
    public User getUserById(long id) throws RemoteException {
        return users.get(id);
    }

    @Override
    public List getAllUsers() throws RemoteException {
        return new ArrayList<>(users.values());
    }

    @Override
    public boolean addUser(User user) throws RemoteException {
        if (user.getId() == 0) user.setId(idGen.getAndIncrement());
        return users.put(user.getId(), user) == null;
    }

    @Override
    public boolean updateUser(User user) throws RemoteException {
        return users.replace(user.getId(), user) != null;
    }

    @Override
    public boolean deleteUser(long id) throws RemoteException {
        return users.remove(id) != null;
    }
}

2.4 服務端啟動類


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

public class RMIServer {
    public static void main(String[] args) {
        try {
            // 1. 建立並導出遠程物件
            UserService service = new UserServiceImpl();

            // 2. 啟動 Registry (嵌入式,同進程) - 埠 1099
            // 生產環境建議獨立進程: `rmiregistry 1099 &`
            Registry registry = LocateRegistry.createRegistry(1099);

            // 3. 綁定名稱 (URL 格式: rmi://host:port/name)
            String serviceName = "UserService";
            registry.rebind(serviceName, service); // rebind 覆蓋舊綁定
            // 或 Naming.rebind("rmi://localhost:1099/UserService", service);

            System.out.println("RMI 服務已啟動,綁定名稱: " + serviceName);
            System.out.println("等待客戶端調用...");

            // 4. 添加關閉鉤子優雅關閉
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                try { UnicastRemoteObject.unexportObject(service, true); } catch (Exception ignored) {}
                System.out.println("RMI 服務已關閉");
            }));

        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }
}

2.5 客戶端調用類


import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.Naming;
import java.util.List;

public class RMIClient {
    public static void main(String[] args) {
        String host = (args.length > 0) ? args[0] : "localhost";
        int port = 1099;
        String serviceName = "UserService";
        String url = "rmi://" + host + ":" + port + "/" + serviceName;

        try {
            // 1. 從 Registry 查找遠程物件 (返回 Stub 代理)
            Registry registry = LocateRegistry.getRegistry(host, port);
            UserService service = (UserService) registry.lookup(serviceName);
            // 或 UserService service = (UserService) Naming.lookup(url);

            System.out.println("成功獲取遠程服務代理: " + service.getClass().getName());

            // 2. 像本地呼叫一樣調用遠程方法
            service.addUser(new User(0, "張三", 25));
            service.addUser(new User(0, "李四", 30));
            
            System.out.println("所有用戶:");
            for (User u : service.getAllUsers()) {
                System.out.println("  " + u);
            }

            User u = service.getUserById(1);
            System.out.println("ID=1 用戶: " + u);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三、關鍵配置與部署細節

3.1 安全策略文件 (server.policy / client.policy)

RMI 預設啟用 SecurityManager,**必須授權** 代碼庫權限。
建立 server.policy:


grant codeBase "file:./bin/-" { // 或 jar 路徑
    permission java.security.AllPermission; // 開發測試用,生產最小權限
};

啟動參數:


java -Djava.security.policy=server.policy -Djava.rmi.server.hostname=192.168.1.100 RMIServer
java -Djava.security.policy=client.policy RMIClient

關鍵參數:java.rmi.server.hostname 指定 Stub 中嵌入的伺服器 IP (多網卡/NAT 環境必須設置,否則客戶端連不上)。

3.2 獨立 Registry 進程 (生產推薦)


# 終端 1: 啟動獨立 Registry
rmiregistry 1099 &

# 終端 2: 啟動 Server (不再 createRegistry)
java -Djava.rmi.server.hostname=... RMIServer

優點:Registry 崩潰不影響服務邏輯進程,便於集群管理。

3.3 代碼下載 (動態 Stub/類傳輸)

  • 客戶端若無 Stub 類/遠程接口/參數類定義,**需從代碼庫下載**。
  • Server 端設置:-Djava.rmi.server.codebase=https://serverhost:8080/classes/ (HTTP 服務托管編譯後的 .class/JAR)。
  • Client 端策略需授權 java.net.SocketPermission 連接代碼庫。
  • **JDK 5+ 動態 Stub 生成** (動態代理) 已大幅減少代碼下載需求,但參數/返回值類仍需雙方共享定義。

四、常見錯誤與排查清單

錯誤原因解決
ConnectException: Connection refusedRegistry 未啟動、埠錯誤、防火牆阻擋檢查 rmiregistry 進程、埠 1099、防火牆規則
ClassNotFoundException: UserService_Stub客戶端缺少 Stub/接口類、代碼下載失敗確保雙方 classpath 含共享接口/類、啟用 codebase 下載
UnmarshalException: ClassNotFound: User參數/返回值類 (User) 客戶端無定義共享通用模組 JAR (api-module) 給雙方
AccessControlExceptionSecurityManager 禁止操作配置 .policy 授權 AllPermission 或最小權限集
ConnectException to 127.0.0.1Stub 中嵌入 IP 為 127.0.0.1 (多網卡/容器)啟動 Server 設置 -Djava.rmi.server.hostname=真實IP
RemoteException: nested: java.io.NotSerializableException參數/返回值未實現 Serializable實現 Serializable、聲明 serialVersionUID

五、RMI 進階主題

  • 分散式垃圾回收 (DGC):RMI 運行時追蹤遠程引用計數,無引用時自動清理 (Lease 續租機制)。
  • RMI over SSL (RMI/SSL):javax.rmi.ssl.SslRMIClientSocketFactory + SslRMIServerSocketFactory,雙向認證加密傳輸。
  • 激活機制:rmid (Activation Daemon) 支援遠程物件按需啟動、持久化狀態。
  • 自定義 Socket Factory:實現 RMIClientSocketFactory/RMIServerSocketFactory 控制連接行為 (超時、壓縮、代理、SOCKS)。

六、現代替代方案對比

結語

Java RMI 以**遠程接口 + 序列化 + Stub 代理 + Registry 命名**構建了經典的分布式物件模型。掌握其開發全流程 (接口→實現→服務端→客戶端)、關鍵配置 (SecurityManager、hostname、codebase)、常見網路/序列化/安全錯誤排查,是 Java 後端工程師的基本功。在現代微服務架構中,RMI 多作為**內部模組通訊、測試存根、老系統整合**的輕量選擇;核心業務交互已遷移至 gRPC/Dubbo 等高性能、治理完善的 RPC 框架。但 **RMI 的設計思想 (位置透明、代理模式、序列化邊界、命名服務)** 深刻影響了後續所有 RPC 框架的架構演進。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
框架協定序列化服務發現優勢適用場景
RMIJRMP (Java 序列化)Java SerializationRegistry (手動)零依賴、標準庫、強類型內部系統、老舊維護、教學
gRPCHTTP/2ProtobufConsul/Etcd/K8s跨語言、高效能、IDL 契約、流式微服務、多語言、高併發
DubboDubbo/REST/gRPCHessian2/ProtobufZookeeper/Nacos生態豐富、治理完善、Spring 整合國內微服務主流選擇
Spring Cloud OpenFeignHTTP/RESTJackson JSONEureka/Nacos聲明式、Spring 原生、易上手Spring Boot 微服務