-
2004-05-28
恶斗EJB(四)
我最不愿意见到的事情发生了,这场恶斗有了续集。
我们的应用实际运行在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(三)
一切都结束了吗?别急,刚到再接再厉的好时候,难道你不觉得有了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(二)
我们的最终目的是在不同的机器上调用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(一)
虽然近来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 -textauthbingo!成功。
这当然不是故事的结局,否则太对不起“恶斗”的题目了。
我们最习惯的运行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亲切的问候。
万里长征终于走出第一步!
-
2004-05-19
小心翼翼跨平台
Java的宣传广告中清清楚楚的写着“跨平台”几个大字,可Java程序写出来真的就能横行于各大平台之间吗?我们的开发平台是Windows,而最终的程序将运行在Solaris上,于是我有机会体会平台的差异。
先来看看今天的问题。
只要是你写的东西,指不定它会在哪天蹦出来恶心你。记不清哪位高人曾经说过大意如此的话,今天我算是体会到了。一个扔了大半年的程序突然就要用,在Windows下全部功能一直都跑得好好的,而且在Solaris上基本功能也一切正常,今天用到了一个从未在Solaris跑过的功能,结果……挂了!在手头没有源代码的情况下,我只能反编译,幸好当时没有混淆,庆幸!
经过分析,问题出在了这里:
private static final String CONFIG_FILE = "conf\\config.xml";
它的目的是从工作路径中conf目录下的configx.xml,这在Windows下一切正常,而在Solaris下,它就玩不转了,因为“\”不是Solaris下的目录分隔符。
找到问题解决就容易多了,把文件名改成下面这样就OK了。
private static final String CONFIG_FILE = "conf" + File.separator + "config.xml";这个问题让我想起了前不久遭遇同样因为平台差异造成的问题。
写了一段在包内查找文件的程序,其基本结构来自于Groller的PackageUtil。这段实际包的不同,这段查找分为查找目录和查找JAR文件两种情况。在查找JAR文件的代码中,有这样一段代码:
String path = url.getPath();
String jarFileName = path.substring(FILEPROTOCOLLEN,
path.lastIndexOf(".jar!") + JARTRINGLEN);
因为包内查找使用的是Class LoadergetResource方法,这里将会得到的是一系列的URL,比如像下面这样:
file:/C:/jarfile.jar!/packagename/
上面那段代码的目的是根据从这段URL中解析出jar文件的名字。在最初的版本中,两个常量的定义是
private static final int FILEPROTOCOLLEN = "file:/".length();
private static final int JARTRINGLEN = ".jar".length();
由此得到的JAR文件名是
C:/jarfile.jar
在Widnwos下,这段代码运行的很好,但到了Solaris上,它就坚持不住了。因为在Solaris上,URL变成了这样:
file:/root/jarfile.jar!/packagename/
经过解析得到的JAR文件名成了
root/jarfile.jar
显然原本期望的是一个绝对路径,结果成了相对路径,差在哪呢?多去了一个“/”。
改动方法是修改FILEPROTOCOLLEN的定义:
private static final int FILEPROTOCOLLEN = "file:".length();
这样在Solaris上,JAR文件名就成了
/root/jarfile.jar
这样,它就可以在Solaris上顺利运行了。也许细心的你已经发现了,由此带来的Windows上JAR文件名成了:
/C:/jarfile.jar
一开始,我也担心这个问题,但瞎猫碰死耗子的结果是,这段代码在Windows上也能很好的运行,于是,我就当它不存在。《Code Reading》中提到两种对待代码的态度,大多数是“如果它能工作,那么它就是正确的”,而NetBSD的哲学却是“除非它正确,否则它无法工作”。看来,我属于大多数。^_^
再往前,还有这样的故事。
一个同事向我求救,他的Tomcat起不来,我装了半天高手,结果很没面子,没搞定!最后的原因是这个哥们把Windows版的Tomcat放在Solaris上。按照我们通常的想法,以Java写成的Tomcat应该可以轻松放在Solaris上,但事实不是。号称跨平台的Java虽然比起它的一些前辈在可移植性上已经有上佳的表现,但并非100%的Java代码都能跨平台。虽然Java在可移植性做了很大的努力,比如第一个例子中File.separator,但这丝毫不妨碍我等无知小辈胡乱使用,加之第二个例子中少人注意的星星点点,如果跨平台的话,还真得多非点功夫。如果你和我一样,必须横跨多个平台,最好让代码在这几个平台上都跑跑。这里的平台可不只是操作系统,也可能应用服务器或是其它什么东西。如果代码压根就是绑死在某个平台,而且千秋万代也没有移植的可能,那你大可不必考虑可移植的问题,毕竟移植也需要时间和精力。







