RMI(Remote Method Invocation)远程方法注入,用来实现远程方法调用,是实现分布式技术的一种方法。RMI提供了客户辅助对象和服务辅助对象,为客户辅助对象创建了和服务对象相同的方法。其好处在于我们不必亲自写任何网络或I/O代码。客户程序在调用远程方法时就和调用本地方法一样(感觉上)。
RMI分为客户端和服务端。
服务端的构建步骤如下:
- 扩展远程接口Remote(),使其符合项目需求
- 实现被扩展的接口
- 为服务对象绑定唯一标识
- 利用rmic产生stub(客户辅助对象)和skeleton(服务辅助对象,高版本的Java不需要显示的skeleton对象,但服务端仍然有一些东西负责skeleton的行为)
- 启动rmireistry(rmireistry类似一个注册表,客户可以从中找到代理的位置)
- 启动远程服务
客户端的构建步骤如下:
- 根据注册的服务名查找服务得到实例的引用
- 调用服务的相关方法
通过一个具体的例子来说明:
假设现在有一个自动售货机公司(销售的主要产品为糖果),为了能尽可能的扩大受益,公司需要知道每个自动售货机的运行状态,从而在售货机中的糖果卖完时,可以及时的添加;如果哪台售货机坏掉了,可以及时的维修。但是售货机分布在一个城市的各个角落,如果用人工去一个个检查,效率十分低下。所以公司希望能有一个监控系统,这样就可以一个人坐在办公室,点点鼠标,就一切尽掌握啦~
这样一个系统的最大的特殊点在于我们都是远程监控这些自动售货机,所以我们需要用到RMI。
服务端程序:
第一步、我们先定义一个对象,这个对象用来存储自动售货机的状态信息:自动售货机的地理位置、运行状态和剩余糖果数。远程方法的变量必须被打包并通过网络传输,这就需要通过序列化来完成。一般的原语类型、字符串和许多API中的内定类型(包括数据和集合),都不会存在问题,但是如果是自己定义的类,就需要实现Serializable接口。具体代码如下:
等等,还要再这里插入一小段程序:枚举。用于定义机器的运行状态,具体代码如下:
1 package pattern.proxy; 2 3 /** 4 * 机器的运行状态 5 * RUNNING:正常运行 6 * BREAKDOWN:坏掉了 7 * REPAIR:维修中 8 * @author CS_Xiaochao 9 * 10 */ 11 public enum MachineState { 12 RUNNING, BREAKDOWN, REPAIR 13 }
1 package pattern.proxy; 2 3 import java.io.Serializable; 4 5 6 /** 7 * 糖果机对象 8 * 实现Serializable接口,因为需要在网络中进行传输 9 * @author CS_Xiaochao 10 * 11 */ 12 public class GumballMachineBean implements Serializable{ 13 14 /** 15 * 16 */ 17 private static final long serialVersionUID = -6395522198769463162L; 18 private String localtion; // 糖果机的位置 19 private int count; // 糖果的剩余数量 20 private MachineState state; // 机器的运行状态 21 22 public GumballMachineBean() { 23 super(); 24 // TODO Auto-generated constructor stub 25 } 26 27 public GumballMachineBean(String localtion, int count, MachineState state) { 28 super(); 29 this.localtion = localtion; 30 this.count = count; 31 this.state = state; 32 } 33 34 public String getLocaltion() { 35 return localtion; 36 } 37 38 public void setLocaltion(String localtion) { 39 this.localtion = localtion; 40 } 41 42 public int getCount() { 43 return count; 44 } 45 46 public void setCount(int count) { 47 this.count = count; 48 } 49 50 public MachineState getState() { 51 return state; 52 } 53 54 public void setState(MachineState state) { 55 this.state = state; 56 } 57 58 }
第二步、扩展Remote接口,Remote接口中未声明任何方法,只是一个“标记”。所以我们需要扩展该接口来符合自己的项目需求。客户使用远程接口调用服务,也就是说客户会调用实现远程接口的Stub上的方法,而Stub底层用到了网络和I/O,所以存在一定的风险性,必须通过处理或声明远程异常来解决。具体源码如下:
1 package pattern.proxy; 2 3 import java.rmi.Remote; 4 import java.rmi.RemoteException; 5 6 /** 7 * 扩展Remote接口,以适应糖果机 8 * @author CS_Xiaochao 9 * 10 */ 11 public interface GumballMachineMonitor extends Remote { 12 /**
* 获取糖果机状态信息 13 * 网络中存在许多不可测的因素,所以必须对其进行异常控制 14 * @return 15 * @throws RemoteException 16 */ 17 GumballMachineBean getGumballMachineBean() throws RemoteException; 18 19 }
第三步、实现远程扩展的接口。在这里我另开了一个线程用于糖果机的售卖糖果的动作,在实际中糖果机在运行时,销售糖果和上传状态信息是两个独立的动作,所以用两个线程来分别负责比较合理。糖果贩卖源码如下:
1 package pattern.proxy; 2 3 /** 4 * 糖果销售动作对象 5 * @author CS_Xiaochao 6 * 7 */ 8 public class SellCandy implements Runnable { 9 10 private GumballMachineBean gumballMachine; 11 12 public SellCandy() { 13 // TODO Auto-generated constructor stub 14 } 15 16 public SellCandy(GumballMachineBean gumballMachine) { 17 this.gumballMachine = gumballMachine; 18 } 19 20 @Override 21 public void run() { 22 while(true){ 23 if(gumballMachine.getState() == MachineState.RUNNING){ 24 int countTemp = gumballMachine.getCount(); 25 if(countTemp > 0){ 26 --countTemp; //每次卖出一颗糖 27 } 28 gumballMachine.setCount(countTemp); 29 }else{ 30 break; //说明糖果机坏掉,停止运作 31 } 32 //每隔两秒采集一次数据 33 try { 34 Thread.sleep(2000); 35 } catch (InterruptedException e) { 36 // TODO Auto-generated catch block 37 e.printStackTrace(); 38 } 39 } 40 41 } 42 43 }
扩展接口实现类代码如下:
1 package pattern.proxy; 2 3 import java.net.MalformedURLException; 4 import java.rmi.Naming; 5 import java.rmi.RemoteException; 6 import java.rmi.server.UnicastRemoteObject; 7 8 public class GumballMachineMonitorImpl extends UnicastRemoteObject implements 9 GumballMachineMonitor { 10 11 /** 12 * 13 */ 14 private static final long serialVersionUID = -6922251119304810189L; 15 16 private GumballMachineBean gumballMachine; 17 18 /** 19 * 因为父类UnicastRemoteObject的构造方法抛出了RemoteException异常 20 * 所以子类必须手动定义构造方法并抛出该异常,以防止异常链断掉 21 * @throws RemoteException 22 */ 23 protected GumballMachineMonitorImpl(GumballMachineBean gumballMachine) throws RemoteException { 24 super(); 25 this.gumballMachine = gumballMachine; 26 } 27 28 @Override 29 public GumballMachineBean getGumballMachineBean() throws RemoteException { 30 return gumballMachine; //获取糖果机状态对象 31 } 32 33 /** 34 * 驱动函数 35 * @param args 36 */ 37 public static void main(String[] args) { 38 39 try { 40 GumballMachineBean gmb = 41 new GumballMachineBean("湖北省武汉市武昌区八一路武汉大学文理学部自强超市旁", 1000, MachineState.RUNNING); 42 new Thread(new SellCandy(gmb)).start(); //开始销售 43 GumballMachineMonitorImpl gmm1 = new GumballMachineMonitorImpl(gmb); 44 Naming.rebind("GumballMachineMonitor_1", gmm1); 45 46 GumballMachineBean gmb2 = 47 new GumballMachineBean("湖北省武汉市武昌区八一路武汉大学文理学部计算机学院软件工程国家重点实验室旁", 1000, MachineState.BREAKDOWN); 48 new Thread(new SellCandy(gmb2)).start(); //开始销售 49 GumballMachineMonitorImpl gmm2 = new GumballMachineMonitorImpl(gmb2); 50 Naming.rebind("GumballMachineMonitor_2", gmm2); 51 52 GumballMachineBean gmb3 = 53 new GumballMachineBean("湖北省武汉市武昌区八一路武汉大学文理学部樱花大道旁", 1000, MachineState.REPAIR); 54 new Thread(new SellCandy(gmb3)).start(); //开始销售 55 GumballMachineMonitorImpl gmm3 = new GumballMachineMonitorImpl(gmb3); 56 Naming.rebind("GumballMachineMonitor_3", gmm3); 57 58 } catch (RemoteException e) { 59 // TODO Auto-generated catch block 60 e.printStackTrace(); 61 } catch (MalformedURLException e) { 62 // TODO Auto-generated catch block 63 e.printStackTrace(); 64 } 65 66 } 67 68 }
为了让自己的对象成为远程服务对象,需要让对象具备一些远程的功能,所以要继承UnicastRemoteObject类,由于UnicastRemoteObject类的构造方法抛出了RemoteException异常,为了不让异常链断裂,导出类的构造方法也必须抛出该异常,而这需要手动的去实现。getGumballMachineBean方法用于获取糖果机的状态。在驱动类中,我们创建了三个糖果机,分别将其状态设为:RUNNING、BREAKDOWN和REPAIR。然后也给每个糖果机添加了一些糖果,并“摆放”在三个不同的地方,然后启动线程开始销售。
Naming.rebind("GumballMachineMonitor_1", gmm1);
用于将当前服务对象注册到RMI registry的注册表中,因为客户端需要在注册表中查找来获取该远程对象,从而可以达到像操作本地对象一样操作远程对象的目的。
第四步、通过rmic工具来产生Stub和Skeleton。rmic是JDK内的一个小工具,使用方法:通过命令行进入实现类class文件所在路径,执行命令“rmic 类全名(包名+类名,不需要‘.class’)”。执行完成之后,在类所在路径下会生成两个文件(类名+‘_Stub.class’ 和 类名+‘_Skeleton.class’),高版本的jdk一般只有"类名+‘_Stub.class’ "文件。
第五步、执行rmiregistry。运用命令行,进入和上一步相同的路径下,执行命令"rmiregistry"。
第六步、新开一个命令行窗口,执行驱动程序来启动服务。
客服端程序:
客户端程序(在本例中,也即是客户需要使用的监控窗口程序)需要查找RMI的注册表来获取远程服务对象所在地址,并获取到该服务对象,然后通过该对象来调用相关方法,对于客户而言,这一系列的操作就如同在操作本地服务对象一样,是透明的。唯一能感受到的就是网络的不可预测性导致的异常。
第一步、根据注册的服务名查找服务得到实例的引用
第二步、通过获取到的远程服务对象来调用相关的方法
相关代码如下:
1 package pattern.proxy; 2 3 import java.net.MalformedURLException; 4 import java.rmi.Naming; 5 import java.rmi.NotBoundException; 6 import java.rmi.RemoteException; 7 8 public class ClientManageSystem { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 try { 15 GumballMachineMonitor gmm1 = (GumballMachineMonitor) Naming.lookup("rmi://127.0.0.1/GumballMachineMonitor_1"); 16 GumballMachineMonitor gmm2 = (GumballMachineMonitor) Naming.lookup("rmi://127.0.0.1/GumballMachineMonitor_2"); 17 GumballMachineMonitor gmm3 = (GumballMachineMonitor) Naming.lookup("rmi://127.0.0.1/GumballMachineMonitor_3"); 18 19 while(true){ 20 System.out.println("GM_1_Location:" + gmm1.getGumballMachineBean().getLocaltion() + "\n" + 21 "GM_1_State:" + gmm1.getGumballMachineBean().getState() + "\n" + 22 "GM_1_Count:" + gmm1.getGumballMachineBean().getCount()); 23 24 System.out.println("GM_2_Location:" + gmm2.getGumballMachineBean().getLocaltion() + "\n" + 25 "GM_2_State:" + gmm2.getGumballMachineBean().getState() + "\n" + 26 "GM_2_Count:" + gmm2.getGumballMachineBean().getCount()); 27 28 System.out.println("GM_3_Location:" + gmm3.getGumballMachineBean().getLocaltion() + "\n" + 29 "GM_3_State:" + gmm3.getGumballMachineBean().getState() + "\n" + 30 "GM_3_Count:" + gmm3.getGumballMachineBean().getCount()); 31 32 //每隔五秒监控一次 33 Thread.sleep(5000); 34 } 35 36 } catch (MalformedURLException e) { 37 // TODO Auto-generated catch block 38 e.printStackTrace(); 39 } catch (RemoteException e) { 40 // TODO Auto-generated catch block 41 e.printStackTrace(); 42 } catch (NotBoundException e) { 43 // TODO Auto-generated catch block 44 e.printStackTrace(); 45 } catch (InterruptedException e) { 46 // TODO Auto-generated catch block 47 e.printStackTrace(); 48 } 49 50 51 52 } 53 54 }
GumballMachineMonitor gmm1 = (GumballMachineMonitor) Naming.lookup("rmi://127.0.0.1/GumballMachineMonitor_1");
客户端通过lookup方法到RMI registry中查找远程服务对象,RMI registry返回的是Stub对象,然后对stub进行反序列化。
在使用RMI的过程中,最易犯的三个错误:(摘自Head First 设计模式)
- 忘了在启动远程服务之前先启动rmiregistry(要用Naming.rebind()注册服务,rmiregistry必须是运行的)。
- 忘了让变量和返回值的类型成为可序列化的类型(这种错误无法在编译期发现,只会在运行时发现)
- 忘了给客户提供stub类