看了几天,终于把Head First Java看到了最后一章,虽然是最后一章,但是对于Java的学习来说,还只是序章的结束。
这篇笔记将介绍Java 的RMI,Servlets,EJB和Jini,基本上这四个东西都至少需要一小本书才能弄得清楚,因此不可能在一章里面说完,书上只说了原理或是表象,而我也凭我的理解记下这篇笔记。当然,主要介绍的是RMI,因此RMI会比较详细一些。
那么,开始:
RMI (Remote Method Invocation)
RMI是Java最早的分布式处理的解决方案。
设想一下情景:一台手机上的Java程序,需要处理一些大工作量的运算,自身机能有限,所以我们希望能够利用一台服务器来进行这种运算,而我的手机只需要提供原始数据和等待返回结果就可以(恩,是不是有点像云计算?)。那么,这种情况要怎么办?我们要调用另外一台JVM的堆上的对象的方法耶。。
答案就是RMI来帮忙!
RMI的模型是这样的:
途中的箭头表示的是当客户端要调用远程服务时客户端和服务端的整个处理过程:
首先我们应该明白各自的角色:
在Client上,有Client对象和Client helper对象,在服务端有Service对象和Service helper对象。Client对象和Service对象是要进行RMI的对象。
- Client Object:它希望能够调用远程服务器上的服务Service Object。
- Service Object:它提供RMI的被调用对象。
- Client helper:它负责实际的网络通信,它让Client Object感觉好像在调用本机的对象。我们看起来它似乎是在调用远程方法,但是实际上Client Object调用Client helper时只是在使用Client helper的Socket和流处理的功能,所以实际上Client helper是Service Object的代理。 ClientObject调用远程对象的方法时实际上是调用Client helper上的方法
- Service helper:它通过Socket连接接受客户端的请求和信息并发给Service Object处理,在获得Service Object处理后返回的结果后又通过连接发送会给Client helper
实际上,Client helper的真正的术语名称叫RMI stub,Service helper的真正术语名称叫RMI skeleton。这两个单词一个是存根一个是骨架,所以翻译了都不大能说明它真正的用途,还是别译的好。当然如果你非要理解成这个客户拿着存根去找那个被欺骗惨死后变成骨架的NPC作为任务结果道具要得到服务奖励的话我也没办法……
在Java中,RMI会直接帮我们创建好stub和skeleton,不需要我们动手写它们的内容,但是,我们还必须注意的是,网络是会挂的因此RMI的stub和skeleton是会抛出异常的。
另外,在使用RMI时我们需要决定使用哪种协议:
- RMI JRMP(Java Remote Message Protocol):这个协议很简单,也是Java RMI的原生协议,你在Java和Java程序之间进行RMI就只需要用这个协议就够了。
- RMI-IIOP:这个协议是基于CORBA的,它可以让你远程的程序不是Java,有兴趣可以参考这篇比较老的文章:http://www.ibm.com/developerworks/cn/java/j-rmi-iiop/
接下来我们要说明一下如何创建一个RMI程序:
- 首先,我们需要创建一个Remote接口,定义客户端可以远程调用的方法。
- 之后,我们要实现这个接口。
- 接下来,用rmic对上一步写好的类进行处理,获得stub和skeleton
- 运行时,我们需要先在客户端进行rmiregistry(注意,这里的rmiregistry必须要运行在能够存取到你写的程序的目录上),再启动远程服务。
那么,我们开始进行详细的步骤:
第一步:创建一个Remote接口,定义客户可以远程调用的方法
TestRemote.java:import java.rmi.*;
public interface TestRemote extends Remote {
public String sayHello() throws RemoteException;
}
我们建立的这个Test Remote接口就是远程接口,它定义了所有客户端可以远程调用的方法,实现它的类将能够生成stub和skeleton,所以它是个作为服务的多态化类。它有几个小要求:
- 此接口必须继承java.rmi.Remote接口,这个接口是一个标志性的接口,里面没有任何方法,但是这是RMI约定的规则,我们必须继承。
- 我们写的这个接口的所有方法都必须抛出RemoteException,这是因为客户端会使用实现此接口的stub,而stub会进行网络通信和io操作,所以可能会发生异常,应此必须定义成能够抛出RemoteException
- 这个接口定义的所有方法的参数和返回值都必须是primitive或Serializable类型(注意一个对象引用的所有对象都必须Serializable这个对象才能被Serialized)
第二步:实现上面定义的那个接口
TestRemoteImpl.java:import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
public class TestRemoteImpl extends UnicastRemoteObject implements TestRemote {
public String sayHello() throws RemoteException {
return "Server says, 'Hey'";
}
public TestRemoteImpl() throws RemoteException { }
public static void main(String[] args) {
try {
TestRemote service = new TestRemoteImpl();
Naming.rebind("RemoteHello", service);
} catch(Exception e) {
e.printStackTrace();
}
}
}
我们写的这个类就需要实现第一步中定义的TestRemote接口,为了要成为远程服务对象,我们写的这个类的对象必须要有与远程相关的功能,简而言之就是能够导出适当的远程对象与获得stub,因为我们使用JRMP,所以只需要简单的直接继承java.rmi.server.UnicastRemoteObject这个类即可。不过这个类有个问题就是它的构造函数会抛出RemoteException,所以我们最好的处理这个问题的方法就是声明一个会抛出同样异常的构造函数。
好了,处理完这个,我们就已经有了可以远程调用的类了,那我们还要做什么才能使用RMI呢?
先从程序上考虑:
- 显然我们需要一个TestRemoteImpl的对象
- 为了让客户端能够找到我们的远程服务,显然还有注册成为网络服务,我们把TestRemote的对象使用静态的Naming.rebind方法将一个名字绑定到这个远程对象上,客户端只需要使用rmi://url/name就可以访问这个服务了。
我们再看客户端程序:
TestRemoteClient.java:import java.rmi.*;
public class TestRemoteClient {
public static void main (String[] args) {
new TestRemoteClient().go();
}
public void go() {
try {
TestRemote service = (TestRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
String s = service.sayHello();
System.out.println(s);
} catch(Exception e ) {
e.printStackTrace();
}
}
}
这个客户端通过和服务器上的类相同的类型来引用到stub对象,Naming.lookup静态方法将返回这个stub对象的引用(实际上会对stub解序列化,因此客户端也需要stub),不过由于查询结果是Object类型,因此我们必须转换成接口类型。
接下来客户端就可以像本地对象一样调用远程对象了。我们必须铭记的一点是,本地对象和远程对象互相传输的只有参数和返回结果。方法处理的过程都不会在客户端有任何影响。
那么,在实际使用的过程中,我们一旦有了TestRemoteImpl.class就必须用rmic令其产生stub和skeleton(这个例子不会产生skeleton)。
而在真正提供服务时,在服务器上我们先必须在可以存取到TestRemoteImpl类的目录下启动rmiregistry程序。之后才启动我们的远程程序。
最后,在RMI模型下,我们需要确保客户端和服务端要有这些类:
- 客户端:TestRemote.class TestRemoteImpl_Stub.class
- 服务端:TestRemote.class TestRemoteImpl.class TestRemoteImpl_stub.class TestRemoteImpl_Skel.class
客户端是使用接口来调用stub上的方法,但是客户端不会再程序代码中引用到stub类,客户端总是通过接口来操作远程对象。服务端需要stub是因为它要将stub替换成连接在RMI registry上的真正服务。
-----------------------------------------------------------------------------------------------------------------------
EJB(Enterprise JavaBeans):
对于大型企业级应用来说,RMI的功能可能是远远不够的,应此才有了Enterprise Application Server,EJB具有一系列RMI不具备的服务,比如交易管理、安全性、并发性、数据库等等,但是EJB也是和RMI有关系的,通常,EJB服务器作用于RMI调用和服务层之间。
它的部分原理图类似于下面这样:
这里的客户端其实可以是任何设备,不过通常是同一个J2EE服务器上的servlet。两端的通信还是RMI,不过区别就是到了服务器上后,RMI skeleton会把有业务逻辑的请求提交给EJB对象这个中间件,EJB Object获得业务逻辑的bean,再提交给entreprise bean,bean对象被限制只有服务器能够与bean沟通,这样增强了安全性和管理能力。
------------------------------------------------------------------------------------------------------------------------------------
Jini:
哎呀,终于到了传说中的Jini。Jini也是RMI的拓展。
Jini也可以使用RMI,并且拥有几个关键性的功能:
- Adaptive Discovery(自适应搜索)
- Self-healing Networks(自我恢复网络)
我们知道,RMI的客户端为了获得远程服务,它必须知道服务器的IP地址和服务在服务器上注册的名称,而Jini不需要,Jini客户端只需要知道服务所实现的接口就能够获得服务,这就是所谓的adaptive discovery。
Jini实现adaptive discovery的方法在于Jini的lookup service,Jini有一个提供服务的服务器,还有一个Jini查询服务。Jini查询服务上线的时候,它会在网络上广播寻找已经注册的Jini服务,找到后就会在Jini查询服务上注册。另一方面,如果是Jini服务先上线,Jini服务上线后会搜索网上的Jini查询服务并申请注册。注册时,服务会发送一个序列化对象给查询服务,向Jini查询服务注册这个对象所实现的接口。客户端要调用的时候,就会想Jini查询服务请求是否有服务实现某个接口。如果找到Jini查询服务就会返回这个序列化对象。
而Jini的self-healing networks的原理就是在Jini查询服务和Jini服务注册后,查询服务会定期要求更新租约,如果服务没有相应,那么Jini查询服务就会认为这个服务已经down掉。
------------------------------------------------------------------------------
更新:把RMI程序打包成jar
在发布程序的时候,一般我们需要以jar包的形式发布出去,但是,我自己在使用的时候遇到了问题,运行时抛出了ClassNoFound:xxxx_stub.class这样的类似错误,在sun的java论坛询问过之后终于找到答案了。
在运行rmiregistry的时候需要使用-J-classpath指定jar文件的类路径。
2009-02-20更新完毕