代理模式——遠(yuǎn)程代理(Java RMI)
@(設(shè)計(jì)模式)
一楷力、遠(yuǎn)程代理——大使
在日常開(kāi)發(fā)中喊式,我們經(jīng)常會(huì)有本地服務(wù)完成不了的功能,需要訪(fǎng)問(wèn)遠(yuǎn)程服務(wù)中的對(duì)象(以下稱(chēng)遠(yuǎn)程對(duì)象)萧朝。當(dāng)然岔留,不可能用下面的代碼來(lái)引用遠(yuǎn)程對(duì)象,以獲取其方法:
Ref ref = <另一個(gè)JVM堆的對(duì)象>
1. 從實(shí)踐中歸納
從非面向?qū)ο蠼嵌燃旒恚梢悦枋鰹楸镜胤?wù)通過(guò)網(wǎng)絡(luò)請(qǐng)求遠(yuǎn)程服務(wù)献联。為了實(shí)現(xiàn)本地到遠(yuǎn)程的通信,我們需要實(shí)現(xiàn)網(wǎng)絡(luò)通信何址,處理其中可能的異常里逆。為良好的代碼設(shè)計(jì)和可維護(hù)性,我們將網(wǎng)絡(luò)通信部分隱藏起來(lái)用爪,只暴露給本地服務(wù)一個(gè)接口原押,通過(guò)該接口即可訪(fǎng)問(wèn)遠(yuǎn)程服務(wù)提供的功能,而不必過(guò)多關(guān)心通信部分的細(xì)節(jié)偎血。正如世上本沒(méi)有設(shè)計(jì)模式诸衔,設(shè)計(jì)模式都是從實(shí)踐中總結(jié)出來(lái)的良好設(shè)計(jì)方式,我們也將以上的總結(jié)為遠(yuǎn)程代理颇玷。最后笨农,我們良好設(shè)計(jì)的通信方式可能如下:
通信時(shí)序如下:
2. 面向?qū)ο笏伎?/h5>
從非面向?qū)ο蠼嵌龋覀冴P(guān)注本地服務(wù)如何獲得遠(yuǎn)程服務(wù)的功能帖渠,而從面相對(duì)象角度谒亦,我們關(guān)注本地服務(wù)如何獲得遠(yuǎn)程對(duì)象,從而獲得遠(yuǎn)程服務(wù)提供的功能阿弃。這樣诊霹,本地對(duì)象具有與遠(yuǎn)程對(duì)象完全相同的行為,但是本地的行為是通過(guò)遠(yuǎn)程對(duì)象提供的渣淳,細(xì)細(xì)思考脾还,這就是代理模式了。不同的是代理類(lèi)和真實(shí)主題類(lèi)不在同一個(gè)JVM堆中入愧,但其類(lèi)之間的關(guān)系還是與代理模式相同的:
3.大使
遠(yuǎn)程代理為一個(gè)位于不同的地址空間的對(duì)象提供一個(gè)局域代表對(duì)象鄙漏,這個(gè)不同的地址空間可以是在本機(jī)器中嗤谚,也可以是在另一臺(tái)機(jī)器中,遠(yuǎn)程代理的角色怔蚌,就像一個(gè)國(guó)家在另一個(gè)國(guó)家的大使一樣巩步,重大問(wèn)題還是要跟國(guó)內(nèi)溝通。
二桦踊、Java RMI實(shí)現(xiàn)遠(yuǎn)程代理
現(xiàn)在椅野,實(shí)現(xiàn)遠(yuǎn)程代理,我們需要實(shí)現(xiàn)網(wǎng)絡(luò)通信籍胯,封裝細(xì)節(jié)竟闪,處理異常,程序員都是懶惰而聰明的杖狼,Java已經(jīng)提供了一套成熟的方案來(lái)實(shí)現(xiàn)遠(yuǎn)程代理炼蛤。
利用Java RMI實(shí)現(xiàn)遠(yuǎn)程代理,需要分別實(shí)現(xiàn)客戶(hù)端與服務(wù)端
1. 服務(wù)端
實(shí)現(xiàn)遠(yuǎn)程代理服務(wù)端蝶涩,需要
- 制作遠(yuǎn)程接口理朋,即主題接口
遠(yuǎn)程接口需要實(shí)現(xiàn)java.rmi.Remote
類(lèi),該標(biāo)記接口表示其方法可能從非本地虛擬機(jī)調(diào)用绿聘;
遠(yuǎn)程接口所有方法需聲明java.rmi.RemoteException
異常嗽上,涉及網(wǎng)絡(luò)通信,方法調(diào)用可能產(chǎn)生異常斜友;
遠(yuǎn)程接口方法變量返回值都是原語(yǔ)或者可序列化類(lèi)型炸裆,因?yàn)榉祷刂岛妥兞靠赡苄枰h(yuǎn)程傳輸。 - 實(shí)現(xiàn)遠(yuǎn)程接口鲜屏,即真正主題類(lèi)
繼承java.rmi.server.UnicastRemoteObject
類(lèi)烹看,該類(lèi)提供遠(yuǎn)程對(duì)象 - 啟動(dòng)本地RMI registry
可以使用JDK自帶的rmiregistry
命令啟動(dòng),也可以使用java.rmi.registry.LocateRegistry
提供的createRegistry
方法啟動(dòng) - 注冊(cè)遠(yuǎn)程服務(wù)
使用JNDI注冊(cè)服務(wù)
最后遠(yuǎn)程服務(wù)的類(lèi)圖如下:
代碼清單
// 遠(yuǎn)程接口
public interface IMyRemote extends Remote {
String sayHello() throws RemoteException;
}
// 遠(yuǎn)程接口實(shí)現(xiàn)-遠(yuǎn)程對(duì)象
public class MyRemoteImpl extends UnicastRemoteObject implements IMyRemote {
public MyRemoteImpl() throws RemoteException {
super();
}
@Override
public String sayHello() throws RemoteException {
return "Server says, 'Hey'";
}
public static void main(String[] args) throws RemoteException, MalformedURLException {
IMyRemote service = new MyRemoteImpl();
// 啟動(dòng)本地rmi registry洛史,默認(rèn)端口1099
LocateRegistry.createRegistry(1099);
// 注冊(cè)遠(yuǎn)程對(duì)象
Naming.rebind("rmi://localhost:1099/RemoteHello", service);
}
}
2.客戶(hù)端
對(duì)于客戶(hù)端來(lái)說(shuō)惯殊,只需要使用JNDI服務(wù)找到遠(yuǎn)程對(duì)象,使用即可也殖。代碼如下:
public class MyRemoteClient {
private void go() throws RemoteException, NotBoundException, MalformedURLException {
IMyRemote service = (IMyRemote) Naming.lookup("rmi://localhost/RemoteHello");
System.out.println(service.sayHello());
}
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
new MyRemoteClient().go();
}
}
首先啟動(dòng)服務(wù)端土思,再啟動(dòng)客戶(hù)端,就可以看到來(lái)自遠(yuǎn)程對(duì)象的問(wèn)候了忆嗜。
三己儒、深入Java RMI
Java RMI固然為我們實(shí)現(xiàn)遠(yuǎn)程對(duì)象提供了簡(jiǎn)便的方式,深入了解其實(shí)現(xiàn)細(xì)節(jié)也會(huì)讓我們受益匪淺捆毫。
1. 一切從通信開(kāi)始
不管怎么封裝闪湾,本地服務(wù)要想獲得遠(yuǎn)程對(duì)象,調(diào)用遠(yuǎn)程服務(wù)的方法绩卤,必然要經(jīng)過(guò)網(wǎng)絡(luò)通信途样。Java RMI為我們處理了這些細(xì)節(jié)江醇,現(xiàn)在我們要還原這些細(xì)節(jié)一窺RMI實(shí)現(xiàn)。一般地何暇,其中涉及了socket編程與TCP通信:
- 服務(wù)端監(jiān)聽(tīng)某個(gè)端口
- 客戶(hù)端知道服務(wù)端地址陶夜,并向服務(wù)端監(jiān)聽(tīng)的端口發(fā)請(qǐng)求
這樣的通訊方式引入了一些問(wèn)題:
- 誰(shuí)處理服務(wù)端socket相關(guān)細(xì)節(jié)
- 誰(shuí)處理客戶(hù)端socket相關(guān)細(xì)節(jié)
- 客戶(hù)端如何知道服務(wù)端地址,以及服務(wù)端監(jiān)聽(tīng)哪一個(gè)端口
為解決以上三個(gè)問(wèn)題裆站,RMI引入如下概念
- 誰(shuí)處理服務(wù)端socket相關(guān)細(xì)節(jié) ——skeleton
骨架skelton接收來(lái)自存根的TCP請(qǐng)求条辟,隱藏通信細(xì)節(jié),將真正的調(diào)用交給遠(yuǎn)程對(duì)象宏胯,并負(fù)責(zé)組織返回捂贿,解耦服務(wù)與網(wǎng)絡(luò)通信; - 誰(shuí)處理客戶(hù)端socket相關(guān)細(xì)節(jié) ——stub
存根stub接收來(lái)自客戶(hù)的請(qǐng)求胳嘲,組織參數(shù)通過(guò)TCP發(fā)送到服務(wù)端骨架,并接收組織返回給客戶(hù)扣草,解耦客戶(hù)與網(wǎng)絡(luò)通信了牛; - 客戶(hù)端如何知道服務(wù)端地址,以及服務(wù)端監(jiān)聽(tīng)哪一個(gè)端口 ——rmi registry
服務(wù)端遠(yuǎn)程對(duì)象注冊(cè)到rmi registry辰妙,客戶(hù)端通過(guò)位置總所周知的rmi registry獲取存根stub鹰祸。rmi registry是一個(gè)獨(dú)立的服務(wù),只有在registry服務(wù)啟動(dòng)后密浑,才能將遠(yuǎn)程服務(wù)注冊(cè)(綁定)到registry中蛙婴。
這樣,遠(yuǎn)程對(duì)象的全部時(shí)序細(xì)節(jié)應(yīng)該如下尔破,其中Program
是服務(wù)端負(fù)責(zé)啟動(dòng)的類(lèi)街图,RMI registry與服務(wù)端在同一個(gè)JVM中。
2. 代碼細(xì)節(jié)
// TODO: java rmi的代碼細(xì)節(jié)
四懒构、分布式與RMI
rmi registry是一個(gè)不同于rmi服務(wù)端的獨(dú)立服務(wù)(這也是要先啟動(dòng)rmi registry才能綁定rmi遠(yuǎn)程服務(wù)的原因)餐济。由于將rmi服務(wù)綁定到RMI registry時(shí),registry使用自己的類(lèi)加載器加載服務(wù)類(lèi)胆剧,因此registry必須能夠訪(fǎng)問(wèn)到服務(wù)類(lèi)的classpath絮姆,那么registry必須和rmi服務(wù)在同一臺(tái)機(jī)器上了。這就意味這rmi 服務(wù)不能注冊(cè)到遠(yuǎn)程機(jī)器的rmi registry上秩霍,不能依靠rmi registry解決分布式中rmi服務(wù)的單點(diǎn)故障篙悯。
成熟的方案是集成zookeeper,將分布式部署的rmi服務(wù)的rmi registry地址注冊(cè)到zk某個(gè)節(jié)點(diǎn)铃绒,rmi客戶(hù)端訪(fǎng)問(wèn)該節(jié)點(diǎn)獲取rmi registry地址鸽照,采用合適的負(fù)載策略選擇具體rmi registry,最后通過(guò)rmi registry訪(fǎng)問(wèn)到具體rmi服務(wù)匿垄。那么整個(gè)過(guò)程如下:
rmi registry -> zk node -> rmi registry list -> Load Balancing Strategy -> specific rmi registry -> rmi server
refer to
[1] 深入理解rmi原理
[2] Java RMI遠(yuǎn)程方法調(diào)用詳解
[3] RMI注冊(cè)表
[4] RMI(遠(yuǎn)程方法調(diào)用)
[5] 使用 RMI + ZooKeeper 實(shí)現(xiàn)遠(yuǎn)程調(diào)用框架