【转自:http://blog.csdn.net/lethorld/article/details/7089374#】
1.RMI和Java Remote Method Invocation
在讨论RMI之前,我们先看看网站的架构。典型的网站有一个服务提供商(Web Server),多个服务使用者(Web Client)。网站使用的是浏览器(Browser)作为客户端,也就是所说的BS架构(Browser-Server)。还有一种不是使用浏览器作为客户端的,比如说即时聊天工具(QQ、MSN)等,它们需要你在本地安装相应的客户端程序后才能运行,这就是所说的CS架构(Client-Server)。CS架构和BS架构相比较,BS的优势在于它能够有很好的移植和兼容性(只要你本地安装了浏览器,原则上你就能使用我的系统);而对于CS架构,个人觉得它的优势在于可操作性(客户端完全由自己开发,可以想怎么写就怎么写,完全不需要受到浏览器的约束)。
现在我们说说所谓的RMI,RMI全称为Remote Method Invocation,中文翻译为远程方法调用。指的是从本地调用服务器上的一组方法,服务器根据提交的信息(方法名、方法携带参数)计算得到结果、并将结果返回给调用者。Java Remote Method Invocation指的是在Java上实现上述机制。RMI可以理解成一种思想,一种架构,而JRMI则是这种思想或者架构的Java版本的实现。
好的机制需要屏蔽底层的实现细节。对于RMI的实现,需要做到:调用远程对象(Remote Object)和调用本地对象(Local Object)对于调用者来说没有任何区别。基于这个出发点,我们看看本地调用如何实现。
2.1 调用本地对象
首先我们需要定义一个Java类,该类有一个方法localCall。然后,我们写一个类用于调用这个本地对象。package com.ztesoft.provisioning.local; /** * 本地对象,用于被调用 * @author Lethorld * */ public class LocalObject { /** * 本地调用,用于测试 * @param arg 入参 * @return 返回 */ public String localCall(String arg) { // TODO : do something. return arg; } }
这样我们便完成了本地的一次对象调用。我们的运行main函数后,主程序会new一个LocalObject对象,并且调用该对象的localCall方法。localCall方法根据入参"Hello",返回相应的返回值。这里面没有做处理,所以返回的是Hello。package com.ztesoft.provisioning.local; /** * 本地调用者 * @author Lethorld * */ public class LocalInvoker { /** * 主函数 * @param args 入参 */ public static void main(String[] args) { LocalObject object = new LocalObject(); String retn = object.localCall("Hello"); System.out.println(retn); } }
OK,这段代码应该是所有只要稍微懂一点Java的人都能够看明白,并且很轻松地写出来的。那么,对于Java的远程调用,也就是上面所说的RMI,是否可以简单的把所有的local改成remote就OK了呢?答案是否定的。2.2 定义与实现分离
要实现远程调用,首先必须考虑的是如何将方法的定义和方法的实现分离开。如果方法定义与实现不能相互分离,那么我们怎么能够将方法的实现放到服务器端,而仅仅在客户端上保存一个方法的声明呢?对于Java,要完成将定义与实现相分离的任务,我们自然而然地想到了接口(interface)这个工具。请看百度百科中对于Java接口的描述:“Java中的接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。”你看,这难道不就是我们想要的所谓的定义与实现相分离的特性吗?好吧,下面我们用接口来重写上面的例子。首先我们定以接口,接口的定义这里不赘述,也不属于本文准备详细阐述的内容。这个接口的名字叫ILocalObject,它有一个方法的声明叫localCall。注意localCall只是一个声明,它没有显示它将会如何实现localCall这个操作。然后我们定义一个类LocalObjectImpl,该类实现了接口ILocalObject。方法localCall和2.1中介绍的完全一样。package com.ztesoft.provisioning.local; /** * 本地调用接口 * @author Lethorld * */ public interface ILocalObject { /** * 本地调用,用于测试 * @param arg 入参 * @return 返回 */ public String localCall(String arg); }
- package com.ztesoft.provisioning.local.impl;
- import com.ztesoft.provisioning.local.ILocalObject;
- /**
- * 接口的实现类
- * @author Lethorld
- *
- */
- public class LocalObjectImpl implements ILocalObject {
- public String localCall(String arg) {
- // TODO : do something.
- return arg;
- }
- }
现在我们要改一下我们的主函数,用接口的方式完成对方法的调用。下面代码的17行,我们定义了一个接口对象object,它指向一个实现了该接口的类LocalObjectImpl的实例。package com.ztesoft.provisioning.local.impl; import com.ztesoft.provisioning.local.ILocalObject; /** * 接口的实现类 * @author Lethorld * */ public class LocalObjectImpl implements ILocalObject { public String localCall(String arg) { // TODO : do something. return arg; } }
package com.ztesoft.provisioning.local; import com.ztesoft.provisioning.local.impl.LocalObjectImpl; /** * 本地调用者 * @author Lethorld * */ public class LocalInvoker { /** * 主函数 * @param args 入参 */ public static void main(String[] args) { ILocalObject object = new LocalObjectImpl(); String retn = object.localCall("Hello"); System.out.println(retn); } }
2.3 万事俱备!让我们开始向RMI进发。
有了2.1和2.2的铺垫,现在我们可以尝试写一个由JRMI实现的远程调用的程序了。首先,我们定义一个接口,该接口要继承自Remote接口。Remote接口在jdk中的解释为:“用于标识其方法可以从非本地虚拟机上调用的接口。”其实原理很简单,我们要使用远程调用的特性,那么注定我们的类和简单的本地类之间的处理是不完全一样的。比如说:怎么远程通信?怎么序列化(反序列化)入参、返回值?对于这些差异的处理,要么我们用户自己实现,要么丢给RMI,由RMI框架帮我们实现。当然,作为用户,我们不希望每次写个远程调用都去处理这些共性的东西。于是,RMI框架抽象出来一个Remote接口。这样,对于Java程序来说,凡是遇到继承自Remote的接口,Java编译器就知道这个类可能会被远程调用了。其二,所有的远程调用的接口的方法声明中,必须含有RemoteException的声明。从JDK文档中可以看出来,RemoteException是继承自IOException。这是什么意思呢?因为我们远程调用是通过网络实现的,所以对于服务器上的异常,客户端可以统一认为是IO读写异常。这也就不难理解为什么RemoteException会被定义成IOException的子类了。这里的处理个人理解可能有点泛化的意味在里面。远程调用,在服务器上(或者在通讯过程中)可能会发生异常的错误,这点所有人都可以保证。而反过来说,没有人可以保证远程调用,在服务器上(或者在通讯过程中)永远不发生错误。既然这样,那我们就规定:所有继承Remote的接口的方法声明中,必须含有RemoteException的声明。下面是一个继承自Remote接口的接口,接口取名为IRemoteTest。IRemoteTest内部有个remoteTest的方法的声明。这个方法接受一个String类型的入参,返回值的类型也是String的。同时,它需要申明可能抛出RemoteException异常。接口定义好了,我们可以先把客户端的调用程序写完(接口存在的好处之一就是在不知道方法如何实现的前提下,我们同样可以完成对该方法调用的后续操作)。对于2.2中的调用,我们用一句package com.ztesoft.provisioning.rmiserver; import java.rmi.Remote; import java.rmi.RemoteException; /** * 远程调用RMI,定义的接口 * @author Lethorld * Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。 * 任何远程对象都必须直接或间接实现此接口。只有在“远程接口” *(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。 * 实现类可以实现任意数量的远程接口,并且可以扩展其他远程实现类。 * RMI 提供一些远程对象实现可以扩展的有用类,这些类便于远程对象创建。 * 这些类是 java.rmi.server.UnicastRemoteObject * 和 java.rmi.activation.Activatable。 */ public interface IRemoteTest extends Remote{ public String remoteTest(String name) throws RemoteException; }
便可以实现创建一个LocalObjectImpl对象,并把这个对象赋给ILocalObject接口的任务。但是远程调用不行,因为你无法确切地知道远程服务器上的对象的名称(它并不是你本地的某个java类,它不受你的管束)。我们需要一个新的机制可以帮助我们获得远程对象的引用。这部分的工作有类Naming类实现。JDK中对该类的描述为:“ILocalObject object = new LocalObjectImpl();
Naming
类提供在对象注册表中存储和获得远程对远程对象引用的方法。”那么如何去定位一个远程对象呢?换句话说,我们如何唯一标识一个远程对象?让我们回到通常的WEB程序来看看究竟该如何处理。通常,我们要访问某一个网站,我们会在浏览器中输入网址。比如我们要访问csdn网站,我们可以输入http://www.csdn.net,点击确定,于是我们就可以浏览csdn的主页了。同样,我们还可以输入http://117.79.93.196来访问csdn主页。一般情况下,一个网站的地址会是这样的结构:http://ip:port。http://标志协议名,ip:port是典型的tcp地址。这样我们便能定位到某台机器上的某个特定的发布了的程序了。同理在浏览器中输入http://write.blog.csdn.net/postedit/7089374,你便会进入csdn博客的某篇文章(其实就是本文啦),也就是能访问到csdn网站的某个子资源。于是,我们可以认为,对于web应用http://ip:port/sub1/subsub1/...可以定位一个唯一的资源。对于RMI,其实貌似也是这么做的,只不过RMI不是HTTP,所以不用HTTP://开头;RMI貌似也不需要使用sub1/subsub1这样繁杂的子层嵌套结构。故同理:一个rmi://ip:port/name可以唯一定位一个RMI服务器上的发布了的对象。那么事实是不是这样呢?请看JDK关于Naming类的介绍,你会惊奇的发现,世界上的事,那都是想通的~
那么,剩下的就太简单了:package test.com.ztesoft.zsmart.bss.provisioning.serviceActivation; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.NotBoundException; import java.rmi.RemoteException; import com.ztesoft.provisioning.rmiserver.IRemoteTest; public class RmiSynCallTest { public static void main(String[] args) throws RemoteException, MalformedURLException, NotBoundException { String url = "rmi://localhost:9527/rmitest"; IRemoteTest test = (IRemoteTest)Naming.lookup(url); System.out.println(test.remoteTest("Lethorld")); } }