• 2004-05-28

    恶斗EJB(五)

    Tag:向上走

    从SUN工程师的回复中,我得到了另外一个有用的信息,在SUN ONE Application Server的安装目录下的samples\rmi-iiop\simple有一个完整的普通应用调用EJB的例子。
    赶紧操练起来,按照文档一步一步走来,很是轻松愉悦,除了EJB Stub,还要用到应用服务器安装目录的lib目录下appserv-ext.jar。
    走到“Local or remote RMI/IIOP-based client without ACC(Application Client Container)”的一节,同样的问题出现了,程序运行起来就僵死在那里,没有反应。
    怎么又是这样?在我确定了我确实是按照文档一步一步走下来的,我有了一种束手无策的感觉。
    穷极无聊之际,我扫了一眼Troubleshooting:
    如果运行应用出现了任何问题,看一下server的日志。
    嗯?是啊!我光顾着client端了,竟然根本没有想过server端。打开server的日志,一个异常让我眼前一亮:
    com.iplanet.ias.cis.connection.ConnectException: com.iplanet.ias.cis.channel.tcp.TCPNativeException: -5973:EndPoint.JNI_getValidAddressNative: PR_GetHostByAddr() Failed
            at com.iplanet.ias.cis.connection.Connection.<init>(Connection.java:205)
            at com.iplanet.ias.cis.connection.ServerConnection.accept(ServerConnection.java:251)
            at com.sun.corba.ee.internal.iiop.ListenerThread.run(ListenerThread.java:77)
    Caused by: com.iplanet.ias.cis.channel.tcp.TCPNativeException: -5973:EndPoint.JNI_getValidAddressNative: PR_GetHostByAddr() Failed
            at com.iplanet.ias.cis.connection.EndPoint.getValidAddressNative(NativeMethod)
            at com.iplanet.ias.cis.connection.EndPoint.getValidAddress(EndPoint.java:239)
            at com.iplanet.ias.cis.connection.EndPoint.<init>(EndPoint.java:101)
            at com.iplanet.ias.cis.connection.EndPoint.getEndPoint(EndPoint.java:73)
            at com.iplanet.ias.cis.connection.EndPoint.getEndPoint(EndPoint.java:78)
            at com.iplanet.ias.cis.channel.tcp.TCPChannel.getPeerEndPoint(TCPChannel.java:74)
            at com.iplanet.ias.cis.connection.Connection.<init>(Connection.java:201)
            ... 2 more
    虽然单凭这个异常,我无法得知问题所在,但这比漫无目的的搜寻显然好上许多。

    再次向SUN工程师请教。很快,我得到了回复。显然,这个问题对于SUN的工程师来说已经是轻车熟路了。
    RMI通信的时候,需要主机地址解析,只要在两台机器互相把主机名和IP放到hosts配置文件中就可以了。
    在WinXP上,这个文件是C:\WINDOWS\system32\drivers\etc的目录下hosts,在Solaris上,这个文件是/etc/hosts。
    在文件中加入IP和主机名的映射
        xxx.xxx.xxx.xxx hostname

    运行,bingo,成功!
    SUN工程师的答案果然见效。只是我想不通,我用的是IP直连,这和主机名有什么关系。显然,这让人不爽,因为将来部署的时候,这肯定需要写在安装手册里,增加复杂度,牵扯得不仅仅是应用本身的部署了。

    再来试试自己的例子。显然我前面的思路多少有些偏差,没有加入EJB Stub确实是个问题,但并不是根本原因,地址解析才是我的代码僵死的元凶。
    好了,加入EJB Stub和appserv-ext.jar,运行,我终于见到了期盼已久的问候。
    在这个调用中,我并没有使用ORB的那两个参数,这大概也算是J2EE RI和SUN ONE Application Server的差异之一吧!不过后来的事实证明,这并非一个不可逾越的鸿沟。
    原来的异常出现在server端,client端为什么也要加呢?于是,我去掉了client端IP和主机名的映射,使用IP调用没问题,使用主机名调用的时候,抛出了个无法连接ORB的异常。没问题,和我预想的一样。

    再接再厉,尝试在Servlet中调用。
    因为Servlet最终要运行在应用服务器上,所以我省去了appserv-ext.jar,只加入了EJB Stub。写程序,

    配参数,部署……
    又出问题了,它怎么提示我<ejb-link>的参数不对。这是一个用来在Servlet中指示欲使用的EJB名称的参数。难道它让我把EJB应用也加进去,那还是一个单纯的Web应用了吗?
    瞎猫总能碰到死耗子。当我糊涂的把EJB Stub作为一个Module加入到Web应用中,居然部署成功了。
    敲入URL,又是半天没响应,这次可有经验了,赶紧把那台机器的IP和主机名加到部署EJB的服务器上。
    OK,成功!

    终于搞定了在SUN ONE Application Server上远程调用EJB的问题,最初设置的一个参数又引起了我的兴趣。前面的设置有这样一句:
        env.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");
    这个com.sun.jndi.cosnaming.CNCtxFactory并不是J2EE包特有的,而是存在于rt.jar中,换句话说,它是J2SE中的。那它是否可以用在J2EE RI上呢?
    带着这个问题,我翻出之前的那个例子,改动了参数,去掉ORB的那两个参数。
    运行,Hello来了。
    原来可以这么调。早知如此,何必当初呢!

    总结一下,算是个经验和教训的集合吧!
    J2EE RI和SUN ONE Application Server通用的调用远程EJB的参数是:
        java.naming.factory.initial=com.sun.jndi.cosnaming.CNCtxFactory
        java.naming.provider.url=iiop://host:port

    如果使用的是SUN ONE Application Server,调用远程EJB需要在部署EJB的机器上hosts配置文件中加入调用端IP和主机名的映射。
    如果调用端在IIOP参数中使用的是主机名,则需要在其hosts配置文件中加入部署EJB的机器的IP和主机名的映射。
    在WinXP上,hosts配置文件是C:\WINDOWS\system32\drivers\etc的目录下hosts;在Solaris上,这个文件是/etc/hosts。
    在文件中加入IP和主机名映射的方式是
        xxx.xxx.xxx.xxx hostname

    又是一场恶斗,虽然精疲力竭,却也心情愉悦!战斗后的胜利感觉,总是最美妙的。

  • 2004-05-28

    恶斗EJB(四)

    Tag:向上走

    我最不愿意见到的事情发生了,这场恶斗有了续集。

    我们的应用实际运行在SUN ONE Application Server上。原本以为在J2EE RI上搞定一切的我可以顺利地把这些东西过渡到我的目标平台。没想到,等待我的却是另一场恶斗。
    哎,Java的可移植性啊!

    开始阶段异乎寻常的顺利。
    虽然SUN ONE Studio是个让人不敢恭维的IDE,但与SUN ONE Application Server配合起来,还是比较愉快的。在向导的帮助下,我很快就建起了一个新的HelloWorld EJB,把它加入一个EJB Module,部署,OK。
    几个必要的参数也很快的从文档中被我搜了出来,于是几个参数设成了这样:
        env.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");
        env.put("java.naming.provider.url", "iiop://xxx.xxx.xxx.xxx:port");
    lookup方法中,我使用了JNDI名称,而舍弃了那个需要繁琐设置的引用名称。
    眼前的一切似乎预示着,我只要点一下run,任务就算完成了。

    当我点下run的时候,不可思议的一幕发生了,程序僵死在那里,一动不动。
    我不敢相信这一切,赶紧用出最原始的招数:加打印信息。打印信息给我的答案是,程序运行到下面这句后停住不动了。
        Context initial = new InitialContext(env);

    冷静,冷静,问题出在哪了呢?
    率先闯进来的失误是我没有创建一个EJB Stub。解决问题总是比发现问题来得容易,当我在EJB上点下右键的时候,我傻眼了,根本就没有创建EJB Stub的选项。挺住,再看EJB Module,还是没有,怎么会这样?
    于是,我开始了新一轮的搜索,使用SUN ONE Studio创建EJB Stub。与此同时,我通过公司的同事向SUN的工程师请教。
    SUN网站上的一个例子拯救了我,那也是一个使用SUN ONE Studio创建普通应用调用EJB的例子。
    它创建EJB Stub的方法是创建一个应用,把EJB Module加入其中。右键点击这个应用,菜单上会出现“Export Client Support”的选项。
    我要的只是一个EJB,为什么非得加入应用?况且EJB Module是可以单独部署的。
    人在屋檐下,不得不低头。照猫画虎,当我点击这个选项的时候,蹦出一个对话框“Cannot export client stubs jar for an application this is not deployed”。
    这又是什么道理,非得让我必须部署才行。
    SUN论坛中的一个帖子给了我这样的答案,部署的时候,SUN ONE Studio会在本地创建一个临时目录,里面存放着部署的东西,也就是我们常说的EAR文件,创建EJB Stub实际上就是到那里找到这个EAR文件,根据它进行创建。我通过实验证明,只要在把应用服务器上部署了这个应用,即便把临时目录下的东西都删了,它还是会去创建一个临时目录存放这个EAR文件,然后再创建EJB Stub。
    不管怎样,EJB Stub创建出来了。
    这时,SUN工程师的应答也来了。我又得到了两种得到EJB Stub的方法。
    当EAR文件部署到应用服务器上之后,这个应用的目录下就会产生一个Stub的文件,可以把它拿过来。另外在SUN ONE Studio中,右键点击EJB的时候,会有一个“Create New EJB Test Application...”的选项,通过它绕个弯子,也可以得到EJB Stub。

  • 2004-05-25

    恶斗EJB(三)

    Tag:向上走

    一切都结束了吗?别急,刚到再接再厉的好时候,难道你不觉得有了IP,再有个端口就更好了吗?遗憾的是,文档没有给我们答案。
    以前面的结果为契机,我们继续向前。
    端口常与IP齐舞,那我们就从“org.omg.CORBA.ORBInitialHost”出发。既然它能起作用,显然代码里应该有这个字符串,搜!
    不出所料,在它身边,“org.omg.CORBA.ORBInitialPort”出现了。
    在代码中再加一段:
        System.setProperty("org.omg.CORBA.ORBInitialPort", "1050");
    如果测试的话,使用1050和没使用几乎没什么区别,因为它是缺省端口。改一下容易,但也要同时改了server的端口,才能验证我们的想法。
    有了前面的探索,找到server的配置简单许多。最终我将目标锁定在config/orb.properties上。一个port和一个host。改动port,重新启动server,从打印出来的信息,我确信自己的改动是成功的。
    client,跑!OK!

    port我们已经知道了用途,但那个host呢?难道启动还能绑到别人的IP不成。强烈的好奇心让我暂时偏移了原来的轨道,改动IP,没什么,它依然能够启动,关了吧!
    发现问题了!
    关掉的不是这台机器的server,而是由host指定的那台机器上的server,原来它是干这个的。

    回到原来的路上。
    这里已经有了IP和端口,在Context.PROVIDER_URL中指定的URL也涉及到IP和端口,意义何在呢?
    删了它,试一下。代码居然仍然运行良好。

    再来看一下HelloWorldAppClient.jar,把它加入classpath之后,我们几乎忘了它的存在。
    在J2EE Tutorial中,HelloWorldClient也被打到了这个JAR文件中,我们自己编写就是一个client,那么这个client有作用是什么呢?事实证明,它是标准的托,没用,删了它对大局无碍。
    现在看看HelloWorldAppClient.jar,只有与EJB紧密关联的类,清净不少啊!

    还有什么问题呢?
    还记得我们在开始的时候,将引用名称改成了JNDI名称吧!
    我想把它改回来。
    同样以异常起步,寻着调用逆流而上,一步一步解决出现的问题。
    对比直接用runclient运行,很容易就可以找到二者的差异,大踏步向前。
    runclient其实只是一个批处理文件,其中我最核心的一句简化一下就是下面这个样子:
    %JAVA_COMMAND% com.sun.enterprise.appclient.Main %*
    %JAVA_COMMAND%就相当于我们常用的java命令,那实际上这一句就是运行com.sun.enterprise.appclient.Main。
    同直接运行相比,二者之间的差别肯定是runclient在前期设置了一些东西,才使得代码最终可以运行良好,所以,直接调用就需要自己完成这一些设置的工作。

    当我在代码中加入一些新的代码后,我决定放弃。
    并非问题无法解决,而是从差别中查找出来的一些设置方法同J2EE RI中的一些类紧密关联在一起。如果说前面设置属性的方式是我可以接受的,那么这里就完全破坏代码中的那种美感。这样,即便最终可以完成,那也已经失去应有的意义。

    想来,我最初“调用远程EJB”的目的已经达到了,而在我将来的代码会运行在应用服务器上,这些设置工作应用服务器可以替我很好的完成,我也就不必理会了。

    最后,我要告诫一下对有兴趣尝试一下的朋友,前面所有的代码都是在J2EE RI上完成的,具体的应用服务器之间还是有差别的。
    好了,一场恶斗就此结束。

  • 2004-05-25

    恶斗EJB(二)

    Tag:向上走

    我们的最终目的是在不同的机器上调用EJB,下面就该体验分布式了。
    在另外一台机器上,架起一个Server,把EJB部署上去,然后改动client代码中的IP,准备,跑!
    成功了吗?
    我期望的是失败。
    如果你看见了来自远程的问候,先别兴奋,看看运行client的机器上是否还跑着一个J2EE RI呢?
    我调的是远程的EJB,和我本机起的server有什么关系呢?先别管这些,关了它。
    恭喜你,代码终于正常的失败了。

    谁愿意面对失败呢?出现了问题,我们要做就是解决它。
    异常的提示是个不错的起点。
        javax.naming.CommunicationException: Can't find SerialContextProvider
            at com.sun.enterprise.naming.SerialContext.getProvider(SerialContext.java:63)
            at com.sun.enterprise.naming.SerialContext.lookup(SerialContext.java:120)
            at javax.naming.InitialContext.lookup(Unknown Source)
            ……
    显然,这不是很好的提示,因为我们无法通过异常信息获得更多,但我们知道了出现错误的类。
    幸运的是,SUN的J2EE RI源代码是公开的,这样我就省去了反编译class文件的麻烦。
    打开com.sun.enterprise.naming.SerialContext这个类,找到getProvider,其中的一个debug变量引起了我的兴趣,这是一个调试的标志,只要把它置为true,一些调试信息就会流出来。
    我肯定了自己的想法,改变debug,但如何来编译源代码呢?
    我并不想费尽心力去编译J2EE RI庞大的源码,毕竟我需要的只是改变其中的一个类,而不是研究整个J2EE RI。

    挥出自己常用法宝,在自己的IDE中建立,建立起一个工程之后,直接把包含目标类的JAR文件(可能还有一些比较的JAR文件)加入工程,这样编译这个类所需的全部内容就都有了。
    取过我的目标类,按照包结构放入工程之中,这样代码应该就可以正常编译了。
    将改动后的class文件再塞进JAR,这样我的改动就开始发挥作用了。

    通过改动,我们可以多一些信息。
        org.omg.CORBA.COMM_FAILURE:   vmcid: SUN  minor code: 201  completed: No
            at com.sun.corba.ee.internal.iiop.ConnectionTable.getConnection(ConnectionTable.java:176)
            at com.sun.corba.ee.internal.iiop.ConnectionTable.getConnection(ConnectionTable.java:68)
            at com.sun.corba.ee.internal.iiop.GIOPImpl.getConnection(GIOPImpl.java:70)
            ……
    CORBA错误?是的,CORBA。这就不太好办了,虽然听说过EJB和CORBA之间千丝万缕的联系,但是我可是标准的CORBA门外汉。
    在下定决心不去从头学习CORBA之后,我决定继续用前面的手法挖掘问题的原因。

    逆流而上的结果是我发现了这样的信息。
        com.sun.corba.ee.internal.iiop.GIOPImpl(Thread[main,5,main]): getEndpoint(IIOP_CLEAR_TEXT, 0, null)
        com.sun.corba.ee.internal.iiop.GIOPImpl(Thread[main,5,main]): createListener( socketType = IIOP_CLEAR_TEXT port = 0 )
        com.sun.corba.ee.internal.iiop.ConnectionTable(Thread[main,5,main]): Client get called: host = localhost port = 1050
        com.sun.corba.ee.internal.iiop.ConnectionTable(Thread[main,5,main]): Exception java.lang.RuntimeException: Connection refused: connect while creating socket for new connection: aborting connection
        com.sun.corba.ee.internal.iiop.ConnectionTable(Thread[main,5,main]): DeleteConn called: host = localhost port = 1050
        com.sun.corba.ee.internal.iiop.ConnectionTable(Thread[main,5,main]): Client get called: host = localhost port = 1050
    很奇怪吧!这里出现了localhost。从直觉来说,我们明明在代码中写了调用远程的EJB,应该根本没有localhost什么事。事实就是这样奇怪,调用的时候居然先走localhost,这也就是为什么如果本机起的一个J2EE RI,我们就可以看到来自远方的问候。
    就在我疑惑不知该如何走下一步时,无意之举给了我新的方向。
    我翻起了J2EE RI配套的文档。没错,之前我一直在自行探索。
    在J2EE SDK Tools的文档中,runclient的讲解中有一个小节,其标题是《Accessing a Remote Server》,显然这对我是个极大的触动。
    其中提到了一个属性“org.omg.CORBA.ORBInitialHost”。
    设置这个属性就可以访问远程EJB???
    还等什么?
    我们知道在Java运行时以-D定义一个属性同直接在代码中使用System.setProperty具有相同的功效,于是代码中出现了这样一段:
    System.setProperty("org.omg.CORBA.ORBInitialHost", "xxx.xxx.xxx.xxx");
    运行,祈祷!
    来自远方的问候终于出现在我的面前,那种激动相信是程序员无数次经历却依然愿意体味的感觉。我愿意把这种感觉与吸毒等同起来,完全是一种瘾,这是这种瘾让我愿意迎接一次又一次的挑战,克服一个又一个的困难。

    “众里寻她千百度,蓦然回首,那人却在灯火阑珊处”
    应该说我的习惯并不好,在我自己绞尽脑汁独自摸索时,在我挥动google四处搜寻时,我居然没有想过自己的身边已经有了最好的答案。
    以此为戒,以后解决问题时,先要了解配套文档中有哪些内容。

  • 2004-05-25

    恶斗EJB(一)

    Tag:向上走

    虽然近来EJB 3.0已经吵得沸沸扬扬,但作为EJB 3.0根基的JDK 1.5还不知道什么时候正式发布,享受EJB 3.0带来的便利指不定是在什么时候,眼下我们拥有的还只是麻烦的EJB 2.0。

    终于找到了一个使用EJB稍微合适一些的理由。系统要支持多机,其中最难解决的数据共享,于是我想到了EJB,也开始了与EJB艰苦的战斗。

    按照J2EE Tutorial在J2EE RI上跑通一个Hello级的EJB并不是什么难事。
    下面是我的Hello EJB的客户端代码:
        Context initial = new InitialContext();
        Context myEnv = (Context)initial.lookup("java:comp/env");
        Object objref = myEnv.lookup("ejb/HelloWorld");
           
        HelloWorldHome home = (HelloWorldHome)PortableRemoteObject.narrow(objref, HelloWorldHome.class);
        HelloWorld helloEJB = home.create();
        System.out.println(helloEJB.hello("dreamhead"));

    Hello的实现非常简单:
        public String hello(String name) {
            return "Hello, " + name;
        }

    照本宣科,把J2EE RI启动起来,运行客户端代码:
        set APPCPATH=HelloWorldAppClient.jar
        runclient -client HelloWorldApp.ear -name HelloWorldClient -textauth

    bingo!成功。

    这当然不是故事的结局,否则太对不起“恶斗”的题目了。
    我们最习惯的运行Java程序的方式是
        java classname
    而这里运行代码用得居然是runclient,J2EE RI自带的一个工具,显然这并不符合我们的日常行为规范。
    我的目标,直接用java运行EJB客户端。

    直接运行HelloWorldClient显然不行,如果你不信邪,可以自己尝尝失败的滋味。

    一些资料给了我些许提示,设置一些必要的属性会帮助也许可以帮助我完成任务。于是在代码中加上了这样几句:
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.enterprise.naming.SerialInitContextFactory");
        env.put(Context.PROVIDER_URL, "iiop://xxx.xxx.xxx.xxx:1050");
    这里关注的主要是Context.INITIAL_CONTEXT_FACTORY和Context.PROVIDER_URL,它们其实是两个字符串,分别代表着“java.naming.factory.initial”和“java.naming.provider.url”,在一些资料里,它们往往素面示人。
    上面代码中给出的是针对J2EE RI的设置,不同的应用服务器设置是不同的,如果想在自己的应用服务器上使用类似的代码,先要找到自己的应用服务器上这两个参数是什么。因为关于J2EE RI的资料太少,就这两个参数,也害我花了好长时间。
    好容易找到的参数,写了一堆代码,可别忘了让它起作用啊!
        Context initial = new InitialContext(env);

    还有一个要改动的地方,那就是JNDI的名称。
    跟着J2EE Tutorial一路走来,我们很少在意代码中用以查找的那个“java:comp/env/ejb/HelloWorld”根本不是JNDI的名称,而只是一个引用名称。直接运行代码,给出的只是一个个错误提示。把它改回JNDI名称,问题迎刃而解。
        Object objref = initial.lookup("MyHelloWorld");

    J2EE的东西可不是随处可见的,所以,要自己运行的话,把J2EE RI的lib目录下的j2ee.jar拿过来会省去不少麻烦。另外,使用这个东西运行时可能会用到一些属性文件,我采用最直接的方式,将config目录下的东西直接搬了过来。

    代码可以编译了,别急着运行。想必听过EJB种种传闻的你一定知道Stub之类的东西,少了这个,代码绝对跑不起来,所以我们要把HelloWorldAppClient.jar加入到classpath中,传说中的Stub就在其中。

    我想许多人和我一样,跑这种例子会把server和client放在同一台机器上,上面的IP设置的是localhost或是本机IP。这样我们就可以幸福的通过第一关,看到EJB亲切的问候。

    万里长征终于走出第一步!