2005年07月25日

      做J2ME应用开发的程序员也许经常会被一些名词或者概念所迷惑,比如Personal Basic Profile和Personal Profile有什么关系?基于CLDC的应用程序能够无修改的移植到基于CDC的设备上嘛?要回答这些问题并不容易,因为你必须首先揭开J2ME平台的神秘面纱。
  & nbsp;   2000年当J2ME平台的CLDC发布的时候,所有的java开发者都为之兴奋不已。SUN也不负众望在两个月后发布了基于CLDC的MIDP,给开发者提供了开发交互式应用程序的平台。我想现在国内还主要是基于CLDC/MIDP在开发应用吧。J2ME平台的另一个规范CDC在大概10个月后才发布,同时发布的还有Foundation Profile。但是由于他们都没有给开发者提供开发可交互应用程序的API,因此他们发布的影响远远小于CLDC/MIDP的发布。大概在2003年SUN发布了基于CDC的Personal Basic Profile和Personal Profile。他们的组合替代了以前的PersonalJava平台。

    是不是已经被这些名字弄糊涂了,我们现在来看J2ME平台的结构。J2ME平台是本着Configuration和Profile结合来设计的。Configuration是提供支持最大范围设备的最小的平台。Profile是针对特定的设备提供相应的开发包集合。在J2ME的两个基本配置CDC和CLDC是按照如下的标准进行区分的。
CLDC:

512 KB 以下内存
有限能源供应(通常使用电池)
有限或非持续网络连接
简单的用户界面
16位或者32位的处理器
CDC:

2M以上内存
具有网络连接能力,通常为无线网络
需要实现java虚拟机规范的全部功能
32位或者64位的处理器

    从上述的要求中我们不难看出CLDC主要针对那些资源非常受限的设备比如手机、PDA、双工寻呼机等。而CDC主要面对那些家电产品,比如机顶盒、汽车导航系统等。
    下面我们来看看J2ME平台的结构图:

         
    我们可以看出J2ME的核心是Configuration,在它里面定义了java的虚拟机,通过它来和底层的Host OS打交道。Profile提供了访问设备的IO或者图形界面的能力,这样Configuration和Profile共同构成了J2ME的运行环境。比如CLCD/MIDP可以提供给你开发手机程序的环境。在Profile之上针对不同的设备还可以提供不同的可选开发包。
    由于CLDC/MIDP大家都比较熟悉了,我们下面主要介绍一下基于CDC的J2ME系统构架,同样还是看它的系统图:
      

    基于CDC的设备可以支持全部的Java2语言规范和Java虚拟机规范,它是CLDC的超集并且远大于CLDC,因此基于CLDC的程序是可以无修改的移植到基于CDC的设备的。Foundation Profile是对CDC进行扩展的Profile,但是他并没有提供GUI的开发包,GUI是在Personal Basic Profile中进行定义的,它提供了AWT的一个子集给开发人员。它还提供了Xlet应用程序模式——针对java TV。Personal Profile是Personal Basci Profile的超集。他提供了对Applet的支持,丰富了AWT的组件,添加了java.awt.datatransfer包。
     总结:上面是我对J2ME平台进行的阐述,我想最重要的就是Configuration/Profile的设计。理解了它就掌握了一半。如果大家有不同的意见,欢迎一起交流!

2005年06月08日

        out momory 一阵天旋地转内存又溢出了。在手机上这种痛苦经常都有,套一句俗话在手机上用内存必须勒紧裤腰带。虽然现在pc内存上G都不奇怪,可是在手机上却只能以K来记,可能某位同志会马上跳出来说也有上M的,记住中国还不富大多数手机都是低端手机。写手机程序让我仿佛回到了dos时代(自我安慰一下那个时代也炼出了不少高手说不定我是下一个)。言归正传做内存优化可以归结为以下几种方法。代码优化,图片优化,第三方工具优化等…

一.代码优化
 内存会溢出肯定和代码逃不了关系,99.99%学java的人都知道垃圾回收器是java的一大优点并据此来嘲笑C++。显然这个特性为代码编写者省了不少事,但这个特性却带来了不少隐患。举个例子在游戏当中经常有不同场景的切换,如从游戏逻辑退到主菜单逻辑,对游戏逻辑对象的态度很多人会选择忘记等待垃圾回收器来收尸^_^。乍看之下似乎并无不妥垃圾回收器会来善后。实际上垃圾回收器并非实时的,它不像C++的Delete语句马上释放不用的内存。当从游戏逻辑切换到主菜单逻辑这时两个对象同时存在很可能这时内存就不够用了。读到这里很多人会发现实际上垃圾回收器在j2me上并不怎么好用,从一个角度上来讲在j2me上所有垃圾必须由手工释放,除简单类型以外所有对象都必须显式地置空例如 imgs=null;  实际上java提供了一个不错的工具用来查找内存溢出,java.lang.Runtime.freeMemory() 。它可以返回当前的剩余内存数,将它适当的安放在代码中可以有效的监测内存使用状况。很大一部份的j2me程序员之前都是从事pc软件开发工作,充裕的内存掩盖了许多写代码的不良习惯。如下所示:
     //a 不为空
     a=new Logic();
很多人可能对此有异议,他们会认为新的对象会把旧的对象冲掉并且释放内存。这里面包含两个问题:1. 该段代码是先创建对象然后再进行赋值操作的,也就是说在这期间有两个对象同时存在这就很可能会产生溢出。2. 这样做也会妨碍垃圾回收器的工作
较好的写法如下:
            a=null;
            a=new Logic(); 
虽然麻烦了点但在j2me中还是必要的。接着看下例。
  drawString("游戏时间:" + time ,50,50,Graphics.LEFT|Graphics.TOP);
 "游戏时间:" + time  很完美在paint()方法当中每次都被刷一遍显示在屏幕上。危机往往隐藏在美丽的外表,该语句会引起新的内存重新分配来存储 "游戏时间:" + time   而显示完以后又必须由垃圾回收器释放,用了双倍时间,并且容易发生内存溢出。依此类推在重复执行的方法里应尽量避免重复定义对象。与paint()方法类似在循环里也有类似的情况存在。 
 把所有对象的初始化放在构造函数里想必是再正当不过了,大多数人通常的做法是把当前逻辑所要用到的资源通通初始化完毕。
很大一部份的内存溢出都是发生在构造函数中。内存使用的高峰期都是在构造函数中所以避开这个高峰能有效的防止溢出。建议最好的办法是第一次使用时初始化。如下所示
    if (img==null){
        //初始化
    }
 现在做游戏很多时候都需要地图数组,声音数组,还有一些其它资源这些资源很多可以放在代码中也有的可以放在文件当中。
强烈建议将这些资源放在文件中需要时在load进来。这些资源文件如果放在代码中则会占用不小的代码段空间,而代码一般是程序一运行就装载到内存当中。
 除上面列举的方法外还有一些大家所熟知的顺便一提, 比如关闭没用的rms ,关闭没用的网络连接,关闭没用的流。正确地停止线程。良好的程序架构减少代码偶合性也是一个不错的方法,无论在代码调式,内存释放都可以做到非常清析。

二. 图片优化
 j2me的内存杀手无疑非图片莫属,一张3k的图片可以占用20多k的内存不信大家把load前后的内存剩余打印出来对比看看。所以防止内存溢出最直接的办法就是从图片入手。
1.图片压缩: 多数人马上会想到这个办法。不错这个办法是最有效的。在photoshop里图片制作完成后不要选择 "存储为",而是选择 "存储为 web 所用格式"  可以根据里面的选项进行压缩,特别是颜色这一项越小越好不过相应的图像会有所失真。不要认为这样就完了。
实际上该图片还可以再次压缩,在网上有许多类似的工具。推荐一款可以压缩png格式的软件 xat.com Image Optimizer 效果不错。经常都有 70% 的压缩率且图像不会失真。
     假如你有多张规格一样的图片,那么建议你把它做成一张长条图片。有两个原因:
1、 这样节省存储空间和内存空间。大家可做个试验将10张图片的内容放在一张当中对比看看文件大小有没有变化。
2 、10张图片需要10个image 对象需要进行10次io操作浪费时间不说还浪费内存。当笔者发现这个好处时兴奋地把所有图片都存成一张,吱地一声内存又溢出了…原因想必大家也知道!!图片太大了不要把不同界面的图片整合在一起否则经常会得不偿失。
 作图时还有一些细节需要注意,颜色数量,分辩率,图像模式(最好是索引颜色),画布大小都会影响到图片大小。

三. 工具优化
 谁都知道混淆器是用来保护代码的以加大反编译的难度(个人认为这是在嘲笑程序员的智商)。实际上用它来优化程序也是不错的选择,至少有两点好处:1、 压缩程序大小。一个60k的程序经常可以压掉10k左右。10k的空间对于写低端手机的程序员简直是雪中送碳,多少超过64k限制的游戏都受过它的恩惠;2、节省内存空间。用脚去想也想得出来代码少了内存里的代码段自然就短了。
 根据经验很多人都会用jb自带的混淆器RetroGuard,实际上它效果并不怎么好。推荐使用proguard 在 http://sf.net 可以免费下载,它可以比retroguard 多压缩3至4 k 以上而且安全性更好。

2005年04月20日

  Buoy 是一个构建在 Swing 之上的免费用户界面(UI)工具包,它为 UI 开发人员提供了方便性和简单性。在本文中作者用一个简单的 fractal 用户界面程序,介绍了 Buoy 可以做什么、为什么这么做。

  第一次尝试用 Java 语言构建简单的用户界面时,我对 Swing 接口的复杂性感到有些惊讶。老实说,有点想打退堂鼓。最近,一个朋友向我提到,他使用的渲染程序 Art of Illusion(请参阅 参考资料)基于一个不同的工具包:Buoy。推荐它的原因之一是它的界面更友好。当他第一次提到它时,我以为他在谈 "BUI",而它与 GUI 这个名字的相似是故意的。在这里 B 代表 better(更好),但是名字 Buoy 并不是缩写。

  Buoy 是免费的。实际上,它是公共的东西。它并没有在某个开放程度合理的许可下发布,实际上它根本不受任何许可控制。这意味着在任何用 Java 语言编写的能够运行 Buoy 的项目中都可以使用 Buoy,而不用考虑许可问题。因为提供了完整的源代码,所以这个工具包很容易修改和扩展。本文基于 Buoy 1.3 发行版,要求读者对 Swing 有基本的了解,虽然不了解也能对付过去。

  示例程序

  我曾经尝试用 Swing 构建的第一个应用程序最后以失败告终。为了看出工具包之间的对比情况,我决定使用 Buoy 来构建这同一个程序。文章中的代码示例全部来自该程序的 Buoy 版本。程序生成了一些分形,具体地说,是迭代的分形。基本思想很简单:在平面上定义一系列的线条区段,从(0,0) 到(1,0),围绕任意一个单位线条定位。绘制这些区段之后,绘制同一套变形线条,用这个区段作为单位向量。做起来比说的更容易,就像在图 1 中看到的。



