-
2004-06-28
我的J2EE误区
最近参加了一个J2EE的培训,培训本身并没有多少新鲜东西,不过,在培训的过程,我发现了自己原来对于J2EE中不少概念的认识并不那么正确或是正统。
首当其冲的就是JNDI。我之前一直认为JNDI就是为了编写EJB、数据源、JMS等客户端调用代码而附加的讨厌东西。实际上,我们完全可以把JDNI看作一个大规模的Service Locator,我们可以把自己感兴趣的一些东西配置成JNDI的一部分,利用统一的方式进行访问。
正是有了JNDI的透明访问,我们才能写出可移植的分布式代码。
在《恶斗EJB》中,我费了九牛二虎之力,实际上都是与JNDI相关参数打斗。应该说,可以正常运行的代码证明了我的努力没有白费,但实际上,我的做法有些画蛇添足。
通过设置各种参数,我实际***问的是远程的JNDI,然后得到EJB,这么做固然可以运行,但由于设置参数的原因,我们的代码肯定是无法在应用服务器间移植的。
+--------------+ +--------------+
| ---|->| JNDI |
| Client | |--------------|
| ---|->| EJB |
+--------------+ +--------------+正统的做法是我不必在自己的代码中设置参数,而只是访问本地的JNDI,由本地的JNDI和远程的JNDI之间进行映射。这样我的代码就成为了可移植的代码,至于本地JNDI和远程JNDI怎么映射,那就是具体应用服务器的问题了。
+--------------+ +--------------+
| JNDI ---|->| JNDI |
|--------------| |--------------|
| Client ---|->| EJB |
+--------------+ +--------------+另外一个我一直没有搞清楚的就是在应用中,JNDI名字通常还对应一个引用名字。我总是认为有了JNDI名字就够了,要这个引用名字是多余的。
如果说JNDI名字相当于全局变量的话,那么引用名字就相当于一个局部变量。这样做的好处何在呢?还是可移植性的问题。
当我们把应用部署到一台新的应用服务器上时,由于JNDI名字的全局性,这个JNDI的名字可能已经被别人抢先占住,我们只有曲线救国,把自己应用用到的JNDI名字改一下。如果在代码中,直接使用JNDI名字的话,改变JNDI名字势必要修改代码。如果我们使用引用名字,在同一个引用中保证引用名字的唯一性,我们还是能够做到的,这样我们只要修改引用名字和JNDI名字的映射,一切就迎刃而解了。还有一个认识上的大错误是关于应用客户端的JAR文件。在《恶斗EJB》中,我曾质疑导出这个JAR文件还要我部署到服务器上。实际上,这么做是必须的。
不同于常见的Web应用直接把WAR文件扔到应用服务器相关目录下就算完成部署,如果直接把含有EJB的EAR文件扔到应用服务器对应的目录下,通常是无法运行的。我们都知道,编写会话Bean和实体Bean时,我们会写两个接口(这里就不区分是是本地)和一个类,显然有接口就要有实现,只不过这两个接口的实现不是我们完成,而是由应用服务器完成,这就牵扯到讨论EJB常说的Stub和Skeleton。我们不能指望不同的应用服务器生成相同的Stub和Skeleton,而Stub和Skeleton的生成工作通常会在部署的时候完成,所以,只有部署之后,才会有一个有意义的Stub和Skeleton,这时产生的客户端的JAR文件才是有意义的。总而言之,J2EE太复杂,不仅仅是J2EE规范本身就很庞大,编写代码就很费劲,而且部署也是一个很有学问的工作,难怪J2EE中还有专门负责部署的人。《Unix程序设计艺术》的第二章回顾Unix历史时说,第一个系统一般都做得不怎么样,第二个系统为了改进第一个系统的问题就会拼命的完善,最后导致被自身重量压得坍塌了,只有到第三个系统,整个设计才会回归简单。由此推算,以EJB 3.0为主的新版J2EE还是值得期待的。
-
2004-06-24
重载游戏
看看下面这个程序,在运行之前,先估计一下结果是什么。
public class OverloadGame {
public static void main(String[] args) {
test(null);
}public static void test(Object o) {
System.out.println("test Object");
}public static void test(Object[] oa) {
System.out.println("test Object array");
}
}先跟着我往歧途上走一段。
这是一个演示重载用法的小程序,重载的精华就在于不同的方法可以使用相同方法名,从某种意义上来说,这也是一种多态。JUnit中一大堆assert方法应该算是重载用法的一次集中展示,正是有了强大的重载功能,当我们写下这样的代码:
assertEquals(expected, actual);
我们通常不会去仔细考虑,expected和actual究竟是对象还是基本类型。在Java中,区分重载方法是在编译阶段完成的,所以,了解一下Java代码如何编译,上面的问题就可以迎刃而解。关于Java语言的一切,还有谁比Java语言规范更有说服力呢?Java语言规范的15.12节讨论的就是如何确定调用哪个方法。
让我们直取要害,看看如何区分重载方法。
在确定了有多个可用的方法之后,Java语言确定具体方法时,使用了一条“most specific method”的规则从众多的候选方法中选择最合适的一个。如果一个参数能够匹配多个方法的声明,那就越贴切越好,比如一个Circle对象,遇到的方法有用自己直接父类Shape,也有用Object,既然自己和自己的父类这么近,就不必麻烦老祖宗Object了。基本上可以用“有子类不用父类”简单的概括这以规则。
在参数类型与众多方法声明的参数类型无法准确匹配的时候,这个规则意义更大,一旦可以准确匹配,那那个方法当然就是首选了。回头看看,前面的程序,null匹配谁更合适呢?
在Java中,null可以代表任何对象,所以它匹配Object一点问题都没有,至于Object数组,算了吧,我要一个人,不用一个连。说一千,道一万,不如实际跑一遍:
test Object array
怎么会这样?前面振振有辞的分析,怎么没有换来理想的结果?把Java语言规范翻到第十章《数组》,这章开篇写道:“在Java程序设计语言中,数组就是对象。”
不错,数组就是对象,由此得出结论Object[]是Object的子类,我知道,从情感上来说,这很难让人接受,人多势众反而不如原先地位高。虽然心理上很难接受,但这是事实。
接下来的就好解释了,前面说过,有子类可以匹配,就不会麻烦父类,因为Object[]是Object的子类,所以,编译时,优先选择Object[],而非前面预期的Object。这个游戏还可以接着往下做,我们可以尝试在类中再加入一个方法:
public static void test(String oa) {
System.out.println("test String");
}
不用运行,因为编译时就会出错。
根据前面的分析,Object首先出局,就看Object[]和String那个更适合null的发挥,遗憾的是,它们俩根本不是一路人,null也不知道跟谁走更合适了,只好报错,于是,著名的ambiguous呈现在我们面前。 -
2004-06-09
初始化游戏
我们有两个类,A中的一个属性是B的实例,代码如下:
public class A {
private B b;
...
}public class B {
...
}以Spring完成代码时,通常我们会选择以Setter Injection将B的一个实例注射进去。不过,那么做法会使我们的游戏失去意义,我们换一种方式。
在Spring的配置中,可以使用init-method定义一个初始化方法,这个方法会在bean构造的过程中执行,这就给了bean一个执行初始化动作的机会。我们就选择在这个初始化方法中获取B的实例。
随之而来的问题是,从哪获取。这个问题很好解决,做一个全局的访问点,我们可以通过它获取bean的实例,下面是一种实现方式:
public class Global {
private static ApplicationContext ctx;static {
try {
ctx = new ClassPathXmlApplicationContext("config.xml");
} catch (Throwable t) {
t.printStackTrace();
}
}public static Object getBean(String name) {
return ctx.getBean(name);
}
}
这里,我们用到Spring中ApplicationContext,接下来一切就简单了,A的初始化方法通过它来获取B。
public void init() {
b = (BeanB)Global.getBean("b");
}补上我们欠下的配置文件,这样才显得完整。
<bean id="a" class="A" init-method="init"/>
<bean id="b" class="B"/>好了,加上启动代码一切就万事OK了。
public static void main(String[] args) throws Exception {
BeanA a = (BeanA)Global.getBean("a");
}可以跑了,运行结果怎么样?如果一切正常的话,出现在我们你看到的将是空指针异常,位置就在getBean的ctx那里。
怎么会这样?从输出信息上,我们没有看到ctx初始化失败的信息,所以调用ctx的时候,它应该已经初始化成功,难不成Java的new会new出空来,这不是一个超大规模的应用,内存不会这么轻易被耗尽吧!
我还是不要把大家在错误的方向上引得太远。
仔细看一下异常信息,调用getBean就是我们的init方法,换句话说,是在A初始化的时候出现了异常。答案很简单,因为A的初始化是在ctx构造的过程中完成的,所以,A在初始化的时候ctx还没有构造完成,所以,当然赋值操作还没有做,所以ctx依然为空,这时候的调用当然会出现一个空指针异常了。也许你会质疑这种做法的必要性,其实我也怀疑。不过可以肯定的是,这段代码不是我凭空杜撰出来的代码,我的一个同事确实写出了这样的代码,也确实遇到了这个问题。
事以至此,怨天尤人没有用,还是考虑一下怎么解决问题吧!出问题的主要原因是在A的init方法中,调用尚未完成构造的ctx。如果ctx能够完成构造,这个问题也就迎刃而解了。怎么能在调用A的init方法之前完成ctx的构造呢?我们换一个角度来考虑问题,如果ctx的构造时A不进行初始化,问题不也就解决了。也就是说,我们需要推迟A的初始化。
推迟A的初始化……
出现在我大脑中的是lazy-init的概念,不错,Spring支持lazy-init的概念,也就是将初始化推迟到不能再推的时候再去执行。我们可以不修改一行代码,而只修改配置文件就支持lazy-init的概念。
<bean id="a" class="A" init-method="init" lazy-init="true"/>
再运行一下,世界从此清净了。如果不能lazy-init怎么办?比如说我们会在init方法中启动一个服务监听来自客户端的请求,我们需要它在系统开始运行的时候就启动,lazy-init可能会让它在系统启动后丢掉许多请求。
从前面的例子可以看出,我对这个问题没有更好的答案。不过,还回到前面提过的一个话题上,这种写法本身的合理性就值得质疑,如果改成Setter Injection,这些问题也就不是问题了。如果真有这种需求,我们何必在一棵树上吊死呢?
-
2004-06-05
没有工具的重构
近来的业余时间是在Linux上用vi写着C程序。
我的开发习惯是先把功能完成,而不必理会代码是否优美,内在结构是否合理,因为有重构在后面等着。
这习惯完全是长时间使用Java IDE被惯出来的,毕竟Java IDE的重构功能实在太方便了。至少在常用的几项重构,比如改名、比如提取方法,无论是表现一般的JBuilder、表现尚可的Eclipse都能尽职尽责,更不用说表现优异的IntelliJ IDEA了。带着同样的习惯来到Linux上,打开vi,写着C程序。拼凑完功能,准备招手重构时,忽然发现原有的习惯并不适合这个新的环境。一个变量名,我只能一路手工清扫过去,然后靠编译信息确定是否还有残留余孽。幸好只是一个小程序,否则估计我只能罢手和吐血二者之间抉择。
忽然发现,原来重构已经成为我开发生活中不可或缺的一部分。偶尔也想,那些没有重构的日子我是怎样度过的。
感谢Martin Flower的那本《refactoring》将重构的理念送入我的大脑。唯一感觉对不起这本书的是,虽然我早早就拥有了这本书,但迄今为止,我依然未能将全部的重构手法读完。因为在我眼中,了解了重构的意义之后,这本书最有价值的就是那些Bad Smell了。发现问题远要比解决问题来得更加困难,只要能嗅到Bad Smell,如何重构就是个人游戏了。
Martin Flower写这本书的时候,重构工具还不是很丰富,按照书中介绍的重构手法一步步走下去,虽然可行,却也费神。时过境迁,随着人们越来越重视重构,重构渐渐成为开发人员的基本技能,于是,重构工具也如雨后春笋一般一个个冒了出来。作为开发人员好助手的IDE自然也不甘落后,这一次,Java IDE终于走在微软的Visual Studio之前。在Java程序员享受着重构的便利之时,Windows平台的程序员只有眼巴巴羡慕的份。微软当然不会视而不见,据说那个叫Whidbey的家伙将支持重构。
正是IDE们的精彩表现,完成一个重构之后,我甚至可以放心大胆不去运行单元测试,这就是自动化工具带来的便利,比起手工重构,不知节省了多少时间和精力。
手工重构?不,不到万不得已,我不愿意。 -
2004-05-28
恶斗EJB(五)
从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又是一场恶斗,虽然精疲力竭,却也心情愉悦!战斗后的胜利感觉,总是最美妙的。







