Java RMI (Remote Method Invocation) 讓執行在不同 JVM (甚至不同機器) 的物件能像本地呼叫一樣透明地調用遠程物件方法。雖然 gRPC、Dubbo、Spring Cloud 等現代 RPC 框架功能更強,但 RMI 作為 **Java 標準庫零依賴** 的分布式基礎設施,理解其核心架構 (Stub/Skeleton、Registry、序列化、代碼下載) 對掌握分布式系統本質、排查微服務通訊問題、維護老舊系統具有不可替代的價值。本文完整演示 RMI 開發全流程:遠程接口、實現類、服務端啟動、客戶端調用、安全策略、常見錯誤排查。
| 組件 | 角色 | 關鍵類/介面 |
|---|---|---|
| 遠程接口 | 定義可遠程調用的方法簽名 | java.rmi.Remote 標記介面 |
| 遠程物件實現 | 實現業務邏輯,繼承 UnicastRemoteObject | UnicastRemoteObject, RemoteServer |
| Stub (客戶端代理) | 本地代理,封裝網路通訊、序列化 | 動態生成 (JDK 5+) 或 rmic 靜態生成 |
| Registry (命名服務) | 綁定名稱→遠程物件引用,默認埠 1099 | LocateRegistry, Registry, Naming |
| 序列化機制 | 參數/返回值/異常跨 JVM 傳輸 | java.io.Serializable, Externalizable |
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;
}
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...
}
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;
}
}
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);
}
}
}
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();
}
}
}
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 環境必須設置,否則客戶端連不上)。
# 終端 1: 啟動獨立 Registry
rmiregistry 1099 &
# 終端 2: 啟動 Server (不再 createRegistry)
java -Djava.rmi.server.hostname=... RMIServer
優點:Registry 崩潰不影響服務邏輯進程,便於集群管理。
-Djava.rmi.server.codebase=https://serverhost:8080/classes/ (HTTP 服務托管編譯後的 .class/JAR)。java.net.SocketPermission 連接代碼庫。| 錯誤 | 原因 | 解決 |
|---|---|---|
ConnectException: Connection refused | Registry 未啟動、埠錯誤、防火牆阻擋 | 檢查 rmiregistry 進程、埠 1099、防火牆規則 |
ClassNotFoundException: UserService_Stub | 客戶端缺少 Stub/接口類、代碼下載失敗 | 確保雙方 classpath 含共享接口/類、啟用 codebase 下載 |
UnmarshalException: ClassNotFound: User | 參數/返回值類 (User) 客戶端無定義 | 共享通用模組 JAR (api-module) 給雙方 |
AccessControlException | SecurityManager 禁止操作 | 配置 .policy 授權 AllPermission 或最小權限集 |
ConnectException to 127.0.0.1 | Stub 中嵌入 IP 為 127.0.0.1 (多網卡/容器) | 啟動 Server 設置 -Djava.rmi.server.hostname=真實IP |
RemoteException: nested: java.io.NotSerializableException | 參數/返回值未實現 Serializable | 實現 Serializable、聲明 serialVersionUID |
javax.rmi.ssl.SslRMIClientSocketFactory + SslRMIServerSocketFactory,雙向認證加密傳輸。rmid (Activation Daemon) 支援遠程物件按需啟動、持久化狀態。RMIClientSocketFactory/RMIServerSocketFactory 控制連接行為 (超時、壓縮、代理、SOCKS)。| 框架 | 協定 | 序列化 | 服務發現 | 優勢 | 適用場景 |
|---|---|---|---|---|---|
| RMI | JRMP (Java 序列化) | Java Serialization | Registry (手動) | 零依賴、標準庫、強類型 | 內部系統、老舊維護、教學 |
| gRPC | HTTP/2 | Protobuf | Consul/Etcd/K8s | 跨語言、高效能、IDL 契約、流式 | 微服務、多語言、高併發 |
| Dubbo | Dubbo/REST/gRPC | Hessian2/Protobuf | Zookeeper/Nacos | 生態豐富、治理完善、Spring 整合 | 國內微服務主流選擇 |
| Spring Cloud OpenFeign | HTTP/REST | Jackson JSON | Eureka/Nacos | 聲明式、Spring 原生、易上手 | Spring Boot 微服務 |