图 1. 分形编辑器中的分形


  这个程序的界面相当简单。它有一些界面小部件,有一个画布,在画布上绘制漂亮的图片,还支持用鼠标操纵图片。实际上,必须要做的全部工作就是操纵构成原始曲线的点,原始曲线会迭代地绘制出来。界面还有一个最小化的菜单;它可以打开和关闭文件,关闭窗口,或者把当前图像保存为 PNG 格式的文件。虽然简单,但是这个界面简要地提供了一个 Buoy 小部件的合理示例,还有相当数量对事件处理系统的体验。

  程序实际的核心代码 —— 分形生成器 —— 已经写好了,这把这个示例变成一个很好的测试程序。当然,在更新它的过程中,我也发现并且修补了一些 bug。

  发行包中包含示例程序的源代码,还有编译好的类文件和 Buoy 的 JAR 文件(单击本文顶部或底部的 Code 图标,下载 factal.tar)。包中还包含一个叫做 frac 的目录,里面包含一些示例分形。如果使用一台 UNIX 风格的机器,在路径中有 Java 编译器,那么只要运行 make 就能运行它。否则,需要设置 classpath 包含当前路径和 Buoy 的 JAR 文件所在的目录,然后运行 FractalViewer 类。在 Windows 系统上,正确的命令行应当是 java -classpath .;Buoy.jar FractalViewer。

  sed -e s/J/B/g

  在第一次深入研究代码时,也许会形成这样的印象:把 Swing 代码转换成 Buoy 代码简单得就像把 UI 元素名称中的字母 J 换成 B 一样简单。例如, FractalViewer 类不再扩展 JFrame;它现在扩展的是 BFrame。主要的小部件名称也可以照此推测得到。Spinner 和 slider 像以前一样有相同的名字,只是换了一个字母。 MenuBar(菜单条) 仍然由 Menus(菜单)构成,菜单则容纳 MenuItems。

  有些命名转换略有不同。在 Swing 引用 BorderLayout 的地方,Buoy 有 BorderContainer。一般来说,Buoy 的命名转换相当统一,虽然不总是与 Swing 的命名一样。一个明显的区别是 Buoy 几乎组合了容器和布局管理器的概念;每种容器类型都知道自己如何布局。这大大简化了设计。例如,在分形生成器中使用的 LabelWidget 类是一个 BorderContainer;在 Swing 中,这可能是一个带有 BorderLayout 布局管理器的 JPanel。

  但是,两者还是有许多相似之处。这对适应新东西有很大帮助。更重要的是,Buoy 构建在 Swing 之上。这意味着,一般来说,如果需要做的事不能轻松地用 Bouy 完成时,可以把 Buoy 对象传递给它包装的 Swing 对象。对于这种情况,如果想访问一些没有 Buoy 对应物的 Swing 对象,可以简单地把它包装在 AWTWidget 对象中,这个对象提供了非常薄的包装器,通过它,不仅 Buoy 自己的小部件,而且所有的小部件都能访问 Buoy 的小部件 API。例如,如果发现确实需要 GridBagLayout,可能就需要这样做。

  例如,FractalPanel 类是一个 AWTWidget。在早期设计中, 它是 JPanel 的子类, 但实际上我并不需要 JPanel 代码。相反,我构建了包装定制类的类 FractalCanvas, 它本身是普通的 Canvas 类的一个子类。把它变成一个 AWTWidget,就可以在它上面利用 Buoy 高效的事件处理机制。

  事件处理代码非常简单。在按下鼠标按钮时,通过 addEventLink() 的魔力,Buoy 发送一个新的 MousePressedEvent 事件到 mousePressed() 函数。我忽略了按下哪个按钮这个问题,只考虑按住 shift 单击或普通单击。普通单击选择最靠近的点,而按住 shift 单击则重新把显示居中。然后,如果鼠标移动,那么每次 Buoy 注意到移动时都会开始发送 MouseDraggedEvent 事件。在处理这些事件时,FractalPanel 会生成自己的事件。

  近观 PointChangedEvent

  为了让一些讨论更加具体,请来看 PointChangedEvent。这是一个试验性的类,如果不喜欢它,那也只能怪老天了。这个类的想法是:让一个类来表示状态点中的变化。编辑器跟踪“当前”点 —— 也就是编辑器小部件目前正在编辑的点。可以用这些小部件或在分形面板中单击选择新的点,选择的是最靠近的点。

  我得出这样一个结论:在代码中,大概有三类涉及到点的事件需要从一个类发送到另一个类。

  一个是改变某个点的特征: POINT 事件类型。如果由编辑器发送,就是告诉分形改变原型线条上的点,并要求重画线条。如果由分形发送,则是告诉编辑器刚刚选中的点的特性。

  下一个是选择某个点。可以按索引或位置进行选择。所以,如果只提供了索引或位置,那么构造函数会认为意图是填充其他值。有一点特殊的地方,点索引 -1 用来表示没有选中的点,所以必须用 -2表示编辑器正在寻找指定位置的点。这可能不漂亮,但是有效。

  有点意思的是 Fractal 类响应 SELECT 事件的方式。如果成功地选择了一个点,就会发回一个新的 POINT 类型的 PointChangedEvent 事件,如清单 1 所示。

  清单 1. 用事件回答事件


case PointChangedEvent.SELECT:
if (e.getIndex() >= -1)
selectPoint(e.getIndex());
else
selectPoint(e.getPoint());
// just in case they don’t know
event(new FractalChangedEvent(FractalChangedEvent.SIZE, size));
if (selectedPoint >= 0 && selectedPoint < size)
event(new PointChangedEvent(selectedPoint, points[selectedPoint]));
else
event(new PointChangedEvent(selectedPoint, null));
event(new FractalChangedEvent(FractalChangedEvent.REDRAW));
break;


  最后,移动点是一个特殊情况,如果不需要改变点的其他属性(例如颜色),那么所要处理的就是位置。这就是 MOVE 事件类型。在效果上,它与 POINT 事件类型效果很像,但它不需要事件生成器(通常是 FractalPanel 类)去关心那些它根本不知道的属性。

  INSERT 和 DELETE 事件类型只有部分相关,可能应当属于 FractalChangedEvent 事件。

  事件处理

  正如已经开始看到的,事件处理是 Buoy 与 Swing 最明显的不同之处。事件处理提供了大量灵活性。Buoy 本身的事件集相当丰富,且允许您挑选自己感兴趣的事件,从任何小部件向其他对象发送事件。例如,如果想在 Swing 中捕获鼠标事件,捕获事件的类需要实现 MouseListener 接口。这个接口有 5 个函数需要实现,即使它们就是摆设也必须实现。而且必须使用接口提供的函数名称。更糟的是,函数必须是侦听器接口的公共部分;要么把这作为公共接口的一部分公开,要么创建一个什么都不做、只是包装事件侦听器代码的内部类。

  在 Buoy 中,每个小部件都是 EventSource 。这意味着可以从每个小部件侦听事件。什么类型的事件呢?任何类型都可以。关键的函数是 addEventLink()。这允许您指定类、侦听器以及可选的方法。每当 EventSource 分派这个类或它的子类的事件时,侦听器都会接收到事件,要么是通过一个叫做 processEvent()的方法,要么是通过在开始调用 addEventLink() 时提供的方法名称。提供的函数不能接受参数,也不能接受与指定事件类型兼容的类的对象;父类和接口可以。

  这是一个方便的设置。可以把不同的事件路由到不同的函数或相同的函数。例如,MousePressedEvent 和 MouseReleasedEvent 会被分别处理。在示例程序中,鼠标的按下、释放和拖动分别有不同的线程,如清单 2 所示。注意,这远远超过 Swing 的 MouseListener 所能做的。如果用 Swing 编程的话,就需要实现 MouseListener 和 MouseMotionListener 这两个接口。

  清单2. 只挑感兴趣的事件


this.addEventLink(MousePressedEvent.class, this, "mousePressed");
this.addEventLink(MouseReleasedEvent.class, this, "mouseReleased");
this.addEventLink(MouseDraggedEvent.class, this, "mouseDragged");
[...]
public void mouseReleased(WidgetMouseEvent ev) {
lastCenter = null;
dispatchEvent(new FractalChangedEvent(FractalChangedEvent.SLOW));
setAntiAliasing(true);
}


  mouseReleased() 函数只有最少的工作要做。它只是在 mousePressed() 函数之后进行清理,告诉 Fractal 对象到了开始全面重绘的时候了。

  Buoy 的事件处理还有另外一个有趣的特性。如果愿意的话,可以创建新的事件类型。一个事件类型就是一个类。确实如此。它甚至不需要继承任何类或实现什么。它就是一个类。如果这个类的对象被发送到 dispatchEvent(),那么它或它的父类的侦听器就会被调用。在 Swing 中也可以创建新的事件类型,但是完全要自己进行;必须设计 Listener 接口,还要编写自己的代码生成事件并侦听事件。在示例程序中,设计了 Fractal 类,演示了可以相对容易地把事件处理功能加到任何原有的类中。只需要声明一个 FractalViewer 类用来添加侦听器的事件源 EventSource。FractalViewer 类就会把来自事件源(例如 FractalEditor)的事件链接设置到它们的侦听器,如清单 3 所示。

  清单3. 绑定


private void tieEvents() {
// Set up event handling relations.
addEventLink(WindowResizedEvent.class, this, "layoutChildren");
addEventLink(WindowResizedEvent.class, panel, "repaint");
tieControlEvents();
tieFractalEvents();
tiePanelEvents();
}


  定制事件类一般是为了表示用户行为。在 Buoy 中,一般只通过用户行为,而不是系统接口生成事件 —— 除非自己想显式地调用 dispatchEvent() 自行生成事件。当分形对象以某种会造成字段更新的方式变化的时候,所有部件的控制面板都会得到通知。这样,我们发明一个新类 ParameterChangedEvent,用它表示参数已经变化。或者,如果变化的是选中的点的位置或是索引,就发送一个新的 PointChangedEvent。如果行为足够明显的话,那么事件处理器甚至不需要接受参数。作为事件处理的一个示例,请看清单 4,它演示了 FractalEditor 的 parameterChanged() 方法的开始部分。

  清单 4. 参数发生了变化


