一、RMI工作原理
RMI的本質就是實現在不同JVM之間的調用,它的實現方法就是在兩個JVM中各開一個Stub和Skeleton,二者通過socket通信來實現參數和返回值的傳遞。
有關RMI的例子代碼網上可以找到不少,但絕大部分都是通過extend the interface java.rmi.Remote實現,已經封裝的很完善了,不免使人有霧裡看花的感覺。下面的例子是我在《Enterprise JavaBeans》裡看到的,雖然很粗糙,但很直觀,利於很快了解它的工作原理。
1、定義一個Person的接口,其中有兩個business method, getAge() 和getName()
代碼:
public interface Person {
public int getAge() throws Throwable;
public String getName() throws Throwable;
}
2、Person的實現PersonServer類
代碼:
public class PersonServer implements Person {
int age;
String name;
public PersonServer(String name, int age) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
3、好,我們現在要在Client機器上調用getAge()和getName()這兩個business method,那麼就得編寫相應的Stub(Client端)和Skeleton(Server端)程序。這是Stub的實現:
代碼:
//存根(stub)的實現
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;
public class Person_Stub implements Person {
Socket socket;
public Person_Stub() throws Throwable {
// connect to skeleton
socket = new Socket("computer_name", 9000);
}
public int getAge() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("age");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return inStream.readInt();
}
public String getName() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("name");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return (String)inStream.readObject();
}
}
注意,Person_Stub和PersonServer一樣,都implements Person。它們都實現了getAge()和getName()兩個business method,不同的是PersonServer是真的實現,Person_Stub是建立socket連接,並向Skeleton發請求,然後通過Skeleton調用PersonServer的方法,最後接收返回的結果。
4、骨架(Skeleton)的實現
代碼:
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.net.ServerSocket;
public class Person_Skeleton extends Thread {
PersonServer myServer;
public Person_Skeleton(PersonServer server) {
// get reference of object server
this.myServer = server;
}
public void run() {
try {
// new socket at port 9000
ServerSocket serverSocket = new ServerSocket(9000);
// accept stub's request
Socket socket = serverSocket.accept();
while (socket != null) {
// get stub's request
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
String method = (String)inStream.readObject();
// check method name
if (method.equals("age")) {
// execute object server's business method
int age = myServer.getAge();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeInt(age);
outStream.flush();
}
if(method.equals("name")) {
// execute object server's business method
String name = myServer.getName();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeObject(name);
outStream.flush();
}
}
} catch(Throwable t) {
t.printStackTrace();
System.exit(0);
}
}
public static void main(String args []) {
// new object server
PersonServer person = new PersonServer("Richard", 34);
Person_Skeleton skel = new Person_Skeleton(person);
skel.start();
}
}
Skeleton類extends from Thread,它長駐在後台運行,隨時接收client發過來的request。並根據發送過來的key去調用相應的business method。
5、最後一個,Client的實現
代碼:
public class PersonClient {
public static void main(String [] args) {
try {
Person person = new Person_Stub();
int age = person.getAge();
String name = person.getName();
System.out.println(name + " is " + age + " years old");
} catch(Throwable t) {
t.printStackTrace();
}
}
}
Client的本質是,它要知道Person接口的定義,並實例一個Person_Stub,通過Stub來調用business method,至於Stub怎麼去和Server溝通,Client就不用管了。
注意它的寫法:
Person person = new Person_Stub();
而不是
Person_Stub person = new Person_Stub();
為什麼?因為要面向接口編程嘛,呵呵!
//RMI實質上就是生成2個類stub,skeleton來進行參數和返回值的傳遞,采用值傳遞方式
//類似於以前寫的聊天室程序,被傳遞的對象應實現java.io.Serializable接口
二、Websphere實現
EJB類一覽
這裡結合WebSphere來講講各個類的調用關系吧!
假定我們要創建一個讀取User信息的SessionBean,需要我們寫的有3個文件:
1、UserServiceHome.java
Home接口
2、UserService.java
Remote接口
3、UserServiceBean.java
Bean實現
WSAD最終會生成10個class。其它7個是什麼呢?我們一個一個數過來。
4、_UserServiceHome_Stub.java
這個當然就是Home接口在Client端(動態加載)的Stub類了,它implements UserServiceHome。
5、_EJSRemoteStatelessUserServiceHome_a940aa04_Tie.java
Home接口在Server端的Skeleton類,"a940aa04"應該是隨機生成的,所有其他的相關class名裡都會有這個標志串,Tie是Corba對Skeleton的叫法。
6、EJSRemoteStatelessUserServiceHome_a940aa04.java
Home接口在Server端的實現,當然,它也implements UserServiceHome。
7、EJSStatelessUserServiceHomeBean_a940aa04.java
由#6調用,create _UserService_Stub。(為什麼#6不能直接create _UserService_Stub呢?後面再講。)
8、_UserService_Stub.java
Remote接口在Client端(動態加載)的Stub類。它implements UserService。
9、_EJSRemoteStatelessUserService_a940aa04_Tie.java
Remote接口在Server端的Skeleton類。
10、EJSRemoteStatelessUserService_a940aa04.java
Remote接口在Server端的實現,當然,它也implements UserService。並且,它負責調用UserServiceBean——也就是我們所寫的Bean實現類——裡面的business method。
那麼,各個類之間的調用關系到底是怎麼樣的呢?簡單的說,就是兩次RMI循環。
第一個RMI循環
先來看看Client端的程序是怎麼寫的:
代碼:
try {
InitialContext ctx = new InitialContext();
//第一步
UserServiceHome home =
(UserServiceHome) PortableRemoteObject.narrow(
ctx.lookup(JNDIString),
UserServiceHome.class);
//home: _UserServiceHome_Stub
System.out.println(home.toString());
//第二步
UserService object = home.create();
//ojbect: _UserService_Stub
System.out.println(object.toString());
//第三步
int userId = 1;
UserInfo ui = object.getUserInfo(userId);
}
在第一步之後,我們得到了一個UserServiceHome(interface)定義的對象home,那麼,home到底是哪個class的instance呢?用debug看一下,知道了home原來就是_UserServiceHome_Stub的實例。
從第二步開始,就是我們的關注所在,雖然只有簡單的一行代碼,
UserService object = home.create();
但是他背後的系統是怎麼運做的呢?我們進入代碼來看吧!
1、調用home.create()
代碼:
UserServiceHome home;
UserService obj = home.create();
2、實際是調用_UserServiceHome_Stub.create(),在這個方法裡面,Stub向Skeleton發送了一個create的字串:
代碼:
org.omg.CORBA.portable.OutputStream out = _request("create", true);
in = (org.omg.CORBA_2_3.portable.InputStream)_invoke(out);
3、Server端的Skeleton接收Stub發來的request,並調用相應的方法:
代碼:
_EJSRemoteStatelessUserServiceHome_a940aa04_Tie._invoke() {
......
switch (method.length()) {
case 6:
if (method.equals("create")) {
return create(in, reply);
}
......
}
}
代碼:
_EJSRemoteStatelessUserServiceHome_a940aa04_Tie.create() {
EJSRemoteStatelessUserServiceHome_a940aa04 target = null;
result = target.create();
org.omg.CORBA.portable.OutputStream out = reply.createReply();
Util.writeRemoteObject(out,result);
return out;
}
4、Skeleton調用的是UserServiceHome的Server端實現類的create方法
代碼:
EJSRemoteStatelessUserServiceHome_a940aa04.create() {
UserService _EJS_result;
_EJS_result = EJSStatelessUserServiceHomeBean_a940aa04.create();
}
5、#4又調用EJSStatelessUserServiceHomeBean_a940aa04.create()
代碼:
UserService result = super.createWrapper(new BeanId(this, null));
至此,我們終於結束了第一個RMI循環,並得到了Remote接口UserService的Stub類_UserService_Stub,就是#5裡面的result。
這裡有一個問題,為什麼#4不直接create _UserService_Stub,而又轉了一道#5的手呢?因為#4 extends from
EJSWrapper,它沒有能力create Stub,因此必須借助#5,which extends from EJSHome,這樣才可以生成一個Stub。如果不是為了生成這個Stub,應該可以不走#5這一步。
第二個RMI循環
OK, now we got the object which is instanceOf _UserService_Stub, and implements UserService
現在我們的Client端走到第三步了:
UserInfo ui = object.getUserInfo(userId);
繼續看代碼,開始第二個RMI循環:
1、調用object.getUserInfo()
代碼:
UserService object;
object.getUserInfo(userId);
2、實際是調用_UserService_Stub.getUserInfo(int arg0),在這個方法裡面,Stub向Skeleton發送了一個getUserInfo的字串和arg0這個參數:
代碼:
org.omg.CORBA.portable.OutputStream out = _request("getUserInfo", true);
out.write_long(arg0);
in = (org.omg.CORBA_2_3.portable.InputStream)_invoke(out);
3、Server端的Skeleton接收Stub發來的request,並調用相應的方法:
代碼:
_EJSRemoteStatelessUserService_a940aa04_Tie._invoke() {
switch (method.charAt(5))
{
case 83:
if (method.equals("getUserInfo")) {
return getUserInfo(in, reply);
}
......
}
}
_EJSRemoteStatelessUserService_a940aa04_Tie.getUserInfo() {
EJSRemoteStatelessUserService_a940aa04 target = null;
int arg0 = in.read_long();
UserDTO result = target.getUserInfo(arg0);
org.omg.CORBA_2_3.portable.OutputStream out = reply.createReply();
out.write_value(result,UserDTO.class);
return out;
}
4、Skeleton調用的是UserService的Server端實現類的getUserInfo方法
代碼:
EJSRemoteStatelessUserService_a940aa04.getUserInfo() {
UserServiceBean _EJS_beanRef = container.preInvoke(this, 0, _EJS_s);
_EJS_result = _EJS_beanRef.getUserInfo(id);
}
最後的最後,#4終於調用了我們寫的UserServiceBean裡的getUserInfo方法,這才是我們真正想要去做的事情。
至此,第二個RMI循環也終於結束了。
調用流程圖
回顧一下上面的分析,可以很清晰的看到兩次RMI循環的過程,下圖(見鏈接)描述了整個流程:
圖1
黃色的1,6,10是程序員要寫的,其余是系統生成的。
#1是Home interface, #2和#4都implements了它。
#6是Remote interface, #7和#9都implements了它。
#10是Bean實現。
三、weblogic實現
一個遠程對象至少要包括4個class文件:遠程對象;遠程對象的接口;實現遠程接口的對象的stub;對象的skeleton這4個class文件。
在EJB中則至少要包括10個class:
Bean類,特定App Server的Bean實現類
Bean的remote接口,特定App Server的remote接口實現類,特定App
Server的remote接口的實現類的stub類和skeleton類
Bean的home接口,特定App Server的home接口實現類,特定App
Server的home接口的實現類的stub類和skeleton類
和RMI不同的是,EJB中這10個class真正需要用戶編寫的只有3個,分別是Bean類和它的remote接口,home接口,至於其它的7個class到底是怎麼生成,被打包在什麼地方,或者是否需要更多的類文件,會根據不同的App Server表現出比較大的差異,不能一概而論。
拿Weblogic的來說吧!Weblogic的Bean實現類,以及兩個接口的Weblogic的實現類是在ejbc的時候被打包到EJB的jar包裡面的,這3個class文件可以看到。而home接口和remote接口的Weblogic的實現類的stub類和skeleton類是在EJB被部署到Weblogic的時候,由Weblogic動態生成stub類和Skeleton類的字節碼,因此看不到這4個類文件。
對於一次客戶端遠程調用EJB,要經過兩個遠程對象的多次RMI循環。首先是通過JNDI查找Home接口,獲得Home接口的實現類,這個過程其實相當復雜,首先是找到Home接口的Weblogic實現類,然後創建一個Home接口的Weblogic實現類的stub類的對象實例,將它序列化傳送給客戶端(注意stub類的實例是在第1次RMI循環中,由服務器動態發送給客戶端的,因此不需要客戶端保存Home接口的Weblogic實現類的stub類),最後客戶端獲得該stub類的對象實例(普通的RMI需要在客戶端保存stub類,而EJB不需要,因為服務器會把stub類的對象實例發送給客戶端)。
客戶端拿到服務器給它的Home接口的Weblogic實現類的stub類對象實例以後,調用stub類的create方法,(在代碼上就是home.create(),但是後台要做很多事情),於是經過第2次RMI循環,在服務器端,Home接口的Weblogic實現類的skeleton類收到stub類的調用信息後,由它再去調用Home接口的Weblogic實現類的create方法。
在服務端,Home接口的Weblogic實現類的create方法再去調用Bean類的Weblogic實現類的ejbCreate方法,在服務端創建或者分配一個EJB實例,然後將這個EJB實例的遠程接口的Weblogic實現類的stub類對象實例序列化發送給客戶端。
客戶端收到remote接口的Weblogic實現類的stub類的對象實例,對該對象實例的方法調用(在客戶端代碼中實際上就是對remote接口的調用),將傳送給服務器端remote接口的Weblogic實現類的skeleton類對象,而skeleton類對象再調用相應的remote接口的Weblogic實現類,然後remote接口的Weblogic實現類再去調用Bean類的Weblogic實現類,如此就完成一次EJB對象的遠程調用。
看了一遍文章,感覺還是沒有說太清楚,既然寫了帖子,就想徹底把它說清楚。
先拿普通RMI來說,有4個class,分別是遠程對象,對象的接口,對象的stub類和skeleton類。而對象本身和對象的stub類同時都實現了接口類。而我們在客戶端代碼調用遠程對象的時候,雖然在代碼中操縱接口,實質上是在操縱stub類,例如:
接口類:Hello
遠程對象:Hello_Server
stub類:Hello_Stub
skeleton類:Hello_Skeleton
客戶端代碼要這樣寫:
Hello h = new Hello_Stub();
h.getString();
我們不會這樣寫:
Hello_Stub h = new Hello_Stub();
h.getString();
因為使用接口適用性更廣,就算更換了接口實現類,也不需要更改代碼。因此客戶端需要Hello.class和Hello_Stub.class這兩個文件。但是對於EJB來說,就不需要Hello_Stub.class,因為服務器會發送給它,但是Hello.class文件客戶端是省不了的,必須有。表面上我們的客戶端代碼在操縱Hello,但別忘記了Hello只是一個接口,抽象的,實質上是在操縱Hello_Stub。
拿Weblogic上的EJB舉例子,10個class分別是:
Bean類:HelloBean (用戶編寫)
Bean類的Weblogic實現類:HelloBean_Impl (EJBC生成)
Home接口:HelloHome (用戶編寫)
Home接口的Weblogic實現類 ((Hello Bean))_HomeImpl(EJBC生成)
Home接口的Weblogic實現類的stub類 ((Hello Bean))_HomeImpl_WLStub(部署的時候動態生成字節碼)
Home接口的Weblogic實現類的skeleton類 ((Hello Bean))_HomeImpl_WLSkeleton(部署的時候動態生成字節碼)
Remote接口: Hello (用戶編寫)
Remote接口的Weblogic實現類 ((Hello Bean))_EOImpl(EJBC生成)
Remote接口的Weblogic實現類的stub類 ((Hello Bean))_EOImpl_WLStub(部署的時候動態生成字節碼)
Remote接口的Weblogic實現類的skeleton類 ((Hello Bean))_EOImpl_WLSkeleton(部署的時候動態生成字節碼)
客戶端只需要Hello.class和HelloHome.class這兩個文件。
((Hello Home)) home = (Home) ((Portable Remote Object)).narrow(ctx.lookup("Hello"), ((Hello Home)).class);
這一行代碼是從JNDI獲得Home接口,但是請記住!接口是抽象的,那麼home這個對象到底是什麼類的對象實例呢?很簡單,用toString()輸出看一下就明白了,下面一行是輸出結果:
((Hello Bean))_HomeImpl_WLStub@18c458
這表明home這個通過從服務器的JNDI樹上查找獲得的對象實際上是HelloBean_HomeImpl_WLStub類的一個實例。
接下來,客戶端代碼:
Hello h = home.create()
同樣Hello只是一個抽象的接口,那麼h對象是什麼東西呢?打印一下:
((Hello Bean))_EOImpl_WLStub@8fa0d1
原來是HelloBean_EOImpl_WLStub的一個對象實例。
用這個例子來簡述一遍EJB調用過程:
首先客戶端JNDI查詢,服務端JNDI樹上Hello這個名字實際上綁定的對象是HelloBean_HomeImpl_WLStub,所以服務端將創建HelloBean_HomeImpl_WLStub的一個對象實例,序列化返回給客戶端。
於是客戶端得到home對象,表面上是得到HelloHome接口的實例,實際上是進行了一次遠程調用得到了HelloBean_HomeImpl_WLStub類的對象實例,別忘記了HelloBean_HomeImpl_WLStub也實現了HelloHome接口。
然後,home.create()實質上就是HelloBean_HomeImpl_WLStub.create(),該方法將發送信息給HelloBean_HomeImpl_WLSkeleton,而HelloBean_HomeImpl_WLSkeleton接受到信息後,再去調用HelloBean_HomeImpl的create方法,至此完成第1次完整的RMI循環。
注意在這次RMI循環過程中,遠程對象是HelloBean_HomeImpl,遠程對象的接口是HelloHome,對象的stub是HelloBean_HomeImpl_WLStub,對象的skeleton是HelloBean_HomeImpl_WLSkeleton。
然後HelloBean_HomeImpl再去調用HelloBean_Impl的ejbCreate方法,而HelloBean_Impl的ejbCreate方法將負責創建或者分配一個Bean實例,並且創建一個HelloBean_EOImpl_WLStub的對象實例。
這一步比較有趣的是,在前一步RMI循環中,遠程對象HelloBean_HomeImpl在客戶端有一個代理類HelloBean_HomeImpl_WLStub,但在這一步,HelloBean_HomeImpl自己卻充當了HelloBean_Impl的代理類,只不過HelloBean_HomeImpl不在客戶端,而是在服務端,因此不進行RMI。
然後HelloBean_EOImpl_WLStub的對象實例序列化返回給客戶端,這一步也很有趣,上次RMI過程,主角是HelloBean_HomeImpl和它的代理類HelloBean_HomeImpl_WLStub,但這這一次換成了HelloBean_EOImpl和它的代理類HelloBean_EOImpl_WLStub來玩了。
Hello h = home.create();h.helloWorld();
假設Hello接口有一個helloWorld遠程方法,那麼表面上是在調用Hello接口的helloWorld方法,實際上是在調用HelloBean_EOImpl_WLStub的helloWorld方法。
然後HelloBean_EOImpl_WLStub的helloWorld方法將發送信息給服務器上的HelloBean_EOImpl_WLSkeleton,而HelloBean_EOImpl_WLSkeleton收到信息以後,再去調用HelloBean_EOImpl的helloWorld方法。至此,完成第2次完整的RMI循環過程。
在剛才HelloBean_EOImpl是作為遠程對象被調用的,它的代理類是HelloBean_EOImpl_WLStub,但現在HelloBean_EOImpl要作為HelloBean_Impl的代理類了。現在HelloBean_EOImpl去調用HelloBean_Impl的helloWorld方法。注意!HelloBean_Impl繼承了HelloBean,而HelloBean中的helloWorld方法是我們親自編寫的代碼,現在終於調用到了我們編寫的代碼了!
至此,一次EJB調用過程終於完成。在整個過程中,服務端主要要調用的類是HelloBean_Impl, HelloBean?_HomeImpl,HelloBean_HomeImpl_WLSkeleton,HelloBean_EOImpl,HelloBean_EOImpl_WLSkeleton。客戶端主要調用的類是HelloBean_HomeImpl_WLStub,HelloBean_EOImpl_WLStub,這兩個類在客戶端代碼中並不會直接出現,出現在代碼中的類是他們的接口HelloHome和Hello,因此客戶端需要這兩個接口文件,而Stub是服務器傳送給他們的。
四、理解體會
簡單講,就是為了適應分布式開發的需要。
首先,回到我最後給出的流程圖。
圖2
Client端最原始的沖動,肯定是能直接調用#10.UserServiceBean就爽了。那麼第一個問題來了,Client和Server不在一個JVM裡。
這好辦,我們不是有RMI嗎?好,這個問題就這麼解決了:
1. UserServiceBeanInterface.getUserInfo()
2. UserServiceBeanStub
3. UserServiceBeanSkeleton
4. UserServiceBean
用著用著,第二個問題來了,UserServiceBean只有人用,沒人管理,transaction logic, security logic, bean instance pooling logic這些不得不考慮的問題浮出水面了。
OK,我們想到用一個delegate,EJBObject,來進行所有這些logic的管理。client和EJBObject打交道,EJBObject調用UserServiceBean。
注意,這個EJBObject也是一個Interface,#6.UserService這個interface正是從它extends而來。並且EJBObject所管理的這些logic,正是AppServer的一部分。
現在的流程變為了:
EJBObject
1. UserService.getUserInfo()
2. UserServiceStub
3. UserServiceSkeleton
4. UserServiceImp
5. UserServiceBean
這已經和整幅圖裡的#6, #7, #8, #9, #10一一對應了。
現在能滿足我們的需求了嗎?不,第三個問題又來了:
既然是分布式開發,那麼我當然沒理由只用一個Specified Server,我可能需要用到好幾個不同的Server,而且EJBObject也需要管理呀!
OK,為了適應你的需要,我們還得加再一個HomeObject,首先它來決定用哪個Server(當然,是由你用JNDI String設定的),其次,它來管理EJBObject。
注意,這個EJBHome也是一個Interface,#1.UserServiceHome這個interface正是從它extends而來。並且EJBHome管理EJBObject的logic,也是AppServer的一部分。
現在的調用次序是:
1. EJBHome.create()
2. EJBHomeStub
3. EJBHomeSkeleton
4. EJBHomeImp(EJSWrapper)
5. EJSHome
得到EJBObject。
6. UserService.getUserInfo()
7. UserServiceStub
8. UserServiceSkeleton
9. UserServiceImp
10. UserServiceBean
現在已經完全和流程圖的調用順序一致了。
//EJB的基礎是RMI IIOP,原理並不是很難,關鍵是實現起來比較繞,一個簡單的功能要用10個(或更多)類來實現,但每一個都不是多余的。
//EJB的這種模式(或說RMI)完全屏蔽了底層的網絡,並很好的實現了對業務代碼的保護。