void parameterChanged(ParameterChangedEvent ev) {
FractalParameters p = ev.getParams();
int v = ev.getValue();

switch (ev.getType()) {
case ParameterChangedEvent.ALL:
maxSlider.setValue(p.getMaxIterations());
minSlider.setMaximum(p.getMaxIterations());
minSlider.setValue(p.getMinIterations());
maxSlider.setMinimum(p.getMinIterations());
zoomSlider.setValue(p.getZoom());
break;
[...]


  在这个例子中,用事件处理系统把各种信息前后传递。在以前的版本中,每个类都有对其他每个类的引用,而且乱七八糟的 get 方法是按天排序的。而在目前的版本中,Buoy 的事件处理系统被用来处理各种通知。例如,FractalChangedEvent 类可以用来让代码的其他部分知道对分形的修改,可能是点的数量变化(编辑器用点的数量为点选择器定义正确的 SpinnerNumberModel),或者是需要重绘的通知,如清单 5 所示。

  清单 5. 显然到了重绘的时候


public void fractalChanged(FractalChangedEvent e) {
switch (e.getType()) {
case FractalChangedEvent.REDRAW:
repaint();
break;
}
}


  Buoy 的文档详细讨论了 Swing 事件模型与 Buoy 事件模型的差异,以及这些差异的原因。有很好的理由,而且 Buoy 的模型通常会导致更小、更清晰的代码。当然,仍然可以做多余的或愚蠢的事情,就像在任何系统中都可以做的那样,但是至少在做这些事情的时候有一个干净漂亮的界面。

  学习曲线

  我曾经观察到,学习使用一个 GUI 工具,一下午的时间还不够长。对于 Buoy,我大概需要 6 个小时或者差不多一整个工作日。我确实从更有经验的 Buoy 用户那里得到了很棒的帮助。以前学习 Swing 的经验也是有帮助的,但实际上,我并不认为 Swing 的经验是必需的。Buoy 的文档相当好,而它的简单性确实有帮助。对于基本的 UI 事物,没有太多要学的东西。

  Buoy 的文档并不像 Swing 文档那样完整,但是覆盖了许多细节,而且非常好。另外,源代码也在那儿,所以回答一些关于界面的简单问题非常容易。具有更完整的文档当然是好事。但是,既然这个项目放在 SourceForge 上,所以如果您愿意,您可以编写更多的东西为它做贡献。

  Buoy 的学习曲线比起 Swing 是一个很大的优势。用相当简单的界面就能让大多数界面小部件正确工作。要使用 Buoy 文档中的一个示例:在 Swing 中,JList 要求要么使用静态列表,要么构建一个实现 ListModel 接口的新类。在 Buoy 中,只需向列表中添加项目;在大多数常见情况下,艰巨的工作已经由 Buoy 替您做了。

  Buoy 相当小。完整的发行包中包含源代码、JAR文件和文档,总共不到 1 MB。代码的组织良好,可以容易地找到任何特定的代码段,如果需要调整设计,也不困难。

  Bug

  尽管 Buoy 是一个稳定、有用的系统,但并不是一个绝对完美的东西。偶尔在明显选择很合理的地方它也会有奇怪的表现,产生令人惊讶的行为。如果考虑用 Buoy 来完成一个实际的项目,就需要了解 bug:它们的普遍程度、严重性,以及克服它们的难度。

  在开发这个应用程序的过程中,我碰到一些事情,当时看起来像是 bug。但不全是。有一些可能是文档中的 bug —— 在这些情况中,代码的行为不是预期的,但是却非常合理。实际上,我可以非常肯定,从实质上讲并不是 Buoy 中的 bug,但它们确实呈现了在调试 Buoy 应用程序时可能会遇到一些事情。在调试了几天代码之后,我可以非常肯定,我遇到的每个明显的 bug,要么是我的错误,要么是我不太喜欢的底层 Swing 中的设计决策。可以肯定地说,在 Swing 中不可能避免这些问题。

  滚动条刻度

  早期我最常遇到的一个 bug 是处理标尺中的刻度标记的时候。最初,我无法得到在它们上面显示的标签。

  清单 6. 让人郁闷的滚动条代码


minSlider.setShowLabels(true);
minSlider.setMajorTickSpacing(2);


  清单 6 中的代码不起作用。可以看到,标签只是在设置了刻度间距后才显示。如果在告诉滚动条显示标签之前没有设置刻度间距,它不显示任何刻度就结束了。更微妙的是,随后也不能改变刻度间距;改变刻度间距的尝试没有效果。但是,这实际不是 Buoy 的 bug,而是 Swing 的工作方式。由于 BSlider 类只是把请求传递给 JSlider,所以责怪 Buoy 是不公平的。

  一个更微妙、也与底层 JSlider 的毛病有关的 bug 发生在对齐刻度的行为上。 BSlider 的构造函数把次要刻度设为5,把主刻度设为 20 —— 相对于默认尺寸 100 来说这两个值是合理的。但是,当用 1-10 的范围创建滚动条时,却看不到次要刻度,因此只能把主刻度间距值设为 1。结果产生一个刻度值为 1-10 的滚动条,而且只停留在 1 和 6 处;对齐刻度的行为妨碍了采用其他的值,因为对齐到了次要刻度而不是主刻度。

  虽然这个问题源自 JSlider 的实现,但却在 Buoy 的默认行为中发生了,在即将发布的 1.4 发行版中会修复它。

  注意,这对我是个问题的惟一原因是,示例程序要不断地更新一些滚动条的范围。例如,如果有一个线条区段,只允许进行最多 50 次迭代,那么要在滚动条上标上每个数字的工作量可能有点多。另一方面,如果只允许少数迭代,那么遗漏某些数字看起来就不好了。在一个滚动条范围不常更新的界面,还是很方便的。

  菜单快捷键

  Buoy 用字符或键盘事件(KeyEvent)方便地为菜单快捷键提供了构造函数。在第一次测试它时,我没法让它工作。看起来必须使用小写字母;但在调用构造函数时必须用 ‘W’ 代替 ‘w’,如清单 7 所示。

  清单 7. 添加 close 菜单项


mi = new BMenuItem("Close", new Shortcut(‘W’));
mi.setActionCommand("close");
mi.addEventLink(CommandEvent.class, this, "menuEvent");
fileMenu.add(mi);


  这样可能必须要处理 Java 5.0 SDK 与 1.42 不小心模糊重复的地方。表面来看,如果把大写字母传递给构造器,所做的事情正与期望的一样。底层的问题 —— JVM 要用哪一套键或修饰符来表示 Ctrl-? —— 还需要一个小的自由的库才能完全解决。

  文件选择

  出于一些不明显的原因,在 Mac 系统上启动新的文件选择器时,Buoy 默认启动的是根目录。我做了一个详尽的 bug 报告,是关于它看起来是怎样遗漏大量文件的,但是随后我就认识到我已经把我的主目录从 /Users/seebs 移动到了 /Volumes/Home/seebs,而文件选择器确实显示了磁盘上的东西。分数:Buoy 1,Seebs 0。

  我仍然想知道为什么它要从文件系统的根开始。这也许是 JVM 的 Mac 实现的毛病。

  结束语

  Buoy 遵循著名的古老的 UNIX 哲学:百分之十的工作解决百分之九十的问题。Buoy 并不想为所有人解决所有的问题,但是它可以完成界面用户或设计师需要的大部分工作。它拥有可能是最好的许可条款,而且还在不断发展。最好的是,如果发现它不能让您做自己确实需要做的事情,您可以随心所欲地研究它、修改它,要么修改 Buoy 的源代码,要么调用 getComponent() 并编写自己的 Swing 代码。

  如果觉得较大的 UI 工具包太可怕,那么 Buoy 是个不错的选择。它可以让简单的 UI 继续简单,把复杂的代码留到需要的时候。在实践中,对于少数 Swing 比 Buoy 有优势的情况,直接在 Buoy 构建的程序中编写少数代码就能处理。这是一个让我值得花时间用 Java 进行 UI 编程的工具包。

2005年03月09日
  一、引子

  我们去科技市场为自己的机器添加点奢侈的配件,很多DIYer都喜欢去找代理商,因为在代理商那里拿到的东西不仅质量有保证,而且价格和售后服务上都会好很多。客户通过代理商得到了自己想要的东西,而且还享受到了代理商额外的服务;而生产厂商通过代理商将自己的产品推广出去,而且可以将一些销售服务的任务交给代理商来完成(当然代理商要和厂商来共同分担风险,分配利润),这样自己就可以花更多的心思在产品的设计和生产上了。

  在美国,任何企业的产品要想拿到市场上去卖就必须经过代理商这一个环节,否则就是非法的。看来代理商在商业运作中起着很关键的作用。 不小心把话题扯远了,回过头来,那么在我们的面向对象的程序设计中,会不会有代理商这样的角色呢?来看这篇文章的人肯定不会说:没有!

  那么就跟着这篇文章来看看代理模式的奇妙吧。

  
二、定义和分类

  代理模式在设计模式中的定义就是:为其他对象提供一种代理以控制对这个对象的访问。说白了就是,在一些情况下客户不想或者不能直接引用一个对象,而代理对象可以在客户和目标对象之间起到中介作用,去掉客户不能看到的内容和服务或者增添客户需要的额外服务。

  那么什么时候要使用代理模式呢?在对已有的方法进行使用的时候出现需要对原有方法进行改进或者修改,这时候有两种改进选择:修改原有方法来适应现在的使用方式,或者使用一个“第三者”方法来调用原有的方法并且对方法产生的结果进行一定的控制。第一种方法是明显违背了“对扩展开放、对修改关闭”(开闭原则),而且在原来方法中作修改可能使得原来类的功能变得模糊和多元化(就像现在企业多元化一样),而使用第二种方式可以将功能划分的更加清晰,有助于后面的维护。所以在一定程度上第二种方式是一个比较好的选择!

  当然,话又说回来了,如果是一个很小的系统,功能也不是很繁杂,那么使用代理模式可能就显得臃肿,不如第一种方式来的快捷。这就像一个三口之家,家务活全由家庭主妇或者一个保姆来完成是比较合理的,根本不需要雇上好几个保姆层层代理:)

  根据《Java与模式》书中对代理模式的分类,代理模式分为8种,这里将几种常见的、重要的列举如下:

  1. 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个局域代表对象。比如:你可以将一个在世界某个角落一台机器通过代理假象成你局域网中的一部分。

  2. 虚拟(Virtual)代理:根据需要将一个资源消耗很大或者比较复杂的对象延迟的真正需要时才创建。比如:如果一个很大的图片,需要花费很长时间才能显示出来,那么当这个图片包含在文档中时,使用编辑器或浏览器打开这个文档,这个大图片可能就影响了文档的阅读,这时需要做个图片Proxy来代替真正的图片。

  3. 保护(Protect or Access)代理:控制对一个对象的访问权限。比如:在论坛中,不同的身份登陆,拥有的权限是不同的,使用代理模式可以控制权限(当然,使用别的方式也可以实现)。

  4. 智能引用(Smart Reference)代理:提供比对目标对象额外的服务。比如:纪录访问的流量(这是个再简单不过的例子),提供一些友情提示等等。

  代理模式是一种比较有用的模式,从几个类的“小结构”到庞大系统的“大结构”都可以看到它的影子。

  三、结构

  代理模式中的“代理商”要想实现代理任务,就必须和被代理的“厂商”使用共同的接口(你可以想象为产品)。所以自然而然你会想到在java中使用一个抽象类或者接口(推荐)来实现这个共同的接口。于是代理模式就有三个角色组成了:

  1.抽象主题角色:声明了真实主题和代理主题的共同接口。

  2.代理主题角色:内部包含对真实主题的引用,并且提供和真实主题角色相同的接口。

  3.真实主题角色:定义真实的对象。

  使用类图来表示下三者间的关系如下:



  当然,图上所示的是代理模式中的一个具体情况。而代理模式可以非常灵活的使用其他方式来实现,这样就与图上所示有很大的区别。

  也许,现在你已经对代理模式已经有了一个宏观的认识了,下面我们来看看怎么实际的使用代理模式。

  四、举例

  以论坛中已注册用户和游客的权限不同来作为第一个例子:已注册的用户拥有发帖,修改自己的注册信息,修改自己的帖子等功能;而游客只能看到别人发的帖子,没有其他权限。为了简化代码,更好的显示出代理模式的骨架,我们这里只实现发帖权限的控制。 首先我们先实现一个抽象主题角色MyForum,里面定义了真实主题和代理主题的共同接口——发帖功能。

  代码如下:


public interface MyForum

{

public void AddFile();

}


  这样,真实主题角色和代理主题角色都要实现这个接口。其中真实的主题角色基本就是将这个接口的方法内容填充进来。所以在这里就不再赘述它的实现。我们把主要的精力放到关键的代理主题角色上。代理主题角色代码大体如下:


public class MyForumProxy implements MyForum
{
 private RealMyForum forum ;
 private int permission ; //权限值

 public MyForumProxy(int permission)
 {
  forum = new RealMyForum()
  this.permission = permission ;
 }

 //实现的接口

 public void AddFile()
 {
  //满足权限设置的时候才能够执行操作
  //Constants是一个常量类
  if(Constants.ASSOCIATOR == permission)
  {
   forum.AddFile();
  }
  else
   System.out.println(“You are not a associator of MyForum ,please registe!”);
 }
}


  这样就实现了代理模式的功能。当然你也可以在这个代理类上添加自己的方法来实现额外的服务,比如统计帖子的浏览次数,记录用户的登录情况等等。

  还有一个很常见的代理模式的使用例子就是对大幅图片浏览的控制。在我们常见的网站上面浏览图文的信息时,不知道你有没有注意到,图片位置放置的是经过缩小的,当有人要仔细的查看这个图片时,可以通过点击图片来激活一个链接,在一个新的网页打开要看的图片 。这样对于提高浏览速度是很有好处的,因为不是每个人都要去看仔细图上的信息。这种情况就可以使用代理模式来全面实现。这里我将思路表述出来,至于实现由于工作原因,就不表述了,至于这种方式在B/S模式下的真实可行性,我没有确认过,只是凭空的想象。如果不是可行的方式,那这个例子可以放到一个C/S下来实现,这个是绝对没有问题的,而且在很多介绍设计模式的书和文章中使用。两种方式的实现有兴趣的可以来尝试一下。

  我们在浏览器中访问网页时是调用的不是真实的装载图片的方法,而是在代理对象中的方法,在这个对象中,先使用一个线程向浏览器装载了一个缩小版的图片,而在后台使用另一个线程来调用真实的装载大图片的方法将图片加载到本地,当你要浏览这个图片的时候,将其在新的网页中显示出来。当然如果在你想浏览的时候图片尚未加载成功,可以再启动一个线程来显示提示信息,直到加载成功。

  这样代理模式的功能就在上面体现的淋漓尽致——通过代理来将真实图片的加载放到后台来操作,使其不影响前台的浏览。

  五、总结

  代理模式能够协调调用者和被调用者,能够在一定程度上降低系统的耦合度。不过一定要记住前面讲的使用代理模式的条件,不然的话使用了代理模式不但不会有好的效果,说不定还会出问题的

2005年03月01日

  为什么要重构

  从Martin Fowler所著的《重构–改善既有代码的设计》一书连续两年成为最畅销的计算机图书之一,就可以知道重构给程序员所带来的欣喜程度了。

  那么什么是重构呢?重构就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。

  也许有人会问,为什么不在项目开始时多花些时间把设计做好,而要以后花时间来重构呢?要知道一个完美得可以预见未来任何变化的设计,或一个灵活得可以容纳任何扩展的设计是不存在的。系统设计人员对即将着手的项目往往只能从大方向予以把控,而无法知道每个细枝末节,其次永远不变的就是变化,提出需求的用户往往要在软件成型后,始才开始”品头论足”,系统设计人员毕竟不是先知先觉的神仙,功能的变化导致设计的调整再所难免。所以”测试为先,持续重构”作为良好开发习惯被越来越多的人所采纳,测试和重构像黄河的护堤,成为保证软件质量的法宝。

  通过重构可以达到以下的目标:

  ·持续偏纠和改进软件设计

  重构和设计是相辅相成的,它和设计彼此互补。有了重构,你仍然必须做预先的设计,但是不必是最优的设计,只需要一个合理的解决方案就够了,如果没有重构、程序设计会逐渐腐败变质,愈来愈像断线的风筝,脱缰的野马无法控制。重构其实就是整理代码,让所有带着发散倾向的代码回归本位。

  ·使代码更易为人所理解

  Martin Flower在《重构》中有一句经典的话:”任何一个傻瓜都能写出计算机可以理解的程序,只有写出人类容易理解的程序才是优秀的程序员。”对此,笔者感触很深,有些程序员总是能够快速编写出可运行的代码,但代码中晦涩的命名使人晕眩得需要紧握坐椅扶手,试想一个新兵到来接手这样的代码他会不会想当逃兵呢?

  软件的生命周期往往需要多批程序员来维护,我们往往忽略了这些后来人。为了使代码容易被他人理解,需要在实现软件功能时做许多额外的事件,如清晰的排版布局,简明扼要的注释,其中命名也是一个重要的方面。一个很好的办法就是采用暗喻命名,即以对象实现的功能的依据,用形象化或拟人化的手法进行命名,一个很好的态度就是将每个代码元素像新生儿一样命名,也许笔者有点命名偏执狂的倾向,如能荣此雅号,将深以此为幸。

  对于那些让人充满迷茫感甚至误导性的命名,需要果决地、大刀阔斧地整容,永远不要手下留情!

  ·帮助发现隐藏的代码缺陷

  孔子说过:温故而知新。重构代码时逼迫你加深理解原先所写的代码。笔者常有写下程序后,却发生对自己的程序逻辑不甚理解的情景,曾为此惊悚过,后来发现这种症状居然是许多程序员常患的”感冒”。当你也发生这样的情形时,通过重构代码可以加深对原设计的理解,发现其中的问题和隐患,构建出更好的代码。

  ·从长远来看,有助于提高编程效率

  当你发现解决一个问题变得异常复杂时,往往不是问题本身造成的,而是你用错了方法,拙劣的设计往往导致臃肿的编码。

  改善设计、提高可读性、减少缺陷都是为了稳住阵脚。良好的设计是成功的一半,停下来通过重构改进设计,或许会在当前减缓速度,但它带来的后发优势却是不可低估的。

  何时着手重构

  新官上任三把火,开始一个全新的项目时,程序员往往也会燃起三把火:紧锣密鼓、脚不停蹄、加班加点,一支声势浩大的千军万”码”夹裹着程序员激情和扣击键盘的鸣金奋力前行,势如破竹,攻城掠地,直指”黄龙府”。

  开发经理是这支浩浩汤汤代码队伍的统帅,他负责这支队伍的命运,当齐恒公站在山顶上看到管仲训练的队伍整齐划一地前进时,他感叹说”我有这样一支军队哪里还怕没有胜利呢?”。但很遗憾,你手中的这支队伍原本只是散兵游勇,在前进中招兵买马,不断壮大,所以队伍变形在所难免。当开发经理发觉队伍变形时,也许就是克制住攻克前方山头的诱惑,停下脚步整顿队伍的时候了。

  Kent Beck提出了”代码坏味道”的说法,和我们所提出的”队伍变形”是同样的意思,队伍变形的信号是什么呢?以下列述的代码症状就是”队伍变形”的强烈信号:

  ·代码中存在重复的代码

  中国有118 家整车生产企业,数量几乎等于美、日、欧所有汽车厂家数之和,但是全国的年产量却不及一个外国大汽车公司的产量。重复建设只会导致效率的低效和资源的浪费。

  程序代码更是不能搞重复建设,如果同一个类中有相同的代码块,请把它提炼成类的一个独立方法,如果不同类中具有相同的代码,请把它提炼成一个新类,永远不要重复代码。


  ·过大的类和过长的方法

  过大的类往往是类抽象不合理的结果,类抽象不合理将降低了代码的复用率。方法是类王国中的诸侯国,诸侯国太大势必动摇中央集权。过长的方法由于包含的逻辑过于复杂,错误机率将直线上升,而可读性则直线下降,类的健壮性很容易被打破。当看到一个过长的方法时,需要想办法将其划分为多个小方法,以便于分而治之。

  ·牵一毛而需要动全身的修改

  当你发现修改一个小功能,或增加一个小功能时,就引发一次代码地震,也许是你的设计抽象度不够理想,功能代码太过分散所引起的。

  ·类之间需要过多的通讯

  A类需要调用B类的过多方法访问B的内部数据,在关系上这两个类显得有点狎昵,可能这两个类本应该在一起,而不应该分家。

  ·过度耦合的信息链

  ”计算机是这样一门科学,它相信可以通过添加一个中间层解决任何问题”,所以往往中间层会被过多地追加到程序中。如果你在代码中看到需要获取一个信息,需要一个类的方法调用另一个类的方法,层层挂接,就象输油管一样节节相连。这往往是因为衔接层太多造成的,需要查看就否有可移除的中间层,或是否可以提供更直接的调用方法。

  ·各立山头干革命

  如果你发现有两个类或两个方法虽然命名不同但却拥有相似或相同的功能,你会发现往往是因为开发团队成员协调不够造成的。笔者曾经写了一个颇好用的字符串处理类,但因为没有及时通告团队其他人员,后来发现项目中居然有三个字符串处理类。革命资源是珍贵的,我们不应各立山头干革命。

  ·不完美的设计

  在笔者刚完成的一个比对报警项目中,曾安排阿朱开发报警模块,即通过Socket向指定的短信平台、语音平台及客户端报警器插件发送报警报文信息,阿朱出色地完成了这项任务。后来用户又提出了实时比对的需求,即要求第三方系统以报文形式向比对报警系统发送请求,比对报警系统接收并响应这个请求。这又需要用到Socket报文通讯,由于原来的设计没有将报文通讯模块独立出来,所以无法复用阿朱开发的代码。后来我及时调整了这个设计,新增了一个报文收发模块,使系统所有的对外通讯都复用这个模块,系统的整体设计也显得更加合理。

  每个系统都或多或少存在不完美的设计,刚开始可能注意不到,到后来才会慢慢凸显出来,此时唯有勇于更改才是最好的出路。

  ·缺少必要的注释

  虽然许多软件工程的书籍常提醒程序员需要防止过多注释,但这个担心好象并没有什么必要。往往程序员更感兴趣的是功能实现而非代码注释,因为前者更能带来成就感,所以代码注释往往不是过多而是过少,过于简单。人的记忆曲线下降的坡度是陡得吓人的,当过了一段时间后再回头补注释时,很容易发生”提笔忘字,愈言且止”的情形。

  曾在网上看到过微软的代码注释,其详尽程度让人叹为观止,也从中体悟到了微软成功的一个经验。

2005年02月24日
  在前面的所有的实体Bean的例子中,我们所用的主键都是Id,而且都是由容器自动生成的。

  如:


@Id(generate = GeneratorType.AUTO)
public int getId()
{
 return id;
}


  事实上,主键可以是任意的java基本类型、或者是基本类型的包装类,如Integer、String类型、或者是带字段或属性的主键类。注意,如果这样使用主键,你需要将主键的注释中的生成规则改为NONE。就是由程序生成主键值。

  主键类必须实现hashCode和equals方法。

  这个例子前面举例过,就是一个存储学生信息的实体bean,在那个例子中,使用Name类作为属性,并且以Id作为主键,在这个例子中,我们将使用name作为主键,注意name的类型一个java类。和前面的例子一样,我们还是使用Client测试。

  Name.java:主键。

  Student.java:实体Bean类。

  StudentDAO.java:会话Bean的业务接口

  StudentDAOBean.java:会话Bean的实现类

  Client.java:测试EJB的客户端类。

  jndi.properties:jndi属性文件,提供访问jdni的基本配置属性。

  Build.xml:ant 配置文件,用以编译、发布、测试、清除EJB。

  下面针对每个文件的内容做一个介绍。

  Name.java


package com.kuaff.ejb3.composite;

import java.io.Serializable;
import javax.ejb.AccessType;
import javax.ejb.DependentObject;

@DependentObject(access = AccessType.PROPERTY)

public class Name implements java.io.Serializable
{
 private String first;
 private String last;

 public Name()
 {
 }

 public Name(String first, String last)
 {
  this.first = first;
  this.last = last;
 }

 public String getFirst()
 {
  return first;
 }

 public void setFirst(String first)
 {
  this.first = first;
 }

 public String getLast()
 {
  return last;
 }

 public void setLast(String last)
 {
  this.last = last;
 }

 public int hashCode()
 {
  return (first+last).hashCode();
 }

 public boolean equals(Object object)
 {
  if (this == object) return true;
  if (object == null) return false;
  if (! (object instanceof Name)) return false;
  Name name = (Name)object;
  if ((name.first.equals(first)) && (name.last.equals(last)))
   return true;
  else
   return false;
 }

}


  Student.java


package com.kuaff.ejb3.composite;

import javax.ejb.Dependent;
import javax.ejb.DependentAttribute;
import javax.ejb.Column;
import javax.ejb.Entity;
import javax.ejb.GeneratorType;
import javax.ejb.Id;
import javax.ejb.Table;

@Entity @Table(name = “STUDENT”) public class Student implements java.io.Serializable
{
 private Name name;
 private String grade;
 private String email;

 public void setName(Name name)
 {
  this.name = name;
 }

 @Id(generate = GeneratorType.NONE)
 @Dependent(
  { @DependentAttribute(name = “first”, column ={ @Column(name = “FIRST”) }),
   @DependentAttribute(name = “last”, column ={ @Column(name = “LAST”) }) })
   public Name getName()
   {
    return name;
   }

 public void setGrade(String grade)
 {
  this.grade = grade;
 }

 @Column(name = “GRADE”)
 public String getGrade()
 {
  return grade;
 }

 public void setEmail(String email)
 {
  this.email = email;
 }

 @Column(name = “EMAIL”)

 public String getEmail()
 {
  return email;


  StudentDAO.java


package com.kuaff.ejb3.composite;

import javax.ejb.Remote;
import java.util.List;

@Remote

public interface StudentDAO

{

void create(String first, String last, String grade, String email);



Student find(Name name);

List findByFirstName(String name);

List findByLastName(String name);

List findByEmail(String email);



void merge(Student s);

}


  StudentDAOBean.java


package com.kuaff.ejb3.composite;

import java.util.List;
import javax.ejb.EntityManager;
import javax.ejb.Inject;
import javax.ejb.Stateless;

@Stateless

public class StudentDAOBean implements StudentDAO
{
 @Inject
 private EntityManager manager;

 public void create(String first, String last, String grade, String email)
 {
  Student student = new Student();
  student.setName(new Name(first,last));
  student.setGrade(grade);
  student.setEmail(email);
  manager.create(student);
 }

 public Student find(Name name)
 {
  return manager.find(Student.class, name);
 }

 public List findByFirstName(String name)
 {
  return manager.createQuery(“from Student s where s.name.last = :name”).setParameter(“name”, name).listResults();

 }

 public List findByLastName(String name)
 {
  return manager.createQuery(“from Student s where s.name.first = :name”).setParameter(“name”, name).listResults();
 }

 public List findByEmail(String email)
 {
  return manager.createQuery(“from Student s where s.email = :email”).setParameter(“email”, email).listResults();
 }

 public void merge(Student s)
 {
  manager.merge(s);
 }

}


  Client.java


package com.kuaff.ejb3.composite;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.List;

public class Client
{
 public static void main(String[] args) throws NamingException
 {
  InitialContext ctx = new InitialContext();
  StudentDAO dao = (StudentDAO) ctx.lookup(StudentDAO.class.getName());
  dao.create(“晁”,”岳攀”,”8″,”smallnest@kuaff.com”);
  dao.create(“朱”,”立焕”,”6″,”zhuzhu@kuaff.com”);
  Name name = new Name(“朱”,”立焕”);

  //List list = dao.findByEmail(“zhuzhu@kuaff.com”);

  Student s = dao.find(name);
  System.out.printf(“%s %s的email:%s%n”,s.getName().getFirst(),s.getName().getLast(),s.getEmail());
  /*
   for(Object o:list)
   {
    Student s = (Student)o;
    System.out.printf(“%s %s的email:%s%n”,s.getName().getFirst(),s.getName().getLast(),s.getEmail());
   }
  */
 }
}


  这个客户端用来测试。

  请运行{$JBOSS_HOME}/bin目录下的run.bat: run –c all,启动JBOSS。

http://localhost:8080/jmx-console/HtmlAdaptor?action=inspectMBean&name=jboss%3Aservice%3DHypersonic%2Cdatabase%3DlocalDB,然后调用startDatabaseManager()方法,打开HSQL管理工具管理数据库。

  在Eclipse的Ant视图中执行ejbjar target。或者在命令行下,进入到此工程目录下,执行ant ejbjar,将编译打包发布此EJB。

  在Eclipse的Ant视图中执行run target。或者在命令行下,进入到此工程目录下,执行ant run,测试这个EJB。

2005年02月20日

  1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

  2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

  3. Java中的数据类型有两种。

  一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

  另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:


int a = 3;
int b = 3;


  编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

  特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

  另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。 4. String是一个特殊的包装类数据。即可以用String str = new String(“abc”);的形式来创建,也可以用String str = “abc”;的形式来创建(作为对比,在JDK 5.0之前,你从未见过Integer i = 3;的表达式,因为类与字面值是不能通用的,除了String。而在JDK 5.0中,这种表达式是可以的!因为编译器在后台进行Integer i = new Integer(3)的转换)。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。Java中的有些类,如DateFormat类,可以通过该类的getInstance()方法来返回一个新创建的类,似乎违反了此原则。其实不然。该类运用了单例模式来返回类的实例,只不过这个实例是在该类内部通过new()来创建的,而getInstance()向外部隐藏了此细节。那为什么在String str = “abc”;中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有。

  5. 关于String str = “abc”的内部工作。Java内部将此语句转化为以下几个步骤:

  (1)先定义一个名为str的对String类的对象引用变量:String str;

  (2)在栈中查找有没有存放值为”abc”的地址,如果没有,则开辟一个存放字面值为”abc”的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为”abc”的地址,则查找对象o,并返回o的地址。

  (3)将str指向对象o的地址。

  值得注意的是,一般String类中字符串值都是直接存值的。但像String str = “abc”;这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!

  为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。


String str1 = “abc”;
String str2 = “abc”;
System.out.println(str1==str2); //true


  注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。
结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。

  我们再来更进一步,将以上代码改成:


String str1 = “abc”;
String str2 = “abc”;
str1 = “bcd”;
System.out.println(str1 + “,” + str2); //bcd, abc
System.out.println(str1==str2); //false


  这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为”bcd”时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。

  事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。

  再修改原来代码:


String str1 = “abc”;
String str2 = “abc”;

str1 = “bcd”;

String str3 = str1;
System.out.println(str3); //bcd

String str4 = “bcd”;
System.out.println(str1 == str4); //true


  str3这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。

  我们再接着看以下的代码。


String str1 = new String(“abc”);
String str2 = “abc”;
System.out.println(str1==str2); //false


  创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。


String str1 = “abc”;
String str2 = new String(“abc”);
System.out.println(str1==str2); //false


  创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

  以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

  6. 数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。 7. 结论与建议:

  (1)我们在使用诸如String str = “abc”;的格式定义类时,总是想当然地认为,我们创建了String类的对象str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向String类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为”abc”的String类。清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。

  (2)使用String str = “abc”;的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String(“abc”);的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但JDK的内部在这里实现是否应用了这个模式,不得而知。

  (3)当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。

  (4)由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。

  一、创建型模式

  FACTORY?人才市场:以往是要哪个人才,就找哪个人才,效率低,现在有了人才市场,我们只需直接去人才市场挑一个好了;

  BUILDER?生产流水线:以前是手工业作坊式的人工单个单个的生产零件然后一步一步组装做,好比有了工业革命,现在都由生产流水线代替了。如要造丰田汽车,先制定汽车的构造如由车胎、方向盘、发动机组成。再以此构造标准生产丰田汽车的车胎、方向盘、发动机。然后进行组装。最后得到丰田汽车;

  PROTOTYPE?印刷术的发明:以前只能临贴才能保持和别人的字迹基本相同,直从印刷技术发明,从而保证了复制得和原物一模一样;

  SINGLETON?唯一:以前是商标满天飞,相同的商标难免造成侵权,直从有商标保护法后,就保证了不会再产生第家企业使用相同的商标;


  二、结构型模式

  ADAPTER?集众人之私,成一己之公:武当派张三丰会太极拳,少林派智空大师会金刚般若掌,如果他们两个都成为我的师傅,我就既会太极拳,又会金刚般若掌了;

  DECORATOR?青出于蓝而胜于蓝:武当派张三丰会太极拳,是我师傅,他教会了我太极拳,但我自己还会点蒙古式摔交,张三丰却不会。于是我就成了DECORATOR模式的实现;

  BRIDGE?白马非马:马之颜色有黑白,马之性别有公母。我们说”这是马”太抽象,说”这是黑色的公马”又太死板,只有将颜色与性别和马动态组合,”这是(黑色的或白色的)(公或母)马”才显得灵活而飘逸,如此bridge模式精髓得矣。

  COMPOSITE?大家族:子又生孙,孙又生子,子子孙孙,无穷尽也,将众多纷杂的人口组织成一个按辈分排列的大家族即是此模式的实现;

  FACADE?求同存异:高中毕业需读初中和高中,博士也需读初中和高中,因此国家将初中和高中普及成九年制义务教育;

  FLYWEIGHT?一劳永逸:认识三千汉字,可以应付日常读书与写字,可见头脑中存在这个汉字库的重要;

  PROXY?垂帘听政:犹如清朝康熙年间的四大府臣,很多权利不在皇帝手里,必须通过辅佐大臣去办;

  三、行为模式

  CHAIN OF RESPONSIBLEITY?租房:以前为了找房到处打听,效率低且找不到好的房源。现在有了房屋中介,于是向房屋中介提出租房请求,中介提供一个合适的房源,满意则不再请求,不满意继续看房,直到满意为止;

  COMMAND?借刀杀人:以前是想杀谁就杀,但一段时间后领悟到,长此以往必将结仇太多,于是假手他人,挑拨他人之间的关系从而达到自己的目的;

  INTERPRETER?文言文注释:一段文言文,将它翻译成白话文;

  ITERATOR?赶尽杀绝:一个一个的搜索,绝不放掉一个;

  MEDIATOR?三角债:本来千头万绪的债务关系,忽出来一中介,包揽其一切,于是三角关系变成了独立的三方找第四方中介的关系;

  MEMENTO?有福同享:我有多少,你就有多少;

  OBSERVER?看守者:一旦被看守者有什么异常情况,定会及时做出反应;

  STATE?进出自由:如一扇门,能进能出,如果有很多人随时进进出出必定显得杂乱而安全,如今设一保安限制其进出,如此各人进出才显得规范;

  STRATEGY?久病成良医:如人生病可以有各种症状,但经过长期摸索,就可以总结出感冒、肺病、肝炎等几种;

  TEMPLATE METHOD?理论不一定要实践:教练的学生会游泳就行了,至于教练会不会则无关紧要;

  VISITOR?依法治罪:因张三杀人要被处死,李四偷窃要被罚款。由此势必制定处罚制度,故制定法律写明杀人、放火、偷窃等罪要受什么处罚,经通过后须变动要小。今后有人犯罪不管是谁,按共条例处罚即是,这就是访问者模式诞生的全过程。

2005年02月19日

Bobby Woolf 谈 J2EE 体系结构和设计

专家访谈: Bobby Woolf 谈 J2EE 体系结构和设计 

转自developerWorks WebSphere Web
在这篇一问一答的文章里,我们邀请到了 J2EE 专家 Bobby Woolf 来回答关于 J2EE 应用程序体系结构及设计的问题。
引言
Java? 2 企业版(Java? 2 Enterprise Edition,J2EE)是开发并部署企业应用程序的环境。J2EE 平台由服务、应用程序编程接口(application programming interfaces,API)及协议所组成,该协议提供了开发多层的且基于 Web 的应用程序的功能。关于产品信息的内容,请见 WebSphere Application Server 专区 和 WebSphere Studio 专区。

问: EJB 或 Web 服务的 BEA 控制及注释看上去是非常强大的并且易于使用这些技术。对此,您的看法是什么?在 WebSphere? 中使用这些技术有多容易?

答: 您似乎希望获得 BEA? WebLogic Workshop 和 IBM WebSphere Studio Application Developer(以下简称 WebSphere Studio)的比较。我没有使用过 Workshop,所以不能直接评论它的特点。WebSphere Studio 确实包含许多对于元信息的编辑器,如部署描述符。使用这些编辑器比编辑原始的 XML 要容易得多(虽然 Source 记录页面也允许您那样做)。同样,在 WebSphere Studio V5 中重新设计了这些编辑器,使其与 J2EE 指定的和 WebSphere 指定的配置设置有更加明显的不同。

问: 您知道如果启用了 WebSphere Application Server V5.1 Global Security,那么 Web 服务和 WebSphere Application Server 之间的传输是否可以自动地转换成 https?例如,使用 defaultSSLsettings 和 jmserver 认证。我不必在 Web 和 Application 服务器之间创建新的证书。同时,我已经具有 Web 和浏览器客户端之间的 https 了。目前,这仅是不完整的 Web<>WAS https。全局安全性还没有被启用。

答: 在 WebSphere Application Server 5.1 中启用全局安全性会自动地进行 Web 服务的 HTTPS 转换吗?据我所知不是这样的。您需要自己手动完成。请见 WebSphere Application Server Information Center 中的”调用 HTTPS 上的 web 服务”和”使用 HTTP 基础认证来安全化安全的 Socket 层上的简单对象访问协议服务”。

问: 在我们的项目中有一套 EJB。基本上,我们具备安装在 WebSphere Studio V5.1.2 上的 EAR 文件。当测试环境服务器运行时,对于 EJB 项目及客户端项目的代码变更都不会受到影响。每次我都需要重新启动测试环境服务器才能使得改变生效。那么,存在任何方案或配置装置来解决这个问题吗?

答: 如果在您使用调试器编辑代码的时候(例如编辑 EJB 实现类,但不改变 EJB 的接口)启用了 hot-method 替换,那么将立即改变正在运行的应用程序的代码。请见 WebSphere Studio 的在线帮助中的”Hot-method 替换”。您也可以重启个别项目,这比重新启动整个项目要快得多。然而,许多变更需要重启测试服务器才能实现;请见 WebSphere Studio 的在线帮助中的”何时需要重启测试服务器”。也可参阅”调试服务器上的企业级 bean”和”调试服务器上的 servlet”。

问: 在 Web 应用程序中,我们希望将 uid 和 pwd 传递到 EJB 层中,以便会话 EJB 能够通过 uid 和 pwd 连接到后端系统。我们不想将 uid 和 pwd 作为参数传递,或者存储到一些从 Web 层到 EJB 层传递的对象中。同时,存在不同的流程,它需要使用 uid 和 pwd 来连接后端。您能推荐完成该任务的解决方案吗?

答: 对于 J2EE 应用程序,所有用户都使用同样的登录信息来访问后端系统。J2EE 不能使每个应用程序的用户都使用各自的登录信息。您的 EJB 层应当是无状态的服务,该服务由无状态的会话 bean 来负责管理,所以不能存储用户的数据,如用户名或密码。您可以在用户的 HTTPSession 中存储这些项目,但是这可能是巨大的潜在安全漏洞,并且登录信息可能不是非常有用的。所以,您需要使用 J2EE 服务来访问后端系统,它对于所有用户都使用同样的登录信息。

如果您的后端系统是 JDBC 数据库,那么您应当将登录信息与数据源联系起来。例如,在 WebSphere Studio 中,在服务器配置编辑器中添加登录信息作为安全页面上的 JAAS 认证条目,然后将其指定为数据源容器管理的认证别名(在数据源页面上)。对于其他的后端系统,您应当获取或开发适合于 J2EE 连接器体系结构(J2EE Connector Architecture)的适配器,它也提供了登录信息。

如果您有两种类型的用户,那么他们应当使用两种不同的登录信息,我的文章消除 J2EE 1.3 的服务探测器实现中的缓存中讨论了如何使用两个不同的 EJB 类来完成此项工作(登录)。显而易见,该技术不能使得成百上千的用户都用他们自己的登录信息登录。

问: 我不是 EJB 专家,但是我已经学习了 IBM 提供的该方面的大量的教程和红皮书。我个人认为 WebSphere Studio 生成”管理的代码”(它被优化来管理容器)的能力激发了开发人员更大的创造力,远远超过不使用 EJB 开发的时候。这甚至适用于用不到 EJB 的情形。我的问题是:应当纠正它吗?我已经试着让我的公司从纯生产的角度来学习 WebSphere Studio 或 WebSphere Application Server 的 EJB 开发,但外面的顾问已经发现了所有 EJB 的负面案例。

如果 IBM 有以小型或中型商店为对象而编写的白皮书,那么它将起到帮助作用。该商店展示了比”Banking”应用程序(使用 WebSphere Studio 和类似于 Spring/Hibernate/JDO/”whatever”解决方案的 WebSphere Application Server 来开发的应用程序)更复杂的基于 web 的 EJB 应用程序。我认为加快设计及开发应用程序的速度要与减少实际编写的代码量同时进行,通过使用 WebSphere Studio 能够使代码编写更高效。这假设工具能有效地支持您的向导生成的代码。

花费的时间、创建的代码、应用程序的性能及每方面的耗费的具体细节都能证明通过使用 WebSphere Studio 或 WebSphere Application Server 使得 J2EE 或 EJB 开发比一行一行地推敲业务及管理代码更加高效且可维护。再一次,将与提高整体性能相关的硬件耗费同缩短开发时间及提高对于庞大的代码库的维护能力所需的耗费进行权衡之后,我认为前者可以忽略不计。

答: 您对于向 SMB 市场提供更多的市场及技术材料的意见我十分赞同,但是在我的部门这并不实际。我所能做的是试着回答技术问题。

您在正确的轨道上,也就是您应当最大限度地利用这些工具使得您的工作更简单,最大限度地利用容器使得代码的编写工作更容易。简短的代码更易于编写、测试、调试、维护及通过端口传输。您的代码应以您唯一的业务值为主,而不是寻找每个典型的应用程序的需求,这可以由容器来处理。

我对 EJB 的负面案例没有强烈的兴趣。许多由于 EJB 1.0 的时间框架和忽视诸如本地接口和容器管理的关联(container-managed relationships,CMR)的改进。大多数负面案例包含实体 bean 并忽视会话及消息驱动的 EJB 的危险程度。

即使使用实体 bean,O/R 映射还是非常困难的,那么您使用什么会好一些呢?我建议开发者开始使用 CMP 实体 bean 来将他们的域建模并且处理他们数据库的 O/R 映射。”然而 CMP 是非常慢的!”也许,O/R 映射是通用的,至少对于 CMP 而言,您使用容器来优化该流程。IBM 有许多开发人员致力于将 O/R 映射工作做得尽可能好。我想问每个客户:您真的认为您的团队能够开发更好的代码吗?(尤其是他们是否已经认为 EJB 太难了?)我认为首先尝试 CMP。如果证实它确实太慢了(或不能忍受),那么确定出现问题的特定的 bean 并替换它们。然后您需要再次运行您的加载测试,并且验证您的新代码真的优于它所取代的 CMP 代码。如果开发者严格地检查结果,并且不相信传闻,我认为他们会发现标准的 J2EE 的特点及 WebSphere 支持对于他们实际工作是非常有利的。请见 Persisting Domain Objects。

问: 我们使用 WebSphere Studio 5.0 作为开发环境开发了一个 J2EE 应用程序。但是我们希望将其部署在 WebSphere Application Server 4.0 上。由于 EAR 文件是由 WebSphere Studio 5.0 生成的,所以它兼容 WebSphere Application Server 5.0。它给出了关于 DTD 的错误。考虑到相关的 EJB,我们仅使用了无状态会话 bean。需要哪些改变?在 WebSphere Application Server 4.0 中的哪些地方需要部署应用程序?

答: 如果我正确地理解了您的问题,那您的意图是使用 WebSphere Studio 5.0(目前最新的版本是 5.1.2,所以您可能希望升级到该版本)来开发应用程序,并将其部署在 WebSphere Application Server 4.0 中。看上去您为 WebSphere Application Server 5.0 开发应用程序,这是 WebSphere Studio 5.0 的缺省模式,目前它不能部署而且 WebSphere Application Server 4.0 也是如此,所以您想知道原因及应该做什么。我希望正确地理解了您的问题。

好消息!您一定可以使用 WebSphere Studio 5.0 来开发 WebSphere Application Server 4.0 应用程序。这是 WebSphere Studio 5.0 所特有的,以便开发者能升级到我们最新的工具,即使产品不准备升级到最新的 WebSphere Application Server 版本。您仅需要知道如何做。

您需要做的是为 WebSphere Application Server 4.0 开发应用程序,而不是 WebSphere Application Server 5.0 默认的 WebSphere Studio 5.0。下面是关于在哪里及如何完成这项工作的两个例子:

在 J2EE 透视图中,当您选择 New -> Enterprise Application Project 时,请选择创建 J2EE 1.2 项目。这样做是因为 WebSphere Application Server 4.0 支持 J2EE 1.2,不是 J2EE 1.3 默认的 WebSphere Studio 5.0。
同样在 J2EE 透视图中,当您选择 New -> EJB Project 时,请选择创建 EJB 1.1 项目。这样做是因为 WebSphere Application Server 4.0 和 J2EE 1.2 支持 EJB 1.1,不是 EJB 2.0 默认的 WebSphere Studio 5.0。(当然,您不能获得最新的 EJB 2.0 特征,但是为此您需要升级到 WebSphere Application Server 5.0.2 或之后的版本。)
在 Server 透视图中,当您选择 New -> Server and Server Configuration 时,请选择创建服务器配置类型为 WebSphere v4.0 Server Configuration 的服务器。您可以在 Server Type 列表的 WebSphere version 4.0 文件夹中找到它。这创建了测试服务器,它是 WebSphere Application Server 4.0 独立的服务器,为了能测试您部署在完整的 WebSphere Application Server 4.0 中的 J2EE 1.2 应用程序。
在 Window -> Preferences 中,J2EE 下,对于部署中所用的最高的 J2EE 版本,您可以选择 J2EE 1.2。然后,WebSphere Studio 将认为您正在为 WebSphere Application Server 4.0(或另一个 J2EE 1.2 容器)进行部署。

问: 这是有关事务管理和回滚的设计问题,它们被编码在单独的 DB2? 实例(非分布式事务)上。应用程序使用 DAO 模式来将数据存储到多个表中,一个应用程序使用的每个 DAO 都保存在一张表中。会话外观 EJB 业务方法调用了一个以上的 DAO 来将其保存在一张以上的表中。启用了管理事务的容器。我阅读了一些声明的事务管理的文章,您需要完成其中的一件事:

使用业务方法中的 setRollBackOnly()
使业务方法抛出运行时的异常

目前,已经选择了第二项;构造 DAOException 来扩大 RuntimeException,并且当错误发生时所有的 DAO 类都抛出 DAOException 实例。请您给出建议,哪个选择是最好的?哪个能使我们的代码有更好的复用性?对于第一种选择,我认为您需要为每个业务方法添加额外的代码行 setRollBackOnly(),我认为这很麻烦。

使用 WebSphere Studio V4 IDE 测试环境,事务管理及回滚会很好地进行,但是无论何时将应用程序部署到 WebSphere Application Server V4 上,都不会有回滚发生。存在任何特定的配置或设置来启用 WebSphere Application Server V4 上声明的事务管理吗?

答: 您很好地利用了模式。数据访问对象(Data Access Object,DAO)及会话外观(Session Facade)都是非常值得一用的。由于您使用 EJB,所以进入了普遍存在的困境中:如何实现您的 EJB 代码及如何使用应用程序的异常?何时使用 setRollbackOnly()?怎样同时配合使用它们?

应用程序异常表示发生了业务逻辑错误:取款超出了账单余额、预订已被订出的座位、获取已被冻结的信用卡的费用等等。这同系统异常不同,系统异常表示系统级别的错误,如耗尽内存或者数组越界。根据 EJB 规范(第 18 章)中指出的,应用程序异常必须是 Exception 的子类,并且不能是 RuntimeException 或 RemoteException 的子类,这是非常好的建议。同时,根据该规范,容器必须能够捕获系统异常,标记事务用于回滚,并向EJB 客户端抛出 RemoteException 或 EJBException 异常。容器必须将应用程序异常传递到 EJB 客户端并且不改变该异常以及事务。

我认为这是非常矛盾的,系统异常引发了自动回滚而应用程序异常不能。已经抛出了异常,那么您真的要执行这些事务吗(哪个是自动的行为)?我不这么认为。然而,这是 EJB 的工作方式。

因此,对于您的情况:您的应用程序异常类是 RuntimeException 的子类。这是不好的。当它自动执行回滚的同时,也将您有含义的描述的应用程序异常转换成了通用的异常。规范中指出不要这样做。

您必须自己处理回滚。这意味着您在会话外观(您的 EJB 容器的最高层)中编写代码时需要养成这样的习惯——捕获应用程序异常、调用 setRollbackOnly() 方法并重新抛出同样的异常以便使 EJB 客户端知道发生了什么。这是编写优秀的 EJB 代码的方法:在向 EJB 容器外抛出异常之前,请不要使用 RuntimeException 的子类及 setRollbackOnly() 方法。

问: 先前我的应用程序是基于 WebSphere 4.0 的 J2EE 1.2 标准开发的。目前,我试着使用 WebSphere Studio 5.0(WebSphere 5.0)的 J2EE 1.3 标准转换它。我将应用程序从 j2ee1.2 移植到 j2ee1.3 的级别。

我有两个模块,一个是 EJB 模块(它包含所有的 EJB),另一个是 Web 模块。该 Web 模块被成功地转换成每个 J2EE 1.3 标准。该 EJB 模块在转换过程中出现了一些错误。日志文件如下:


Info: Project structure did not need migration: SCLS-ejb.
Info: J2EE version level did not need migration: SCLS-ejb.
Info: Migration was not required for META-INF/ejb-jar.xml.
Error: java.lang.NullPointerException
Error: Unable to migrate the Map and Schema file structure in project SCLS-ejb

 

由于这个错误,我不能正常地运行应用程序。所有的实体 bean 都不起作用。我不能使用实体 bean 来插入记录。

答: 您已经使用了 WebSphere Studio 5.0 中的 J2EE Migration Wizard 试图将您的 J2EE 1.2 应用程序移植到 J2EE 1.3,但是由于使用了 NullPointerException 而使其失败了。由于我不了解您的代码,所以判断工具出现的故障是很困难的。您或许需要求助于 IBM Support。至于您的 EJB 代码为什么不能正常地执行,可能与您移植工具中出现的故障有关,但也可能因为您需要移植代码。移植向导仅更新了您的部署描述符,而没有更新 EJB 实现中的代码。请见 WebSphere Studio 在线帮助中的”移植 J2EE 应用程序”和”从 J2EE 1.2 移植到 1.3″。

问: 我真需要学习 EJB 吗?

答: 我要讲一个在我出席 WebSphere Technical Exchange 会议时发生的故事。我提出了分布式(XA)事务上的会话及实验:如何将其编码及它们如何工作。该实验由一些简单的样例代码组成,主要是由 JDBC 数据库及 JMS 消息系统更新的无状态会话 bean(EJB),因此需要分布式的事务。该样例还包含被 SSB 简单调用的 servlet。该实验进行得非常好,事务回滚按预期的计划进行,所有都很顺利。实验之后,客户问我:”真是太棒了!我如何能够仅使用 servlet 来完成它?”回答是:您不能。我问他为什么不使用 EJB。他和他的合作者都感到 EJB 很难。即使 EJB 及容器管理的任何内容能使复杂的编程工作变得更简单,人们仍旧认为它们不够简单。

该故事的含义是:如果您有完成复杂任务的工具,并且希望完成那些任务,那么您需要学习这些工具。我不想论证 EJB 是容易的,但是我不认为用它能非常容易地完成复杂的任务:事务、安全、池、远程调用——当然,由于实体 bean 具有容器管理的持久性。其他的哪些技术使您能够使用面向几百个用户的对象来支持上千个用户?其他的哪些技术使您能够声明您的事务模型、安全模型、持久性模型等在 XML 中的模型并且能在运行时运行那些模型?

Java(J2SE)包含庞大的类库,使您开发应用程序时不能任意妄为。J2EE 包含服务及框架,它们完成了许多您应用程序所需的工作。使容器完成尽可能多的工作,这样您的应用程序就不必去做了。以尽可能少的代码编写您的应用程序,并且使容器来完成此项工作。代码量越少,开发进度就越快,维护越容易,且能更快地通过端口传输到 J2EE 和 WebSphere Application Server 的下一个版本中。因此,我的建议是尽可能多地掌握容器给您带来的益处,以及诸如 WebSphere Studio Application Developer 之类的开发工具使您能完成哪些工作,并且最大限度地利用它们。

问: 我应当使用同步或异步的信息传递吗?

答: 该问题一直存在。我们正在使用 RPC(例如,RMI、CORBA、具有远程接口的 EJB 等等);为什么我们应当使用 JMS 或消息传递?通常,当用户等候回答时,由于用户是同步的,所以可以同步地调用。当一个应用程序调用其它的应用程序时,进行异步调用。如果一个应用程序要向其他一些应用程序通告事件,那么也是异步调用。同步调用极易受破坏,所以为了使远程调用更稳固,要进行异步调用。

存在这样的理解——消息传递比 RPC 慢且低效。然而,消息传递确实有比 RPC 更高的地方,仅有一点性能差异,除非您加入了高品质的服务,如持久性的消息传递。如果消息传递很慢,这由于您的网速很慢,在这种情况下 RPC 也会很慢或者根本不能工作。

通常用户接口不能支持异步调用,因为 UI 阻碍了调用,可能需等候一段时间。一种技术是重载该页面,直到获得结果为止。请见由 Kyle Brown 所著的 J2EE 中的异步查询。更好的方法是重新设计 UI,使其不能同步。例如,电子商务站点当打包并传输订单的时候不会出现浏览器阻碍。它告知用户”谢谢订购”并且当订单传输的时候发送包含该包裹号码的电子邮件。这需要 UI 打破工作流,异步地执行,因此您需要诸如 Process Choreographer 之类的工作流引擎。然后,处理以异步的方式完成,并且用户可以随时检查它的进行情况。

这是 Web 服务的缺陷,没有价值。目前,Web 服务(WS-I 基本概要 1.1)是同步的。调用者发出 HTTP 请求,并且阻止了等候 HTTP 响应。由于 Web 服务是用于应用程序到应用程序的信息传递,所以若它们能够支持异步的信息传递将会更加有用。

问: SOA 和 ESB 之间的区别是什么?

答: 面向服务的体系结构(Service Oriented Architecture,SOA)是一种方式或架构,用于具有自服务功能的应用程序,应用程序随后通过用户接口(UI)或经过工作流将其聚合成用户的功能。服务不仅是可复用代码的组件,更是运行程序的一部分,客户端可以不必合并它自己的代码直接调用该程序。事实上,应用程序的界限变得非常模糊了,它包括所有能被调用来执行它的功能的服务。

企业服务总线(Enterprise Service Bus,ESB)是用于调节 SOA 中的调用者及服务提供者的机制。它使得调用者在不知道提供者或提供者使用的地址的情况下调用该服务。ESB 可在多个提供者、提供者的负载平衡及停止使用提供者(当失效时)之间进行选择,并且基于调用者的需求在提供者之间进行选择,这些提供者提供了各种质量级别的服务。ESB 能够调节同步或异步服务,事实上对于同一服务可以提供同步及异步的访问。

因此 SOA 和 ESB 是相对应的。具备 SOA 的应用程序应当使用 ESB 来调用它的服务。SOA 和 ESB 不必用 Web 服务实现。然而,经常需要 ESB 来调用服务,该服务提供自我描述及发现的能力,这由 Web 服务帮助完成。在 SOA 中经常需要由一种技术实现的调用者,它们用于调用由其它技术实现的服务,这也由 Web 服务帮助完成。所以 SOA、ESB 和 Web 服务都集中于创建这样的领域——一个应用程序中的功能在其它应用程序中也是可用的,本质是复用性。

参考资料


消除 J2EE 1.3 的服务探测器实现中的缓存
在 WebSphere Studio 中配置并使用 XA 分布式事务
J2EE 中的异步查询
WS-I 基本概要 1.1
WebSphere Application Server Information Center
WebSphere Application Server Support
WebSphere Studio Application Developer 5.1 信息中心
WebSphere Studio Support
J2EE in Practice (blog)
WebSphere Application Server 专区
WebSphere Studio 专区
企业集成模式
设计模式 Smalltalk 伴侣

  JavaBeans的属性

  JavaBeans的属性与一般Java程序中所指的属性,或者说与所有面向对象的程序设计语言中对象的属性是一个概念,在程序中的具体体现就是类中的变量。在JavaBeans设计中,按照属性的不同作用又细分为四类:Simple, Index, Bound与Constrained属性。

  1. Simple属性

  一个简单属性表示一个伴随有一对get/set方法(C语言的过程或函数在Java程序中称为”方法”)的变量。属性名与和该属性相关的get/set方法名对应。例如:如果有setX和getX方法,则暗指有一个名为”X”的属性。如果有一个方法名为isX,则通常暗指”X”是一个布尔属性(即X的值为true或false)。例如在下面这个程序中:


public class alden1 extends Canvas {
 string ourString= “Hello”; //属性名为ourString,类型为字符串
 public alden1(){     //alden1()是alden1的构造函数,
  与C++中构造函数的意义相同
  setBackground(Color.red);
  setForeground(Color.blue);
 }
 /* “set”属性*/
 public void setString(String newString) {
  ourString=newString;
 }
 /* “get”属性 */
 public String getString() {
  return ourString;
 }
}


   2. Indexed属性

  一个Indexed属性表示一个数组值。使用与该属性对应的set/get方法可取得数组中的数值。该属性也可一次设置或取得整个数组的值。例:


public class alden2 extends Canvas {
 int[] dataSet={1,2,3,4,5,6}; // dataSet是一个indexed属性
 public alden2() {
  setBackground(Color.red);
  setForeground(Color.blue);
 }
 /* 设置整个数组 */
 public void setDataSet(int[] x){
  dataSet=x;
 }
 /* 设置数组中的单个元素值 */
 public void setDataSet(int index, int x){
  dataSet[index]=x;
 }
 /* 取得整个数组值 */
 public int[] getDataSet(){
  return dataSet;
 }
 /* 取得数组中的指定元素值 */
 public int getDataSet(int x){
  return dataSet[x];
 }
}


  3. Bound属性

  一个Bound属性是指当该种属性的值发生变化时,要通知其它的对象。每次属性值改变时,这种属性就点火一个PropertyChange事件(在Java程序中,事件也是一个对象)。事件中封装了属性名、属性的原值、属性变化后的新值。这种事件是传递到其它的Beans,至于接收事件的Beans应做什么动作由其自己定义。当PushButton的background属性与Dialog的background属性bind时,若PushButton的background属性发生变化时,Dialog的background属性也发生同样的变化。 例:


public class alden3 extends Canvas{
 String ourString= “Hello”;
 //ourString是一个bound属性
 private PropertyChangeSupport changes = new PropertyChangeSupport(this);
 /** 注:Java是纯面向对象的语言,
 如果要使用某种方法则必须指明是要使用哪个对象的方法,
 在下面的程序中要进行点火事件的操作,
 这种操作所使用的方法是在PropertyChangeSupport类中的。
 所以上面声明并实例化了一个changes对象,
 在下面将使用changes的firePropertyChange方法来点火ourString的属性改变事件。*/

 public void setString(string newString){
  String oldString = ourString;
  ourString = newString;
  /* ourString的属性值已发生变化,于是接着点火属性改变事件 */
  changes.firePropertyChange(“ourString”,oldString,newString);
 }
 public String getString(){
  return ourString;
 }
 /** 以下代码是为开发工具所使用的。
 我们不能预知alden3将与哪些其它的Beans组合成为一个应用,
 无法预知若alden3的ourString属性发生变化时有哪些其它的组件与此变化有关,
 因而alden3这个Beans要预留出一些接口给开发工具,
 开发工具使用这些接口,
 把其它的JavaBeans对象与alden3挂接。*/

 public void addPropertyChangeListener(PropertyChangeLisener l){
  changes.addPropertyChangeListener(l);
 }
 public void removePropertyChangeListener(PropertyChangeListener l){
  changes.removePropertyChangeListener(l);
 }


  通过上面的代码,开发工具调用changes的addPropertyChangeListener方法,把其它JavaBeans注册入ourString属性的监听者队列l中,l是一个Vector数组,可存储任何Java对象。

  开发工具也可使用changes的removePropertyChangeListener方法,从l中注销指定的对象,使alden3的ourString属性的改变不再与这个对象有关。

  当然,当程序员手写代码编制程序时,也可直接调用这两个方法,把其它Java对象与alden3挂接。
  4. Constrained属性

  一个JavaBeans的constrained属性,是指当这个属性的值要发生变化时,与这个属性已建立了某种连接的其它Java对象可否决属性值的改变。constrained属性的监听者通过抛出PropertyVetoException来阻止该属性值的改变。例:下面程序中的constrained属性是PriceInCents。


public class JellyBeans extends Canvas{
 private PropertyChangeSupport changes=new PropertyChangeSupport(this);
 private VetoableChangeSupport Vetos=new VetoableChangeSupport(this);
 /*与前述changes相同,
 可使用VetoableChangeSupport对象的实例Vetos中的方法,
 在特定条件下来阻止PriceInCents值的改变。*/

 ……
 public void setPriceInCents(int newPriceInCents) throws PropertyVetoException {
  /*方法名中throws PropertyVetoException的作用是当有
  其它Java对象否决PriceInCents的改变时,
  要抛出例外。*/
  /* 先保存原来的属性值*/

  int oldPriceInCents=ourPriceInCents;
  /**点火属性改变否决事件*/
  vetos.fireVetoableChange(“priceInCents”,new Integer(OldPriceInCents),new Integer(newPriceInCents));

  /**若有其它对象否决priceInCents的改变,
  则程序抛出例外,不再继续执行下面的两条语句,
  方法结束。若无其它对象否决priceInCents的改变,
  则在下面的代码中把ourPriceIncents赋予新值,
  并点火属性改变事件*/

  ourPriceInCents=newPriceInCents;
  changes.firePropertyChange(“priceInCents”, new Integer(oldPriceInCents), new Integer(newPriceInCents));
 }

 /**与前述changes相同,
 也要为PriceInCents属性预留接口,
 使其它对象可注册入PriceInCents否决改变监听者队列中,
 或把该对象从中注销

 public void addVetoableChangeListener(VetoableChangeListener l)
 {
  vetos.addVetoableChangeListener(l);
 }
 public void removeVetoableChangeListener(VetoableChangeListener l){
  vetos.removeVetoableChangeListener(l);
 }
 ……
}


  从上面的例子中可看到,一个constrained属性有两种监听者:属性变化监听者和否决属性改变的监听者。否决属性改变的监听者在自己的对象代码中有相应的控制语句,在监听到有constrained属性要发生变化时,在控制语句中判断是否应否决这个属性值的改变。

  总之,某个Beans的constrained属性值可否改变取决于其它的Beans或者是Java对象是否允许这种改变。允许与否的条件由其它的Beans或Java对象在自己的类中进行定义。

  JavaBeans的事件

  事件处理是JavaBeans体系结构的核心之一。通过事件处理机制,可让一些组件作为事件源,发出可被描述环境或其它组件接收的事件。这样,不同的组件就可在构造工具内组合在一起,组件之间通过事件的传递进行通信,构成一个应用。从概念上讲,事件是一种在”源对象”和”监听者对象”之间,某种状态发生变化的传递机制。事件有许多不同的用途,例如在Windows系统中常要处理的鼠标事件、窗口边界改变事件、键盘事件等。在Java和JavaBeans中则是定义了一个一般的、可扩充的事件机制,这种机制能够:

  ·对事件类型和传递的模型的定义和扩充提供一个公共框架,并适合于广泛的应用。

  ·与Java语言和环境有较高的集成度。

  ·事件能被描述环境捕获和点火。

  ·能使其它构造工具采取某种技术在设计时直接控制事件,以及事件源和事件监听者之间的联系。

  ·事件机制本身不依赖于复杂的开发工具。特别地,还应当:

  ·能够发现指定的对象类可以生成的事件。

  ·能够发现指定的对象类可以观察(监听)到的事件。

  ·提供一个常规的注册机制,允许动态操纵事件源与事件监听者之间的关系。

  ·不需要其它的虚拟机和语言即可实现。

  ·事件源与监听者之间可进行高效的事件传递。

  ·能完成JavaBeans事件模型与相关的其它组件体系结构事件模型的中立映射。

  JavaBeans事件模型的主要构成有: 事件从事件源到监听者的传递是通过对目标监听者对象的Java方法调用进行的。对每个明确的事件的发生,都相应地定义一个明确的Java方法。这些方法都集中定义在事件监听者(EventListener)接口中,这个接口要继承java.util.EventListener。实现了事件监听者接口中一些或全部方法的类就是事件监听者。 伴随着事件的发生,相应的状态通常都封装在事件状态对象中,该对象必须继承自java.util.EventObject。事件状态对象作为单参传递给应响应该事件的监听者方法中。 发出某种特定事件的事件源的标识是:遵从规定的设计格式为事件监听者定义注册方法,并接受对指定事件监听者接口实例的引用。 有时,事件监听者不能直接实现事件监听者接口,或者还有其它的额外动作时,就要在一个源与其它一个或多个监听者之间插入一个事件适配器类的实例,来建立它们之间的联系。

  事件状态对象(Event State Object)

  与事件发生有关的状态信息一般都封装在一个事件状态对象中,这种对象是java.util.EventObject的子类。按设计习惯,这种事件状态对象类的名应以Event结尾。例如:


public class MouseMovedExampleEvent extends java.util.EventObject
{
 protected int x, y;
 /* 创建一个鼠标移动事件MouseMovedExampleEvent */
 MouseMovedExampleEvent(java.awt.Component source, Point location) {
  super(source);
  x = location.x;
  y = location.y;
 }
 /* 获取鼠标位置*/
 public Point getLocation() {
  return new Point(x, y);
 }
}


  事件监听者接口(EventListener Interface)与事件监听者

  由于Java事件模型是基于方法调用,因而需要一个定义并组织事件操纵方法的方式。JavaBeans中,事件操纵方法都被定义在继承了java.util.EventListener类的EventListener接口中,按规定,EventListener接口的命名要以Listener结尾。任何一个类如果想操纵在EventListener接口中定义的方法都必须以实现这个接口方式进行。这个类也就是事件监听者。例如:

/*先定义了一个鼠标移动事件对象*/
  public class MouseMovedExampleEvent
extends java.util.EventObject {
// 在此类中包含了与鼠标移动事件有关的状态信息
     …
  }
  /*定义了鼠标移动事件的监听者接口*/
  interface MouseMovedExampleListener
extends java.util.EventListener {
/*在这个接口中定义了鼠标移动事件监听者所应支持的方法*/
void mouseMoved(MouseMovedExampleEvent mme);
}


  在接口中只定义方法名,方法的参数和返回值类型。 如:上面接口中的mouseMoved方法的具体实现是在下面的ArbitraryObject类中定义的。

class ArbitraryObject implements MouseMovedExampleListener {
  public void mouseMoved(MouseMovedExampleEvent mme)
  { … }
}


 ArbitraryObject就是MouseMovedExampleEvent事件的监听者。

  事件监听者的注册与注销

  为了各种可能的事件监听者把自己注册入合适的事件源中,建立源与事件监听者间的事件流,事件源必须为事件监听者提供注册和注销的方法。在前面的bound属性介绍中已看到了这种使用过程,在实际中,事件监听者的注册和注销要使用标准的设计格式:

public void add< ListenerType>(< ListenerType> listener);
public void remove< ListenerType>(< ListenerType> listener);


  例如:

  首先定义了一个事件监听者接口:

public interface
ModelChangedListener extends java.util.EventListener {
 void modelChanged(EventObject e);
}


  接着定义事件源类:

public abstract class Model {
private Vector listeners = new Vector(); // 定义了一个储存事件监听者的数组

/*上面设计格式中的< ListenerType>在此处即是下面的ModelChangedListener*/

public synchronized void addModelChangedListener(ModelChangedListener mcl)
  { listeners.addElement(mcl); }//把监听者注册入listeners数组中
public synchronized void removeModelChangedListener(ModelChangedListener mcl)
    { listeners.removeElement(mcl); //把监听者从listeners中注销
    }
  /*以上两个方法的前面均冠以synchronized,
是因为运行在多线程环境时,
可能同时有几个对象同时要进行注册和注销操作,
使用synchronized来确保它们之间的同步。
开发工具或程序员使用这两个方法建立源与监听者之间的事件流*/

protected void notifyModelChanged() {
/**事件源使用本方法通知监听者发生了modelChanged事件*/
    Vector l;
    EventObject e = new EventObject(this);
/* 首先要把监听者拷贝到l数组中,
冻结EventListeners的状态以传递事件。
这样来确保在事件传递到所有监听者之前,
已接收了事件的目标监听者的对应方法暂不生效。*/
    synchronized(this) {
      l = (Vector)listeners.clone();
    }
    for (int i = 0; i < l.size(); i++) {
     /* 依次通知注册在监听者队列中的每个监听者发生了modelChanged事件,
     并把事件状态对象e作为参数传递给监听者队列中的每个监听者*/
((ModelChangedListener)l.elementAt(i)).modelChanged(e);
    }
    }
   }


  在程序中可见事件源Model类显式地调用了接口中的modelChanged方法,实际是把事件状态对象e作为参数,传递给了监听者类中的modelChanged方法。

  适配类

  适配类是Java事件模型中极其重要的一部分。在一些应用场合,事件从源到监听者之间的传递要通过适配类来”转发”。例如:当事件源发出一个事件,而有几个事件监听者对象都可接收该事件,但只有指定对象做出反应时,就要在事件源与事件监听者之间插入一个事件适配器类,由适配器类来指定事件应该是由哪些监听者来响应。

  适配类成为了事件监听者,事件源实际是把适配类作为监听者注册入监听者队列中,而真正的事件响应者并未在监听者队列中,事件响应者应做的动作由适配类决定。目前绝大多数的开发工具在生成代码时,事件处理都是通过适配类来进行的。

  JavaBeans用户化

  JavaBeans开发者可以给一个Beans添加用户化器(Customizer)、属性编辑器(PropertyEditor)和BeansInfo接口来描述一个Beans的内容,Beans的使用者可在构造环境中通过与Beans附带在一起的这些信息来用户化Beans的外观和应做的动作。一个Beans不必都有BeansCustomizer、PrpertyEditor和BeansInfo,根据实际情况,这些是可选的,当有些Beans较复杂时,就要提供这些信息,以Wizard的方式使Beans的使用者能够用户化一个Beans。有些简单的Beans可能这些信息都没有,则构造工具可使用自带的透视装置,透视出Beans的内容,并把信息显示到标准的属性表或事件表中供使用者用户化Beans,前几节提到的Beans的属性、方法和事件名要以一定的格式命名,主要的作用就是供开发工具对Beans进行透视。当然也是给程序员在手写程序中使用Beans提供方便,使他能观其名、知其意。

  用户化器接口(Customizer Interface)

  当一个Beans有了自己的用户化器时,在构造工具内就可展现出自己的属性表。在定义用户化器时必须要实现java.Beanss.Customizer接口。例如,下面是一个”按钮”Beans的用户化一器:


public class OurButtonCustomizer
extends Panel implements Customizer {
 … …
 /*当实现象OurButtonCustomizer这样的常规属性表时,
 一定要在其中实现addProperChangeListener
 和removePropertyChangeListener,这样,
 构造工具可用这些功能代码为属性事件添加监听者。*/
 … …
 private PropertyChangeSupport changes=new PropertyChangeSupport(this);
 public void addPropertyChangeListener(PropertyChangeListener l) {
  changes.addPropertyChangeListener(l);
  public void removePropertyChangeListener(PropertyChangeListener l) {
   changes.removePropertyChangeListener(l);
  }
… …


  属性编辑器接口(PropertyEditor Interface)

  一个JavaBeans可提供PropertyEditor类,为指定的属性创建一个编辑器。这个类必须继承自java.Beanss.PropertyEditorSupport类。构造工具与手写代码的程序员不直接使用这个类,而是在下一小节的BeansInfo中实例化并调用这个类。例:


public class MoleculeNameEditor extends java.Beanss.PropertyEditorSupport {
 public String[] getTags() {
  String resule[]={
   ”HyaluronicAcid”,”Benzene”,”buckmisterfullerine”, “cyclohexane”,”ethane”,”water”};
  return resule;}
}


  上例中是为Tags属性创建了属性编辑器,在构造工具内,可从下拉表格中选择MoleculeName的属性应是”HyaluronicAid”或是”water”。

  BeansInfo接口

  每个Beans类也可能有与之相关的BeansInfo类,在其中描述了这个Beans在构造工具内出现时的外观。BeansInfo中可定义属性、方法、事件,显示它们的名称,提供简单的帮助说明。 例如:


public class MoleculeBeansInfo extends SimpleBeansInfo {
 public PropertyDescriptor[] getPropertyDescriptors() {
  try {
   PropertyDescriptor pd=new PropertyDescriptor(“moleculeName”,Molecule.class);
   /*通过pd引用了上一节的MoleculeNameEditor类,取得并返回moleculeName属性*/
   pd.setPropertyEditorClass(MoleculeNameEditor.class);
   PropertyDescriptor result[]={pd};
   return result;
  } catch(Exception ex) {
   System.err.println(“MoleculeBeansInfo: unexpected exeption: “+ex);
   return null;
  }
 }
}


  JavaBeans持久化

  当一个JavaBeans在构造工具内被用户化,并与其它Beans建立连接之后,它的所有状态都应当可被保存,下一次被load进构造工具内或在运行时,就应当是上一次修改完的信息。为了能做到这一点,要把Beans的某些字段的信息保存下来,在定义Beans时要使它实现java.io.Serializable接口。例如:


public class Button
implements java.io.Serializable {}


  实现了序列化接口的Beans中字段的信息将被自动保存。若不想保存某些字段的信息则可在这些字段前冠以transient或static关键字,transient和static变量的信息是不可被保存的。通常,一个Beans所有公开出来的属性都应当是被保存的,也可有选择地保存内部状态。 Beans开发者在修改软件时,可以添加字段,移走对其它类的引用,改变一个字段的private/protected/public状态,这些都不影响类的存储结构关系。然而,当从类中删除一个字段,改变一个变量在类体系中的位置,把某个字段改成transient/static,或原来是transient/static,现改为别的特性时,都将引起存储关系的变化。

  JavaBeans的存储格式

  JavaBeans组件被设计出来后,一般是以扩展名为jar的Zip格式文件存储,在jar中包含与JavaBeans有关的信息,并以MANIFEST文件指定其中的哪些类是JavaBeans。以jar文件存储的JavaBeans在网络中传送时极大地减少了数据的传输数量,并把JavaBeans运行时所需要的一些资源捆绑在一起,本章主要论述了JavaBeans的一些内部特性及其常规设计方法,参考的是JavaBeans规范1.0A版本。随着世界各大ISV对JavaBeans越来越多的支持,规范在一些细节上还在不断演化,但基本框架不会再有大的变动。

public class MouseMovedExampleEvent extends java.util.EventObject
{
 protected int x, y;
 /* 创建一个鼠标移动事件MouseMovedExampleEvent */
 MouseMovedExampleEvent(java.awt.Component source, Point location) {
  super(source);
  x = location.x;
  y = location.y;
 }
 /* 获取鼠标位置*/
 public Point getLocation() {
  return new Point(x, y);
 }
}


  事件监听者接口(EventListener Interface)与事件监听者

  由于Java事件模型是基于方法调用,因而需要一个定义并组织事件操纵方法的方式。JavaBeans中,事件操纵方法都被定义在继承了java.util.EventListener类的EventListener接口中,按规定,EventListener接口的命名要以Listener结尾。任何一个类如果想操纵在EventListener接口中定义的方法都必须以实现这个接口方式进行。这个类也就是事件监听者。例如:

/*先定义了一个鼠标移动事件对象*/
  public class MouseMovedExampleEvent
extends java.util.EventObject {
// 在此类中包含了与鼠标移动事件有关的状态信息
     …
  }
  /*定义了鼠标移动事件的监听者接口*/
  interface MouseMovedExampleListener
extends java.util.EventListener {
/*在这个接口中定义了鼠标移动事件监听者所应支持的方法*/
void mouseMoved(MouseMovedExampleEvent mme);
}


  在接口中只定义方法名,方法的参数和返回值类型。 如:上面接口中的mouseMoved方法的具体实现是在下面的ArbitraryObject类中定义的。

class ArbitraryObject implements MouseMovedExampleListener {
  public void mouseMoved(MouseMovedExampleEvent mme)
  { … }
}


 ArbitraryObject就是MouseMovedExampleEvent事件的监听者。

  事件监听者的注册与注销

  为了各种可能的事件监听者把自己注册入合适的事件源中,建立源与事件监听者间的事件流,事件源必须为事件监听者提供注册和注销的方法。在前面的bound属性介绍中已看到了这种使用过程,在实际中,事件监听者的注册和注销要使用标准的设计格式:

public void add< ListenerType>(< ListenerType> listener);
public void remove< ListenerType>(< ListenerType> listener);


  例如:

  首先定义了一个事件监听者接口:

public interface
ModelChangedListener extends java.util.EventListener {
 void modelChanged(EventObject e);
}


  接着定义事件源类:

public abstract class Model {
private Vector listeners = new Vector(); // 定义了一个储存事件监听者的数组

/*上面设计格式中的< ListenerType>在此处即是下面的ModelChangedListener*/

public synchronized void addModelChangedListener(ModelChangedListener mcl)
  { listeners.addElement(mcl); }//把监听者注册入listeners数组中
public synchronized void removeModelChangedListener(ModelChangedListener mcl)
    { listeners.removeElement(mcl); //把监听者从listeners中注销
    }
  /*以上两个方法的前面均冠以synchronized,
是因为运行在多线程环境时,
可能同时有几个对象同时要进行注册和注销操作,
使用synchronized来确保它们之间的同步。
开发工具或程序员使用这两个方法建立源与监听者之间的事件流*/

protected void notifyModelChanged() {
/**事件源使用本方法通知监听者发生了modelChanged事件*/
    Vector l;
    EventObject e = new EventObject(this);
/* 首先要把监听者拷贝到l数组中,
冻结EventListeners的状态以传递事件。
这样来确保在事件传递到所有监听者之前,
已接收了事件的目标监听者的对应方法暂不生效。*/
    synchronized(this) {
      l = (Vector)listeners.clone();
    }
    for (int i = 0; i < l.size(); i++) {
     /* 依次通知注册在监听者队列中的每个监听者发生了modelChanged事件,
     并把事件状态对象e作为参数传递给监听者队列中的每个监听者*/
((ModelChangedListener)l.elementAt(i)).modelChanged(e);
    }
    }
   }


  在程序中可见事件源Model类显式地调用了接口中的modelChanged方法,实际是把事件状态对象e作为参数,传递给了监听者类中的modelChanged方法。

  适配类

  适配类是Java事件模型中极其重要的一部分。在一些应用场合,事件从源到监听者之间的传递要通过适配类来”转发”。例如:当事件源发出一个事件,而有几个事件监听者对象都可接收该事件,但只有指定对象做出反应时,就要在事件源与事件监听者之间插入一个事件适配器类,由适配器类来指定事件应该是由哪些监听者来响应。

  适配类成为了事件监听者,事件源实际是把适配类作为监听者注册入监听者队列中,而真正的事件响应者并未在监听者队列中,事件响应者应做的动作由适配类决定。目前绝大多数的开发工具在生成代码时,事件处理都是通过适配类来进行的。

  JavaBeans用户化

  JavaBeans开发者可以给一个Beans添加用户化器(Customizer)、属性编辑器(PropertyEditor)和BeansInfo接口来描述一个Beans的内容,Beans的使用者可在构造环境中通过与Beans附带在一起的这些信息来用户化Beans的外观和应做的动作。一个Beans不必都有BeansCustomizer、PrpertyEditor和BeansInfo,根据实际情况,这些是可选的,当有些Beans较复杂时,就要提供这些信息,以Wizard的方式使Beans的使用者能够用户化一个Beans。有些简单的Beans可能这些信息都没有,则构造工具可使用自带的透视装置,透视出Beans的内容,并把信息显示到标准的属性表或事件表中供使用者用户化Beans,前几节提到的Beans的属性、方法和事件名要以一定的格式命名,主要的作用就是供开发工具对Beans进行透视。当然也是给程序员在手写程序中使用Beans提供方便,使他能观其名、知其意。

  用户化器接口(Customizer Interface)

  当一个Beans有了自己的用户化器时,在构造工具内就可展现出自己的属性表。在定义用户化器时必须要实现java.Beanss.Customizer接口。例如,下面是一个”按钮”Beans的用户化一器:


public class OurButtonCustomizer
extends Panel implements Customizer {
 … …
 /*当实现象OurButtonCustomizer这样的常规属性表时,
 一定要在其中实现addProperChangeListener
 和removePropertyChangeListener,这样,
 构造工具可用这些功能代码为属性事件添加监听者。*/
 … …
 private PropertyChangeSupport changes=new PropertyChangeSupport(this);
 public void addPropertyChangeListener(PropertyChangeListener l) {
  changes.addPropertyChangeListener(l);
  public void removePropertyChangeListener(PropertyChangeListener l) {
   changes.removePropertyChangeListener(l);
  }
… …


  属性编辑器接口(PropertyEditor Interface)

  一个JavaBeans可提供PropertyEditor类,为指定的属性创建一个编辑器。这个类必须继承自java.Beanss.PropertyEditorSupport类。构造工具与手写代码的程序员不直接使用这个类,而是在下一小节的BeansInfo中实例化并调用这个类。例:


public class MoleculeNameEditor extends java.Beanss.PropertyEditorSupport {
 public String[] getTags() {
  String resule[]={
   ”HyaluronicAcid”,”Benzene”,”buckmisterfullerine”, “cyclohexane”,”ethane”,”water”};
  return resule;}
}


  上例中是为Tags属性创建了属性编辑器,在构造工具内,可从下拉表格中选择MoleculeName的属性应是”HyaluronicAid”或是”water”。

  BeansInfo接口

  每个Beans类也可能有与之相关的BeansInfo类,在其中描述了这个Beans在构造工具内出现时的外观。BeansInfo中可定义属性、方法、事件,显示它们的名称,提供简单的帮助说明。 例如:


public class MoleculeBeansInfo extends SimpleBeansInfo {
 public PropertyDescriptor[] getPropertyDescriptors() {
  try {
   PropertyDescriptor pd=new PropertyDescriptor(“moleculeName”,Molecule.class);
   /*通过pd引用了上一节的MoleculeNameEditor类,取得并返回moleculeName属性*/
   pd.setPropertyEditorClass(MoleculeNameEditor.class);
   PropertyDescriptor result[]={pd};
   return result;
  } catch(Exception ex) {
   System.err.println(“MoleculeBeansInfo: unexpected exeption: “+ex);
   return null;
  }
 }
}


  JavaBeans持久化

  当一个JavaBeans在构造工具内被用户化,并与其它Beans建立连接之后,它的所有状态都应当可被保存,下一次被load进构造工具内或在运行时,就应当是上一次修改完的信息。为了能做到这一点,要把Beans的某些字段的信息保存下来,在定义Beans时要使它实现java.io.Serializable接口。例如:


public class Button
implements java.io.Serializable {}


  实现了序列化接口的Beans中字段的信息将被自动保存。若不想保存某些字段的信息则可在这些字段前冠以transient或static关键字,transient和static变量的信息是不可被保存的。通常,一个Beans所有公开出来的属性都应当是被保存的,也可有选择地保存内部状态。 Beans开发者在修改软件时,可以添加字段,移走对其它类的引用,改变一个字段的private/protected/public状态,这些都不影响类的存储结构关系。然而,当从类中删除一个字段,改变一个变量在类体系中的位置,把某个字段改成transient/static,或原来是transient/static,现改为别的特性时,都将引起存储关系的变化。

  JavaBeans的存储格式

  JavaBeans组件被设计出来后,一般是以扩展名为jar的Zip格式文件存储,在jar中包含与JavaBeans有关的信息,并以MANIFEST文件指定其中的哪些类是JavaBeans。以jar文件存储的JavaBeans在网络中传送时极大地减少了数据的传输数量,并把JavaBeans运行时所需要的一些资源捆绑在一起,本章主要论述了JavaBeans的一些内部特性及其常规设计方法,参考的是JavaBeans规范1.0A版本。随着世界各大ISV对JavaBeans越来越多的支持,规范在一些细节上还在不断演化,但基本框架不会再有大的变动。