【转】回车键触发表单提交的问题


回车键触发表单提交的问题

我们有时候希望回车键敲在文本框(input element)里来提交表单(form),但有时候又不希望如此。比如搜索行为,希望输入完关键词之后直接按回车键立即提交表单,而有些复杂表单,可能要避免回车键误操作在未完成表单填写的时候就触发了表单提交。
要控制这些行为,不需要借助JS,浏览器已经帮我们做了这些处理,这里总结几条规则:
1、如果表单里有一个type=”submit”的按钮,回车键生效。
2、如果表单里只有一个type=”text”的input,不管按钮是什么type,回车键生效。
3、如果按钮不是用input,而是用button,并且没有加type,IE下默认为type=button,FF默认为type=submit。
4、其他表单元素如textarea、select不影响,radio checkbox不影响触发规则,但本身在FF下会响应回车键,在IE下不响应。
5、type=”image”的input,效果等同于type=”submit”,不知道为什么会设计这样一种type,不推荐使用,应该用CSS添加背景图合适些。
实际应用的时候,要让表单响应回车键很容易,保证表单里有个type=”submit”的按钮就行。而当只有一个文本框又不希望响应回车键怎么办呢?我的方法有点别扭,就是再写一个无意义的文本框,隐藏起来 但是如果设置了submit tabindex=0,则会提交  。根据第3条规则,我们在用button的时候,尽量显式声明type以使浏览器表现一致。

Continue reading 【转】回车键触发表单提交的问题

C/S与B/S架构

为了区别于传统的C/S模式,才特意将其称为B/S模式。认识到这些结构的特征,对于系统的选型而言是很关键的。 
1、系统的性能 
在系统的性能方面,B/S占有优势的是其异地浏览和信息采集的灵活性。任何时间、任何地点、任何系统,只要可以使用浏览器上网,就可以使用B/S系统的终端。 
不过,采用B/S结构,客户端只能完成浏览、查询、数据输入等简单功能,绝大部分工作由服务器承担,这使得服务器的负担很重。采用C/S结构时,客户端和服务器端都能够处理任务,这虽然对客户机的要求较高,但因此可以减轻服务器的压力。而且,由于客户端使用浏览器,使得网上发布的信息必须是以HTML格式为主,其它格式文件多半是以附件的形式存放。而HTML格式文件(也就是Web页面)不便于编辑修改,给文件管理带来了许多不便。 
2、系统的开发 
C/S 结构是建立在中间件产品基础之上的,要求应用开发者自己去处理事务管理、消息队列、数据的复制和同步、通信安全等系统级的问题。这对应用开发者提出了较高的要求,而且迫使应用开发者投入很多精力来解决应用程序以外的问题。这使得应用程序的维护、移植和互操作变得复杂。如果客户端是在不同的操作系统上,C/S结构的软件需要开发不同版本的客户端软件。但是,与B/S结构相比,C/S技术发展历史更为“悠久”。从技术成熟度及软件设计、开发人员的掌握水平来看,C/S技术应是更成熟、更可靠的。 
3、系统的升级维护 
C/S系统的各部分模块中有一部分改变,就要关联到其它模块的变动,使系统升级成本比较大。B/S与C/S处理模式相比,则大大简化了客户端,只要客户端机器能上网就可以。对于B/S而言,开发、维护等几乎所有工作也都集中在服务器端,当企业对网络应用进行升级时,只需更新服务器端的软件就可以,这减轻了异地用户系统维护与升级的成本。如果客户端的软件系统升级比较频繁,那么B/S架构的产品优势明显——所有的升级操作只需要针对服务器进行,这对那些点多面广的应用是很有价值的,例如一些招聘网站就需要采用B/S模式,客户端分散,且应用简单,只需要进行简单的浏览和少量信息的录入。 
4、C/S 模式的优点和缺点 
★ C/S 模式的优点 
● 由于客户端实现与服务器的直接相连,没有中间环节,因此响应速度快。 
操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求。 
● C/S结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程。
★ C/S 模式的缺点 
● 需要专门的客户端安装程序,分布功能弱,针对点多面广且不具备网络条件的用户群体,不能够实现快速部署安装和配置。 
兼容性差,对于不同的开发工具,具有较大的局限性。若采用不同工具,需要重新改写程序。 
开发成本较高,需要具有一定专业水准的技术人员才能完成。 
5、B/S模式的优点和缺点 
★ B/S 模式的优点 
具有分布性特点,可以随时随地进行查询、浏览等业务处理。 
● 业务扩展简单方便,通过增加网页即可增加服务器功能。 
● 维护简单方便,只需要改变网页,即可实现所有用户的同步更新。 
● 开发简单,共享性强。 
★ B/S 模式的缺点 
● 个性化特点明显降低,无法实现具有个性化的功能要求。 
● 操作是以鼠标为最基本的操作方式,无法满足快速操作的要求。 
● 页面动态刷新,响应速度明显降低。 
● 无法实现本地分页缓存显示,给数据库访问造成较大的压力。 
功能弱化,难以实现传统模式下的特殊功能要求。



c/s,b/s混用
异地查询,浏览等,功能简单,数据量小,安全要求低的可通过b/s实现。
对于数据量大,功能复杂,安全要求高的则使用c/s实现。

Continue reading C/S与B/S架构

JAVA内存泄漏——内存泄漏原因和内存泄漏检测工具

摘要 
虽然Java虚拟机(JVM)及其垃圾收集器(garbage collector,GC)负责管理大多数的内存任务,Java软件程序中还是有可能出现内存泄漏。实际上,这在大型项目中 是一个常见的问题。避免内存泄漏的第一步是要弄清楚它是如何发生的。本文介绍了编写Java代码的一些常见的内存泄漏陷阱,以及编写不泄漏代码的一些最佳 实践。一旦发生了内存泄漏,要指出造成泄漏的代码是非常困难的。因此本文还介绍了一种新工具,用来诊断泄漏并指出根本原因。该工具的开销非常小,因此可以 使用它来寻找处于生产中的系统的内存泄漏。

垃圾收集器的作用

虽然垃圾收集器处理了大多数内存管理问题,从而使编程人员的生活变得更轻松了,但是编程人员还是可能犯错而导致出现内存问题。简单地说,GC循 环地跟踪所有来自“根”对象(堆栈对象、静态对象、JNI句柄指向的对象,诸如此类)的引用,并将所有它所能到达的对象标记为活动的。程序只可以操纵这些 对象;其他的对象都被删除了。因为GC使程序不可能到达已被删除的对象,这么做就是安全的。

虽然内存管理可以说是自动化的,但是这并不能使编程人员免受思考内存管理问题之苦。例如,分配(以及释放)内存总会有开销,虽然这种开销对编程人员来说是不可见的。创建了太多对象的程序将会比完成同样的功能而创建的对象却比较少的程序更慢一些(在其他条件相同的情况下)。

而且,与本文更为密切相关的是,如果忘记“释放”先前分配的内存,就可能造成内存泄漏。 如果程序保留对永远不再使用的对象的引用,这些对象将会占用并耗尽内存,这是因为自动化的垃圾收集器无法证明这些对象将不再使用。正如我们先前所说的,如 果存在一个对对象的引用,对象就被定义为活动的,因此不能删除。为了确保能回收对象占用的内存,编程人员必须确保该对象不能到达。这通常是通过将对象字段 设置为null或者从集合(collection)中移除对象而完成的。但是,注意,当局部变量不再使用时,没有必要将其显式地设置为null。对这些变 量的引用将随着方法的退出而自动清除。

概括地说,这就是内存托管语言中的内存泄漏产生的主要原因:保留下来却永远不再使用的对象引用。

典型泄漏

既然我们知道了在Java中确实有可能发生内存泄漏,就让我们来看一些典型的内存泄漏及其原因。

全局集合

在大的应用程序中有某种全局的数据储存库是很常见的,例如一个JNDI树或一个会话表。在这些情况下,必须注意管理储存库的大小。必须有某种机制从储存库中移除不再需要的数据。

这可能有多种方法,但是最常见的一种是周期性运行的某种清除任务。该任务将验证储存库中的数据,并移除任何不再需要的数据。

另一种管理储存库的方法是使用反向链接(referrer)计数。然后集合负责统计集合中每个入口的反向链接的数目。这要求反向链接告诉集合何时会退出入口。当反向链接数目为零时,该元素就可以从集合中移除了。

缓存

缓存是一种数据结构,用于快速查找已经执行的操作的结果。因此,如果一个操作执行起来很慢,对于常用的输入数据,就可以将操作的结果缓存,并在下次调用该操作时使用缓存的数据。

缓存通常都是以动态方式实现的,其中新的结果是在执行时添加到缓存中的。典型的算法是:

检查结果是否在缓存中,如果在,就返回结果。 
如果结果不在缓存中,就进行计算。 
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用。 
该算法的问题(或者说是潜在的内存泄漏)出在最后一步。如果调用该操作时有相当多的不同输入,就将有相当多的结果存储在缓存中。很明显这不是正确的方法。

为了预防这种具有潜在破坏性的设计,程序必须确保对于缓存所使用的内存容量有一个上限。因此,更好的算法是:

检查结果是否在缓存中,如果在,就返回结果。 
如果结果不在缓存中,就进行计算。 
如果缓存所占的空间过大,就移除缓存最久的结果。 
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用。 
通过始终移除缓存最久的结果,我们实际上进行了这样的假设:在将来,比起缓存最久的数据,最近输入的数据更有可能用到。这通常是一个不错的假设。

新算法将确保缓存的容量处于预定义的内存范围之内。确切的范围可能很难计算,因为缓存中的对象在不断变化,而且它们的引用包罗万象。为缓存设置正确的大小是一项非常复杂的任务,需要将所使用的内存容量与检索数据的速度加以平衡。

解决这个问题的另一种方法是使用java.lang.ref.SoftReference类跟踪缓存中的对象。这种方法保证这些引用能够被移除,如果虚拟机的内存用尽而需要更多堆的话。

ClassLoader

Java ClassLoader结构的使用为内存泄漏提供了许多可乘之机。正是该结构本身的复杂性使ClassLoader在内存泄漏方面存在如此多的问题。 ClassLoader的特别之处在于它不仅涉及“常规”的对象引用,还涉及元对象引用,比如:字段、方法和类。这意味着只要有对字段、方法、类或 ClassLoader的对象的引用,ClassLoader就会驻留在JVM中。因为ClassLoader本身可以关联许多类及其静态字段,所以就有 许多内存被泄漏了。

确定泄漏的位置

通常发生内存泄漏的第一个迹象是:在应用程序中出现了OutOfMemoryError。这通常发生在您最不愿意它发生的生产环境中,此时几乎 不能进行调试。有可能是因为测试环境运行应用程序的方式与生产系统不完全相同,因而导致泄漏只出现在生产中。在这种情况下,需要使用一些开销较低的工具来 监控和查找内存泄漏。还需要能够无需重启系统或修改代码就可以将这些工具连接到正在运行的系统上。可能最重要的是,当进行分析时,需要能够断开工具而保持 系统不受干扰。

虽然OutOfMemoryError通常都是内存泄漏的信号,但是也有可能应用程序确实正在使用这么多的内存;对于后者,或者必须增加JVM 可用的堆的数量,或者对应用程序进行某种更改,使它使用较少的内存。但是,在许多情况下,OutOfMemoryError都是内存泄漏的信号。一种查明 方法是不间断地监控GC的活动,确定内存使用量是否随着时间增加。如果确实如此,就可能发生了内存泄漏。

详细输出

有许多监控垃圾收集器活动的方法。而其中使用最广泛的可能是使用-Xverbose:gc选项启动JVM,并观察输出。

[memory ] 10.109-10.235: GC 65536K->16788K (65536K), 126.000 ms 
箭头后面的值(本例中是16788K)是垃圾收集所使用的堆的容量。

控制台

查看连续不断的GC的详细统计信息的输出将是非常乏味的。幸好有这方面的工具。JRockit Management Console可以显示堆使用量的图示。借助于该图,可以很容易地看出堆使用量是否随时间增加。
diyblPic 
Figure 1. The JRockit Management Console

甚至可以配置该管理控制台,以便如果发生堆使用量过大的情况(或基于其他的事件),控制台能够向您发送电子邮件。这明显使内存泄漏的查看变得更容易了。

内存泄漏检测工具

还有其他的专门进行内存泄漏检测的工具。JRockit Memory Leak Detector可以用来查看内存泄漏,并可以更深入地查出泄漏的根源。这个强大的工具是紧密集成到JRockit JVM中的,其开销非常小,对虚拟机的堆的访问也很容易。

专业工具的优点

一旦知道确实发生了内存泄漏,就需要更专业的工具来查明为什么会发生泄漏。JVM自己是不会告诉您的。这些专业 工具从JVM获得内存系统信息的方法基本上有两种:JVMTI和字节码技术(byte code instrumentation)。Java虚拟机工具接口(Java Virtual Machine Tools Interface,JVMTI)及其前身Java虚拟机监视程序接口(Java Virtual Machine Profiling Interface,JVMPI)是外部工具与JVM通信并从JVM收集信息的标准化接口。字节码技术是指使用探测器处理字节码以获得工具所需的信息的技 术。

对于内存泄漏检测来说,这两种技术有两个缺点,这使它们不太适合用于生产环境。首先,它们在内存占用和性能降低 方面的开销不可忽略。有关堆使用量的信息必须以某种方式从JVM导出,并收集到工具中进行处理。这意味着要为工具分配内存。信息的导出也影响了JVM的性 能。例如,当收集信息时,垃圾收集器将运行得比较慢。另外一个缺点是需要始终将工具连在JVM上。这是不可能的:将工具连在一个已经启动的JVM上,进行 分析,断开工具,并保持JVM运行。

因为JRockit Memory Leak Detector是集成到JVM中的,就没有这两个缺点了。首先,许多处理和分析工作是 在JVM内部进行的,所以没有必要转换或重新创建任何数据。处理还可以背负(piggyback)在垃圾收集器本身上而进行,这意味着提高了速度。其次, 只要JVM是使用-Xmanagement选项(允许通过远程JMX接口监控和管理JVM)启动的,Memory Leak Detector就可以与运行中的JVM进行连接或断开。当该工具断开时,没有任何东西遗留在JVM中,JVM又将以全速运行代码,正如工具连接之前一 样。

趋势分析

让我们深入地研究一下该工具以及它是如何用来跟踪内存泄漏的。在知道发生内存泄漏之后,第一步是要弄清楚泄漏了 什么数据--哪个类的对象引起了泄漏?JRockit Memory Leak Detector是通过在每次垃圾收集时计算每个类的现有对象的数目来实现这一步的。如果特定类的对象数目随时间而增长(“增长率”),就可能发生了内存 泄漏。

diyblPic 
图2. Memory Leak Detector的趋势分析视图

因为泄漏可能像细流一样非常小,所以趋势分析必须运行很长一段时间。在短时间内,可能会发生一些类的局部增长, 而之后它们又会跌落。但是趋势分析的开销很小(最大开销也不过是在每次垃圾收集时将数据包由JRockit发送到Memory Leak Detector)。开销不应该成为任何系统的问题——即使是一个全速运行的生产中的系统。

起初数目会跳跃不停,但是一段时间之后它们就会稳定下来,并显示出哪些类的数目在增长。

找出根本原因

有时候知道是哪些类的对象在泄漏就足以说明问题了。这些类可能只用于代码中的非常有限的部分,对代码进行一次快 速检查就可以显示出问题所在。遗憾地是,很有可能只有这类信息还并不够。例如,常见到泄漏出在类java.lang.String的对象上,但是因为字符 串在整个程序中都使用,所以这并没有多大帮助。

我们想知道的是,另外还有哪些对象与泄漏对象关联?在本例中是String。为什么泄漏的对象还存在?哪些对象 保留了对这些对象的引用?但是能列出的所有保留对String的引用的对象将会非常多,以至于没有什么实际用处。为了限制数据的数量,可以将数据按类分 组,以便可以看出其他哪些对象的类与泄漏对象(String)关联。例如,String在Hashtable中是很常见的,因此我们可能会看到与 String关联的Hashtable数据项对象。由Hashtable数据项倒推,我们最终可以找到与这些数据项有关的Hashtable对象以及 String(如图3所示)。

diyblPic 
图3. 在工具中看到的类型图的示例视图

倒推

因为我们仍然是以类的对象而不是单独的对象来看待对象,所以我们不知道是哪个Hashtable在泄漏。如果我 们可以弄清楚系统中所有的Hashtable都有多大,我们就可以假定最大的Hashtable就是正在泄漏的那一个(因为随着时间的流逝它会累积泄漏而 增长得相当大)。因此,一份有关所有Hashtable对象以及它们引用了多少数据的列表,将会帮助我们指出造成泄漏的确切Hashtabl。

diyblPic 
图4. 界面:Hashtable对象以及它们所引用数据的数量的列表

对对象引用数据数目的计算开销非常大(需要以该对象作为根遍历引用图),如果必须对许多对象都这么做,将会花很 多时间。如果了解一点Hashtable的内部实现原理就可以找到一条捷径。Hashtable的内部有一个Hashtable数据项的数组。该数组随着 Hashtable中对象数目的增长而增长。因此,为找出最大的Hashtable,我们只需找出引用Hashtable数据项的最大数组。这样要快很 多。

diyblPic 
图5. 界面:最大的Hashtable数据项数组及其大小的清单

更进一步

当找到发生泄漏的Hashtable实例时,我们可以看到其他哪些实例在引用该Hashtable,并倒推回去看看是哪个Hashtable在泄漏。

diyblPic 
图 6. 这就是工具中的实例图

例如,该Hashtable可能是由MyServer类型的对象在名为activeSessions的字段中引用的。这种信息通常就足以查找源代码以定位问题所在了。

diyblPic 
图7. 检查对象以及它对其他对象的引用

找出分配位置

当跟踪内存泄漏问题时,查看对象分配到哪里是很有用的。只知道它们如何与其他对象相关联(即哪些对象引用了它 们)是不够的,关于它们在何处创建的信息也很有用。当然了,您并不想创建应用程序的辅助构件,以打印每次分配的堆栈跟踪(stack trace)。您也不想仅仅为了跟踪内存泄漏而在运行应用程序时将一个分析程序连接到生产环境中。

借助于JRockit Memory Leak Detector,应用程序中的代码可以在分配时进行动态添加,以创建堆栈跟踪。这些堆栈跟踪可以在工具中进行累积和分析。只要不启用就不会因该功能而产 生成本,这意味着随时可以进行分配跟踪。当请求分配跟踪时,JRockit 编译器动态插入代码以监控分配,但是只针对所请求的特定类。更好的是,在进行数据分析时,添加的代码全部被移除,代码中没有留下任何会引起应用程序性能降 低的更改。

diyblPic 
图8. 示例程序执行期间String的分配的堆栈跟踪

结束语

内存泄漏是难以发现的。本文重点介绍了几种避免内存泄漏的最佳实践,包括要始终记住在数据结构中所放置的内容,以及密切监控内存使用量以发现突然的增长。

我们都已经看到了JRockit Memory Leak Detector是如何用于生产中的系统以跟踪内存泄漏的。该工具使用一种三步式的方法来找出泄漏。首先,进行趋势分析,找出是哪个类的对象在泄漏。接下 来,看看有哪些其他的类与泄漏的类的对象相关联。最后,进一步研究单个对象,看看它们是如何互相关联的。也有可能对系统中所有对象分配进行动态的堆栈跟 踪。这些功能以及该工具紧密集成到JVM中的特性使您可以以一种安全而强大的方式跟踪内存泄漏并进行修复

我在这里举个例子:

java.lang.OutOfMemoryError这个错都见过,这就是内存泄露的结果。

在effective java item6中 讲了如下的例子:

public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[—size]; //这里仍存在对pop出去对象的强引用,问题来了
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

应该修改如下:

public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}

内存泄露(Memory Leak)还有个名称叫做

unintentional object retentions 非故意对象闭留

Continue reading JAVA内存泄漏——内存泄漏原因和内存泄漏检测工具

在ie下,button 标记恐怕还存在几个不大不小的问题。

在ie下,<button>标记恐怕还存在几个不大不小的问题。

  1. 在一个表单里,如果有一个以上"submit"类型的<button>标签存在,在表单被提交时,不管你点击哪个<button>,所有<button>的值都会被post/get。
  2. <button>的缺省type属性被设置为"button",但是在A级别浏览器下,应该设置为"submit"才对
  3. 如果你用javascript去访问<button>的value属性,IE却返回了<button>的innerHTML属性,很让人恼火。 (可以使用"getAttributeNode"方法来避免.)

例如,一段HTML:

CODE:
  1. <form>
  2. <button type="submit" value="1" name="action-1">text 1</button>
  3. <button type="submit" value="2" name="action-2">text 2</button>
  4. </form>

在IE下,上面两个button的数据都会被提交。并且ie会提交这两个button的innerText而不是value。而其它浏览器仅仅提交你点击的那个button。

Continue reading 在ie下,button 标记恐怕还存在几个不大不小的问题。

VV 评审指南

 

评审指南

1.      目的

指导相关人员进行评审的规范流程,预先发现开发和管理中的问题,以便于处理。

2.   范围

本文档应结合《验证过程文件》使用。

本文档适用于公司内部所有软件项目/产品的评审活动。

同行评审的适用范畴:适用于对项目过程中工作产品的评审,如需求评审、设计评审、技术解决方案评审、程序设计评审、变更评审等。
    管理评审的适用范畴:适用于对项目过程中管理活动的评审,如项目计划评审、里程碑评审、阶段性评审等。

单人评审的适用范畴:适用于工作产品是简单明了的,不可能有很多缺陷,而且也不是非常关键的评审,如代码检查、测试用例评审、测试观点评审、测试报告评审等。

3.   输入

待评审产品
评审检查表

与评审有关的软件过程及标准

4.   定义

同行评审:Peer Review(简称PR),由软件工作产品创建者的同行们检查该工作产品,识别产品的缺陷,改进产品的不足。

管理评审:由软件项目/产品管理者对项目过程中管理活动进行评估,识别过程缺陷,改进管理活动。

单人评审:由单独一个评审员对简单的工作产品进行评估,识别产品的缺陷,改进产品的不足。

代码检查:检查编写好的程序代码,发现不符合编码规范、不能实现设计要求的的问题,改进代码的质量

PM主管(Project Manager)

PL:项目经理(Project Leader)

SL:项目小组长(Sub- Leader)

QA:品质保证人员(Quality Assurance

QC:品质控制人员(Quality Control)

 

5.   职责与角色

5.1           同行评审中的角色和职责

 

角  色

职             责

评审组

接受过有关如何进行评审的培训,负责本次正式评审的组织,主持正式评审会议;

保证行动项和建议得到文档化; 跟踪与确认正式评审所提出行动项的落实;

报告评审的结果; 收集和报告同行评审所需数据.

项目经理

安排评审计划(是项目计划的一部分),并参与所有的主要文件评审.

QA

协助安排计划类正式评审,保证评审按照评审规程进行.(注:纯技术类评审QA可不参加)。

记录员/协调员

评审前准备材料;使用评审单,用标准化的形式对评审会议中提出的问题和缺陷进行记录,同时需要记录行动项和建议,并将记录结果的评审单原件交给评审组;将评审单电子文档化后提交给所有评审员.

作者

宣读作品、可提出初步问题进行评审、快速解决所有确定的问题、保持客观,避免抵抗态度。

 

5.2           管理评审中的角色和职责

 

角  色

职             责

项目主管

应定期或在遇到重大问题时对项目进展状况、遇到问题、质量管理状况、存在风险进行评审,并主持评审会议。

项目经理

应定期或在遇到重大问题时对项目进展状况、遇到问题、质量管理状况、存在风险进行评审。

QA

负责向项目经理报告质量管理的运行状况、对纠正和预防措施执行跟踪和验证。

小组负责人

负责小组内部状况报告、提供管理评审所需的相关资料;针对评审中提出的问题包括可能出现的问题,负责提出并组织采取纠正和预防措施。

记录员/协调员

对评审会议中提出的问题和项目情况进行记录,同时需要记录行动项和建议,并将记录结果电子文档化后提交给相关利益者;

 

5.3           单人评审中的角色和职责

 

角  色

职             责

项目经理

确定哪些工作产品可进行单人评审,指定评审员。跟踪缺陷直到其得到解决

评审员

接受过有关如何进行评审的培训,执行评审,保证行动项和建议得到文档化; 跟踪与确认评审所提出行动项的落实;

报告评审的结果; 收集和报告评审所需数据.

作者

宣读作品、快速解决所有确定的问题、保持客观,避免抵抗态度。记录评审结果。

 

6.   入口准则

Ø        评审组长被任命。

Ø        评审在相关计划中被定义。

Ø        被评审的产品准备就绪。

Ø        评审员经过评审规程的培训。

Ø        评审员应经过被评审问题的技能的培训。

Ø        协调员应当受过如何执行评审的正式培训,或者应当参加几次评审的经验。

Ø        《项目计划》已经制定。

7.   评审准则

7.1           同行评审准则

1)评审产品,而不是评审设计者(不能使设计者有任何压力);

2)会场要有良好的气氛;

3)限制争论与反驳(评审会不是为了解决问题,而是为了发现问题);

4)指明问题范围,而不是解决提到的问题;

5)展示记录(最好有黑板,将问题随时写在黑板上);

6)组评审时会议人数应在5-9人为佳;

7)组评审时评审员中应包括被评审产品作者的同行。(例如对程序设计文档的评审,评审员中应包括其他程序设计人员);

8)组评审时评审员中应包括被评审产品的上下游相关人员。(例如对程序设计文档的评审,评审员中应包括详细设计人员和后续的编码人员);

9)坚持会前准备工作;

10)            对全部评审人员进行必要的培训;

7.2           管理评审准则

1)评审产品,而不是评审设计者(不能使设计者有任何压力);

2)指明问题范围,而不是解决提到的问题;

3)评审人员接受过关于评审的必要的培训;

4)评审人员在被评审产品领域具有丰富经验;

7.3           单人评审准则

1)评价项目总体情况和进展状况;

2)评价小组内部的进度和人员状况;

3)评价项目质量控制情况;

4)评价项目进展中遇到的问题并提出解决办法;

5)评价项目当前存在的风险;

6)评价其他情况(视项目阶段而定)

8.   评审步骤

8.1           同行评审

步骤一:制定评审计划
    项目经理在项目策划阶段制定评审计划.
步骤二:评审准备
    1、按照项目计划,在评审会议的一天前(该时间越长越好),由项目经理识别必须参与评审的评审员、并指定记录员、评审会议主持人,然后通知相关利益者。在通常情况下,与项目整体相关的评审,主持人为PL;对于各个模块的评审,主持人为SL。

2、需要提前进行申请的,应至少提前半天向SL提出评审申请(同时要提交需要评审的工作产品)。SL根据项目的进度计划,确定评审会议的具体时间。
    3、由项目经理指定相应的评审员,确定评审相关输入,并确定准备就绪的准则.
    4、在评审会议的前一天将评审通知、待评审材料以及相关的参考资料发给每个评审员,以保证评审员有足够的时间来预审文件. 预定会议的场所等,评审通知的分发可采取邮件等形式。
    5、如果决定取消或推迟会议,需要重新通知所有相关利益者.

6.各评审员依据《评审检查表》对待评审材料进行预审。预审中发现的问题写入《评审检查表》对应的检查点后“备注”栏,如过发现的问题与对应的检查点无关,可在检查表下方填写。
步骤三:评审会议

    由主评审人掌握会议节奏和主持会议.

1、评审工作产品的担当者,对工作产品进行讲解,对评审组成员提出的问题进行解答。

2、对评审中提出的每一个问题必须要有明确的结论。

3、确定问题的修改者和确认者。修改者需要给出调查问题的时间。
    4、由评审员发现问题,并讨论确认;记录员对评审结果进行记录(包括缺陷,行动项和建议).记录写入《评审记录与报告》,其中发现的缺陷分类应依客户要求进行分类,客户无要求应参考《缺陷分类标准》

5、对评审的工作产品依据评审的标准做出结论。

(1)评审的标准(建议:可以按照项目组的规定或者项目组根据情况进行调整):存在的严重缺陷(问题)≤n个(0 =< n <= 10)时可以认为通过评审。
    6、评审结束前,记录员复述所有缺陷,并由评审员进行缺陷分类.
    7、评审完成时,得出结论是否通过评审. 若不通过评审,则确定下一次评审会议的时间.

(1)评审结论的分类:

l        评审通过

l        评审不通过,需要重新评审,要确定重新评审的时间

l        评审通过,但工作产品修改后需要进行Email(或其它方式)确认。

8、主评审人指定相关人员收集所有评审资料(评审通知, 评审检查表,评审材料等).

步骤四:对评审结果采取行动
    1、会议记录员整理会议内容,完成评审记录初稿。要求详细记录评审会中发现的问题,及讨论的结论。

2、会议记录员将评审记录的初稿提交会议的参与者进行确认。根据确认的情况修订评审记录,确认无误后,将评审记录发与参加会议的人员。
    3、由项目经理指定人员或文件作者对评审结果进行分析、确定问题解决计划、对工作产品进行返工,并记录《评审记录与报告》中返工的信息。

4、有《评审记录与报告》不能涵盖的内容需要记录时,由项目经理指定人员撰写《会议纪录》
步骤五:评审结果被跟踪直至完成
    评审结果跟踪处理方式如下:

评审结果

跟踪处理方式

通过

不作修改

稍作修改

1、由项目经理指定人员或者QC对返工结果进行跟踪直到关闭,并记录《评审记录与报告》中跟踪的信息.

2、确认者按照确定的时间对修改的问题进行确认,并将结果及时间填写到《评审记录与报告》中。

不通过

重新修正编制

组织复审


步骤六:提交和归档
    由项目经理指定人员将评审资料交由配置管理员归档.文件提交参见《配置管理指南》

8.2           单人评审

考虑到成本,如果工作产品有很多缺陷或比较关键,那么工作产品应该采取上述同行评审会议的形式。但是如果工作产品是简单明了的,不可能有很多缺陷,而且也不是非常关键,则建议采取单人评审的形式。

单人评审的形式类似于同行评审过程:

1.和项目经理协商后,作者确定评审员。

2.安排了评审后,评审员提前收到评审材料。

3.评审员采用检查表独立的评审工作产品并准备和作者的会议。

4.评审会议只有两个人参与——作者和评审员,在会议中产生缺陷记录(《评审记录与报告》中)。返工事项和行动项被记录和跟踪,以确保其被解决。

项目经理负责跟踪缺陷直到其得到解决。

代码检查、测试用例评审、测试观点评审、测试报告评审等可采用单人评审的形式。

8.3           管理评审

步骤一:制定评审计划

在项目策划阶段制定评审计划.

步骤二:评审准备

1、按照项目计划,在评审会议的一天前(该时间越长越好),由项目经理识别必须参与评审的评审员、并指定记录员,然后通知相关利益者。在通常情况下,项目管理评审主持人为PM。   
2、在评审会议的前一天将评审通知发给每个评审员,评审通知的分发可采取邮件、传真等形式.
3、如果决定取消或推迟会议,需要重新通知所有相关利益者.

步骤三:评审会议
1、由项目经理掌握会议节奏和主持会议.
2、由评审员发现问题,并讨论确认;记录员对评审结果进行记录(包括缺陷,行动项和建议). 记录写入《评审记录与报告》中。
3、评审结束前,记录员复述所有缺陷。

步骤四:对评审结果采取行动

1、会议记录员整理会议内容,完成评审记录初稿。要求详细记录评审会中发现的问题,及讨论的结论。

2、会议记录员将评审记录的初稿提交会议的参与者进行确认。根据确认的情况修订评审记录,确认无误后,将评审记录发与参加会议的人员。
3、由项目经理指定人员或文件作者对评审结果进行分析、确定问题解决方案和计划。

4、有《评审记录与报告》不能涵盖的内容需要记录时,由项目经理指定人员撰写《会议纪录》

步骤五:评审结果被跟踪直至完成
       评审结果跟踪处理方式如下:

评审结果

跟踪处理方式

通过

不作修改

稍作修改

1、由项目经理指定人员或者QC对返工结果进行跟踪直到关闭,并记录《评审记录与报告》中跟踪的信息.

2、确认者按照确定的时间对修改的问题进行确认,并将结果及时间填写到《评审记录与报告》中。

不通过

重新修正编制

组织复审

 

步骤六:提交和归档
由项目经理指定人员或者QC将评审资料交由配置管理员归档。

9.   出口准则

评审中发现的所有问题和缺陷都已经得到解决(关闭)。评审结论为“不通过”时要求一定要安排进行复审。

10. 裁剪

1.      工作产品是否需要进行评审可参考文件《验证确认区分表》

2.      评审通常应采用会议评审的形式,如果有特殊原因不能采用会议评审时,需由项目经理、项目主管共同批准认定。其他形式的评审,包括电子Email、网络视频、等方式,基本同样按照上述5.2内容中进行,另需对各联络的信件(电子信件)进行保存,对评审的结果以信件或其他方式通知参与评审的人员。

3.      必要时可以采用会签的方式最终确认评审结果,会签时应打印评审报告,由评审员手工签名确认。

11. 输出

已通过评审的产品
    评审通知,评审检查表,评审报告(评审缺陷列表、评审结论)

12. 度量

1)评审工作量

2)评审发现缺陷数

3)被评审产品缺陷率

Continue reading VV 评审指南

Oracle导出数据库结构到PowerDesigner

 

Oracle导出数据库结构到PowerDesigner:

具体的操作步骤:打开PowerDesigner-》菜单栏 “Database”-》Database Reverse Engineering-》Using an ODBC data source中选择右边的浏览-》connect to an ODBC Data Source-》在Machine data source中选择你配置好的数据源。

(注:如果第一次连接,需要先配置数据源,步骤如下:点击“Add”按钮,

-》ODBC数据源管理器-》点击“添加”按钮-》创建新数据源-》选择“Oracle in OraHome92” -》完成。

-》进入“Oracle ODBC Driver Configuration”-》配置数据源名称(Data Source Name)以及监听器(TNS Service Name)-》配置完成后点击右边的“Test Connection”。

-》在弹出框里输入连接数据库的用户名和密码-》点击OK-》提示Connection successful-》OK-》数据源配置成功。

-》退出数据源配置后,在数据源连接对话框中(Connect to an ODBC Data Source)中选择好刚才配置的数据源,然后再次填写用户名和密码。

-》点击connect,就可连接到数据库上。)

Continue reading Oracle导出数据库结构到PowerDesigner

什么叫n+1次select查询问题?

 

我说应该叫1 + N问题

在 Session的缓存中存放的是相互关联的对象图。默认情况下,当Hibernate从数据库中加载Customer对象时,会同时加载所有关联的 Order对象。以Customer和Order类为例,假定ORDERS表的CUSTOMER_ID外键允许为null,图1列出了CUSTOMERS 表和ORDERS表中的记录。

以下Session的find()方法用于到数据库中检索所有的Customer对象:

List customerLists=session.find("from Customer as c");

运行以上find()方法时,Hibernate将先查询CUSTOMERS表中所有的记录,然后根据每条记录的ID,到ORDERS表中查询有参照关系的记录,Hibernate将依次执行以下select语句:

select * from CUSTOMERS;
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;

通过以上5条select语句,Hibernate最后加载了4个Customer对象和5个Order对象,在内存中形成了一幅关联的对象图,参见图2。

Hibernate在检索与Customer关联的Order对象时,使用了默认的立即检索策略。这种检索策略存在两大不足:

(1) select语句的数目太多,需要频繁的访问数据库,会影响检索性能。如果需要查询n个Customer对象,那么必须执行n+1次select查询语句。这就是经典的n+1次select查询问题。这种检索策略没有利用SQL的连接查询功能,例如以上5条select语句完全可以通过以下1条select语句来完成:

select * from CUSTOMERS left outer join ORDERS
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID

以上select语句使用了SQL的左外连接查询功能,能够在一条select语句中查询出CUSTOMERS表的所有记录,以及匹配的ORDERS表的记录。

(2)在应用逻辑只需要访问Customer对象,而不需要访问Order对象的场合,加载Order对象完全是多余的操作,这些多余的Order对象白白浪费了许多内存空间。
为了解决以上问题,Hibernate提供了其他两种检索策略:延迟检索策略和迫切左外连接检索策略。延迟检索策略能避免多余加载应用程序不需要访问的关联对象,迫切左外连接检索策略则充分利用了SQL的外连接查询功能,能够减少select语句的数目。

Continue reading 什么叫n+1次select查询问题?

java执行groovy的方式

 

脚本全文(不需要包名匹配,因为这是脚本):

println("hello!this is a script!");

类全文:

package co.java;

import java.io.File;
import java.io.IOException;

import org.codehaus.groovy.control.CompilationFailedException;

import groovy.lang.GroovyClassLoader;
import groovy.lang.Script;
import groovy.util.GroovyScriptEngine;

public class InvokeGroovyTest
{
    public static void main(String[] args)
    {
        InvokeGroovyTest test = new InvokeGroovyTest();
       
//        test.way1();
        test.way2();
    }
   
    public void way1()
    {
        try
        {
            ClassLoader cl = InvokeGroovyTest.class.getClassLoader();
            GroovyClassLoader groovyCl = new GroovyClassLoader(cl);
            Class groovyClass = groovyCl.parseClass(new File("src/co/java/Script.groovy"));
            Script sc = (Script)groovyClass.newInstance();
            sc.run();
        }
        catch (Exception e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }   
       
    }
   
    public void way2()
    {
        try
        {
            GroovyScriptEngine engine = new GroovyScriptEngine(".");
            engine.run("src/co/java/Script.groovy", (String)null);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }       
    }
}

Continue reading java执行groovy的方式

InvalidDestinationException: MQJMS0003: Destination not understood or no longer valid

 

网上几点总结:
①=======================================================================================
Question 4: A WSAS administrator notices the following error in the SystemOut.log:

javax.jms.InvalidDestinationException:
MQJMS0003: Destination not understood or no longer valid.

Which of the following could have caused this error?

A. The JMS destination requires IIOP protocol for access. The JMS client is using the default JMS message protocol.
B. The JMS destination is running on a non-WebSphere environment.
C. The JMS destination was accidentally deleted.
D. The JNDI binding to the destination was changed.
E. The JMS destination is listening on a non standard TCP port.
me: http://www-1.ibm.com/support/docview.wss?uid=swg1IY60708
PROBLEM SUMMARY:
This problem is caused by using JMS interfaces to access MQ JMS
classes (i.e. casting a com.ibm.mq.MQQueue to a
javax.jms.Queue).

The use of an (instanceof MQQueue) test was returning false
when an MQQueue object had been cast as a javax.jms.Queue. The
logic then treated this as an invalid object, and threw an
InvalidDestinationException.

②=======================================================================================
MQJMS0003    Destination not understood or no longer valid.

Explanation: The queue or topic may have become unavailable, the application may be using an incorrect connection for the queue or topic, or the

supplied destination is not of the correct type for this method.
User Response: Check that WebSphere MQ is still running and the queue manager is available. Check that the right connection is being used for your

queue or topic.

③=======================================================================================
The problem is the IBM class com.ibm.mq.jms.MQSession.createQSender has the following code

**************************************************************************************************************************************************
if(!(queue instanceof com.ibm.mq.jms.MQQueue))
{
String s1 = "MQJMS0003";
String s3 = ConfigEnvironment.getErrorMessage(s1);
s3 = s3 + buildExceptionString(queue);
InvalidDestinationException invaliddestinationexception = new InvalidDestinationException(s3, s1);
throw invaliddestinationexception;
}
mqqueue = (com.ibm.mq.jms.MQQueue)queue;
mqqueue1 = getServicesMgr().getOutputQueue(mqqueue, this);
}
**************************************************************************************************************************************************
At first all looks straight for ward but there is a bigger problem.

instanceOf operator of java would return false for objects of the same class if they are being loaded by different classloaders.

In my case i had many webapplications and the Queue object was got by a jndi lookup. Now when first time this code is executed it all works fine

for a application but when you start using anohter web application the same code fails and throws the InvalidDestinationException .

The reason being the 2 objects on which ibm code is doing instanceof are loaded by different classloaders.

The Solution to this problem is delete all the ibm jars (com.ibm.mq.jar and com.ibm.mqjms.jar) from individual web application and add these in

the servers lib directory. Now all instances are loaded by the server classloader and not by webappclassloaders .

This is a criptic issue which took me 2 days to figure out.

wfac错误原因:
配置的factory和destination是两个不同的实现,factory指向的是WAS MQ,而queue指向的是sonic MQ,所以出现此报错。

Continue reading InvalidDestinationException: MQJMS0003: Destination not understood or no longer valid

WSAD环境下JMS异步通信全攻略

 

一、JMS基本概念

1.1 P2P通信

1.2 Pub/Sub通信

二、JMS消息

三、JMS P2P编程

3.1 使用JMS QueueConnection对象

        3.2 处理回退事件

3.3 关闭JMS对象

3.4 接收消息

3.5 消息驱动的Bean

3.6 消息持久化

3.7 消息选择器

四、JMS Pub/Sub编程

五、二阶段提交的事务

━━━━━━━━━━━━━━━━━━━━━━━━━━

EJB 2.0和J2EE 1.3规范开始提供对Java消息服务(JMS)的支持。在J2EE 1.3加入JMS之前,J2EE环境中的组件通过RMI-IIOP协议通信,J2EE是一个完全同步的平台。由于在J2EE 1.3规范中引入了JMS,J2EE环境开始具备一项极其重要的功能--异步通信。

● 说明:RMI-IIOP是Java远程方法调用(RMI,Remote Method Invocation)的一个兼容CORBA的版本,CORBA是Common Object Request Broker Architecture的缩写,即公用对象请求代理(调度)体系结构。RMI-IIOP通过CORBA平台和语言中立的通信协议发送RMI消息。

为了支持异步通信,J2EE 1.3规范还引入了一种新的EJB类型:消息驱动的Bean,即Message Driven Bean,简称MDB。如前所述,在JMS之前,J2EE原来是一个建立在Java RMI-IIOP通信协议基础上的同步环境,但MDB却具有接收异步消息的能力。

异步通信使得企业应用能够建立在一种全新的通信机制之上,它具有如下重要优点:

■ 异步通信程序不直接交换信息。由于这个原因,通信的任意一方能够在对方停止响应时继续自己的操作,例如当接收消息的一方不能响应时,发送方仍能够发出消息。

■ 异步通信有助于提高可靠性。支持异步通信的中间件软件提供了有保证的(可靠的)消息递送机制,即使整个环境出现问题也能够保证消息传递到目的地。 Internet是一种不可靠的通信媒体,对于通过Internet通信的应用来说,异步通信的这一特点非常富有吸引力。

■ 发送异步消息的程序不会在等待应答的时候被锁定,从而极大地有利于提高性能。

JMS本身不是一个通信软件,而是一个标准的应用编程接口(API),用来建立厂商中立的异步通信机制。从这个意义上说,JMS类似于JDBC和 JNDI,例如就JDBC来说,它要求有一个底层的数据库提供者,JMS则要求有一个支持JMS标准的底层异步通信中间件提供者,一般称为面向消息的中间件(Message-Oriented Middleware,MOM)。

MOM是一种支持应用程序通过交换消息实现异步通信的技术。在某种程度上,异步通信有点象是人们通过email进行通信;类似地,同步通信的程序就象是人们通过电话进行通信。在异步通信过程中,程序的结合方式是宽松的,换句话说,异步通信的程序并不直接相互联系,而是通过称为队列(Queue)或主题(Topic)的虚拟通道联系。

同时,异步通信还意味着消息通过一个中转站,按照存储-转发的方式传递,即使通信的接收方当时并未运行,另一方(发送方)的程序也能够顺利发出消息,一旦接收方的程序后来开始运行,消息就会传递给它。

JMS通信主要的优点是提供了一个环境,在这个环境中各个程序通过标准的API进行通信,开发者不必再面对各种不同操作系统、数据表示方式、底层协议所带来的复杂性。

本文讨论了JMS编程的基本概念以及两种JMS通信方式:第一种是端对端通信(Point-to-Point,P2P)方式,第二种是出版/订阅(Publish/Subscribe,Pub/Sub)方式,另外还涵盖了JMS消息结构、JMS主要对象、MDB、JMS和WebSphere编程、消息持久化以及JMS事务支持方面的问题。

一、JMS基本概念

鉴于JMS是一种比较新的技术,所以本文将首先详细介绍JMS编程的一般概念,然后再探讨WSAD 5.0环境下的JMS开发。如前所示,JMS程序本身并不直接相互通信,消息被发送给一个临时中转站--队列或主题。队列和主题都是能够收集消息的中转对象,但两者支持的消息传递机制又有所不同,分别对应两种不同的通信方式--P2P和Pub/Sub。

1.1 P2P通信

P2P消息传递又可以按照推(Push)和拉(Pull)两种方式运作。在P2P拉方式中,程序通过称为队列的虚拟通道通信:在通信会话的发送方,发送程序把一个消息"放入"队列,在接收方,接收程序定期扫描队列,寻找它希望接收和处理的消息。和推方式相比,拉方式的消息传递效率较低,因为它需要周而复始地检查消息是否到达,这个过程会消耗一定的资源。另外必须注意的一点是,当接收方发现一个需要处理的消息时,它就会"提取"消息,从效果上看等于从队列删除了消息。

因此,即使有多个接收程序在处理同一个队列,对于某一特定的消息来说,总是只有一个接收者。JMS程序可以使用多个队列,每一个队列可以由多个程序处理,但是只有一个程序才会收到某个特定的消息。

在P2P推方式的消息传递中,发送程序的工作原理也一样,它把消息发送到队列,但接收程序的工作原理有所不同。接收程序实现了一个Listener接口,包括实现了该接口中的onMessage回调方法,在J2EE环境中监听队列接收消息的任务交给了容器,每当一个新的消息达到队列,容器就调用 onMessage方法,将消息作为参数传递给onMessage方法。

P2P通信最重要的特点(不管是推还是拉)是:每一个消息总是只由一个程序接收。一般而言,P2P程序在通信过程中参与的活动较多,例如,发送程序可以向接收程序指出应答消息应当放入哪一个队列,它还可以要求返回一个确认或报告消息。

1.2 Pub/Sub通信

在Pub/Sub通信方式中,程序之间通过一个主题(Topic)实现通信,用主题作为通信媒介要求有Pub/Sub代理程序的支持(稍后详细介绍)。在消息发送方,生产消息的程序向主题发送消息;在接收方,消息的消费程序向感兴趣的主题订阅消息。当一个消息到达主题,所有向该主题订阅的消费程序都会通过onMessage方法的参数收到消息。

这是一种推式的消息传递机制。可以设想,会有一个以上的消费程序收到同一消息的副本。相比之下,程序在Pub/Sub通信过程中参与的活动较少,当生产者程序向某个特定的队列发送消息,它不知道到底会有多少程序接收到该消息(可能有多个,可能没有)。通过订阅主题实现的通信是一种比较灵活的通信方式,主题的订阅者可以动态地改变,却不需要改动底层的通信机制,而且它对整个通信过程完全透明。

Pub/Sub方式的通信要求有Pub/Sub代理的支持。Pub/Sub代理是一种协调、控制消息传递过程,保证消息传递到接收方的程序。P2P通信方式中程序利用一个队列作为通信的中转站,相比之下,Pub/Sub通信方式中程序直接交互的是特殊的代理队列,这就是我们要在 WebSphere MQ常规安装方式之上再装一个MA0C的原因(只有用WebSphere MQ作为JMS提供者时才必须如此。要求使用MQ 5.3.1或更高的版本),MA0C就是一个支持Pub/Sub方式通信的代理软件。

QueueConnectionFactory和TopicConnectionFactory对象是创建相应的QueueConnection对象和 TopicConnection对象的JMS工厂类对象,JMS程序正是通过QueueConnection对象和TopicConnection对象连接到底层的MOM技术。

二、JMS消息

基于JMS的程序通过交换JMS消息实现通信,JMS消息由三部分构成:消息头,消息属性(可选的),消息正文。消息头有多个域构成,包含了消息递送信息和元数据。

消息属性部分包含一些标准的以及应用特定的域,消息选择器依据这些信息来过滤收到的消息。JMS定义了一组标准的属性,要求MOM提供者支持(如表1所示)。消息正文部分包含应用特定的数据(也就是要求递送到目标位置的内容)。

表1 JMS标准消息属性

可选的属性包括JMSXUserID、JMSXAppID、JMSXProducerTXID、ConsumerTXID、 JMSXRcvTimestamp、JMSXDeliveryCount以及JMSXState。消息头为JMS消息中间件提供了描述性信息,诸如消息递送的目标、消息的创建者、保留消息的时间,等等,如表2所示。

表2 JMS消息头的域

*在所有这些消息类型中,TextMessage是最常用的,它不仅简单,而且能够用来传递XML数据。

JMS支持多种消息正文类型,包括:

■ textMessage:消息正文包含一个java.lang.String对象。这是最简单的消息格式,但可以用来传递XML文档。

■ ObjectMessage:消息正文中包含了一个串行化之后的Java对象,这类Java对象必须是可串行化的。

■ MapMessage:消息正文包含一系列"名字-值"形式的数据元素,通常用来传送一些按键-值形式编码的数据。设置数据元素可以用setInt、 setFloat、setString等方法;在接收方,提取数据元素使用相应的getInt、getFloat、getString等方法。

■ BytesMessage:消息正文包含一个字节数组。如果需要发送应用生成的原始数据,通常采用这一消息类型。

■ StreamMessage:消息正文包含一个Java基本数据类型(int,char,double,等等)的流。接收方读取数据的次序跟发送方写入数据的次序一样,读写消息元素分别使用readInt和writeInt、readString和writeString之类的方法。

JMS消息对象可以用JMS会话对象(参见本文"使用JMS QueueConnection对象"部分)创建。下面的例子显示了如何创建各类消息对象:

TextMessage textMsg = session.createTextMessage();
MapMessage mapMsg = session.createMapMessage();
ObjectMessage objectMsg = session.createObjectMessage();
BytesMessage byteMsg = session.createBytesMessage();

消息对象提供了设置、提取各个消息头域的方法,下面是几个设置和提取JMS消息头域的例子:

// 获取和设置消息ID
String messageID = testMsg.getJMSMessageID();
testMsg.setJMSCorrelationID(messageID);
// 获取和设置消息优先级
int messagePriority = mapMsg.getJMSPriority();
mapMsg.setJMSPriority(1);

另外,消息对象还为标准属性和应用特有的属性提供了类似的设置和提取方法。下面的例子显示了如何设置、提取JMS消息标准属性和应用特有属性域:

int groupSeq = objectMsg.getIntProperty("JMSGroupSeq");
objectMsg.setStringProperty("myName", "孙悟空");

JMS为读写消息正文内容提供了许多方法。下面略举数例,说明如何操作不同类型的消息正文:

// Text Message
TextMessage textMsg = session.createTextMessage();
textMsg.setText("消息内容文本");

// Map Message
MapMessage mapMsg = session.createMapMessage();
mapMsg.setInt(BookCatalogNumber, 100);
mapMsg.setString(BookTitle, "书籍题目");
mapMsg.setLong(BookCost, 50.00);
String bookTitle = mapMsg.getString("BookTitle");

// Object Message
ObjectMessage objectMsg = session.createObjectMessage();
Book book = new Book("WinSocks 2.0");
objectMsg.setObject(book);

三、JMS P2P编程

在JMS P2P通信方式中,发送程序将消息放入一个队列,根据通信要求,发送程序可以要求一个应答信息(请求-应答模式),也可以不要求立即获得应答(发送-遗忘模式)。如果需要应答信息,发送程序通过消息头的JMSReplayTo域向消息的接收程序声明应答信息应当放入哪一个本地队列。

在请求 -应答模式中,发送程序可以按照两种方式操作。一种称为伪同步方式,发送程序在等待应答消息时会被阻塞;另一种是异步方式,发送程序发送消息后不被阻塞,可以照常执行其他处理任务,它可以在以后适当的时机检查队列,查看有没有它希望得到的应答信息。下面的代码片断显示了JMS程序发送消息的过程。

// 发送JMS消息
import javax.jms.Message;
import javax.jms.TextMessage;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueConnection;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Queue;
import javax.jms.JMSException;
import javax.naming.InitialContext;
import javax.naming.Context;

public class MyJMSSender {
private String requestMessage;
private String messageID;
private int requestRetCode = 1;
private QueueConnectionFactory queueConnectionFactory = null;
private Queue requestQueue = null;
private Queue responseQueue = null;
private QueueConnection queueConnection = null;
private QueueSession queueSession = null;
private QueueSender queueSender = null;
private TextMessage textMsg = null;

// 其他代码...
public int processOutputMessages(String myMessage) {
// 查找管理对象
try {
InitialContext initContext = new InitialContext();
Context env = (Context) initContext.lookup
("java:comp/env");
queueConnectionFactory = (QueueConnectionFactory)
env.lookup("tlQCF");
requestQueue = (Queue) env.lookup("tlReqQueue");
responseQueue = (Queue) env.lookup("tlResQueue");
queueConnection = queueConnectionFactory.
createQueueConnection();
queueConnection.start();
queueSession = queueConnection.createQueueSession
(true, 0);
queueSender = queueSession.createSender(requestQueue);
textMsg = queueSession.createTextMessage();
textMsg.setText(myMessage);
textMsg.setJMSReplyTo(responseQueue);
// 处理消息的代码逻辑...
queueSender.send(textMsg);
queueConnection.stop();
queueConnection.close();
queueConnection = null;
} catch (Exception e) {
// 异常处理代码...
}
return requestRetCode = 0;
}
}

下面来分析一下这段代码。JMS程序的第一个任务是找到JNDI名称上下文的位置。对于WSAD开发环境来说,如果JMS程序属于J2EE项目,则 JNDI名称空间的位置由WSAD测试服务器管理,运行时环境能够自动获知这些信息。在这种情况下,我们只要调用InitialContext类默认的构造函数创建一个实例就可以了,即:

InitialContext initContext = new InitialContext();

对于WSAD环境之外的程序,或者程序不是使用WSAD JNDI名称空间,例如使用LDAP(轻量级目录访问协议),程序寻找JNDI名称的操作稍微复杂一点,必须在一个Properties或 Hashtable对象中设定INITIAL_CONTEXT_FACTORY和PROVIDER_URL,然后将该Properties对象或 Hashtable对象作为参数调用InitialContext的构造函数。下面我们来看几个创建InitialContext对象的例子,第一个例子显示了运行在WSAD之外的程序如何找到WSAD InitialContext对象。

// 例一:运行在WSAD之外的程序寻找WSAD InitialContext对象
// 说明:将localhost替换为JNDI服务运行的服务器名称
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY,
"com.ibm.websphere.naming.WsnInitialContextFactory");
props.put(Context.PROVIDER_URL, "iiop://localhost/");
InitialContext initialContext = InitialContext(props);

// 例二:下面的例子显示了如何找到基于文件的JNDI InitialContext
Hashtable hashTab = new Hashtable ();
hashTab.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
hashTab.put(Context.PROVIDER_URL, "file://c:/temp");
InitialContext initialContext = InitialContext(hashTab);

// 例三:下面的例子显示了如何找到基于LDAP的JNDI InitialContext
Hashtable hashTab = new Hashtable ();
hashTab.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
hashTab.put(Context.PROVIDER_URL,
"file://server.company.com/o=provider_name, c=us");
InitialContext initialContext = InitialContext(hashTab);

获得InitialContext对象之后,下一步是要查找java:comp/env子上下文(Subcontext),例如:

Context env = (Context) initContext.lookup("java:comp/env";

java:comp/env是J2EE规范推荐的保存环境变量的JNDI名称子上下文。在这个子上下文中,JMS程序需要找到几个由JMS管理的对象,包括QueueConnectionFactory对象、Queue对象等。

下面的代码寻找由JMS管理的几个对象,这些对象是JMS程序执行操作所必需的。

queueConnectionFactory = (QueueConnectionFactory) env.lookup("QCF");
requestQueue = (Queue) env.lookup("requestQueue");

接下来,用QueueConnectionFactory对象构造出QueueConnection对象。

queueConnection = queueConnectionFactory.createQueueConnection();

3.1 使用JMS QueueConnection对象

JMS QueueConnection提供了一个通向底层MOM(就本文而言,它是指WebSphere MQ队列管理器)的连接,按照这种方式创建的连接使用默认的Java绑定传输端口来连接到本地的队列管理器。

对于MQ客户程序来说(运行在不带本地队列管理器的机器上的MQ程序),QueueConnectionFactory对象需要稍作调整,以便使用客户端传输端口:

QueueConn.setTransportType(JMSC.MQJMS_TP_CLIENT_MQ_TCPIP);

另外,刚刚创建的QueueConnection对象总是处于停止状态,需要调用"queueConnection.start();"将它启动。

建立连接之后,用QueueConnection对象的createQueueSession方法来创建一次会话。必须注意的是,QueueSession对象有一个单线程的上下文,不是线程安全的,因此会话对象本身以及在会话对象基础上创建的对象都不是线程安全的,在多线程环境中必须加以保护。createQueueSession方法有两个参数,构造一个QueueSession对象的语句类如:

queueSession = queueConnection.createQueueSession
(false, Session.AUTO_ACKNOWLEDGE);

createQueueSession方法的第一个参数是boolean类型,它指定了JMS事务类型--也就是说,当前的队列会话是事务化的(true)还是非事务化的(false)。JMS事务类型主要用于控制消息传递机制,不要将它与EJB的事务类型(NotSupported,Required,等等)混淆了,EJB的事务类型设定的是EJB模块本身的事务上下文。 createQueueSession方法的第二个参数是一个整数,指定了确认模式,也就是决定了如何向服务器证实消息已经传到。

如果队列会话是事务化的(调用createQueueSession方法的第一个参数是true),第二个参数的值将被忽略,因为提交事务时应答总是自动执行的。如果由于某种原因事务被回退,则不会有应答出现,视同消息尚未递送,JMS服务器将尝试再次发送消息。如果有多个消息参与到同一会话上下文,它们被作为一个组处理,确认最后一个消息也就自动确认了此前所有尚未确认的消息。如果发生回退,情况也相似,整个消息组将被视为尚未递送,JMS服务器将试图再次递送这些消息。

下面说明一下底层的工作机制。当发送程序发出一个消息,JMS服务器接收该消息;如果消息是持久性的,服务器先将消息写入磁盘,然后确认该消息。自此之后,JMS服务器负责把消息发送到目的地,除非它从客户程序收到了确认信息,否则不会从临时存储位置删除消息。对于非持久性的消息,收到消息并保存到内存之后确认信息就立即发出了。

如果队列会话是非事务化的(调用 createQueueSession方法的第一个参数是false),则应答模式由第二个参数决定。第二个参数的可能取值包括:AUTO_ACKNOWLEDGE,DUPS_OK_ACKNOWLEDGE,以及CLIENT_ACKNOWLEDGE。

■ 对于非事务化的会话来说,AUTO_ACKNOWLEDGE确认模式是最常用的确认模式。对于事务化的会话,系统总是假定使用AUTO_ACKNOWLEDGE确认模式。

■ DUPS_OK_ACKNOWLEDGE模式是一种"懒惰的"确认方式。可以想到,这种模式可能导致消息提供者传递的一些重复消息出错。这种确认模式只用于程序可以容忍重复消息存在的情况。

■ 在CLIENT_ACKNOWLEDGE模式中,消息的传递通过调用消息对象的acknowledge方法获得确认。

在AUTO_ACKNOWLEDGE模式中,消息的确认通常在事务结束处完成。CLIENT_ACKNOWLEDGE使得应用程序能够加快这一过程,只要处理过程一结束就立即予以确认,恰好在整个事务结束之前。当程序正在处理多个消息时,这种确认模式也是非常有用的,因为在这种模式中程序可以在收到所有必需的消息后立即予以确认。

对于非事务化的会话,一旦把消息放入了队列,所有接收程序立即能够看到该消息,且不能回退。对于事务化的会话,JMS事务化上下文把多个消息组织成一个工作单元,消息的发送和接收都是成组执行。事务化上下文会保存事务执行期间产生的所有消息,但不会把消息立即发送到目的地。

只有当事务化的队列会话执行提交时,保存的消息才会作为一个整体发送给目的地,这时接收程序才可以看到这些消息。如果在事务化队列会话期间出现错误,在错误出现之前已经成功处理的消息也会被撤销。定义成事务化的队列会话总是有一个当前事务,不需要显式地用代码来开始一个事务。当然,事务化的环境总是存在一定的代价--事务化会话总是要比非事务化会话慢。

● 注意:JMS队列会话的事务化是一个概念,实现了JMS逻辑的Java方法的事务属性是另一个概念,不要将两者混淆了。TX_REQUIRED属性表示的是方法在一个事务上下文之内运行,从而确保数据库更新和队列中消息的处理作为一个不可分割的工作单元执行(要么都成功提交,要么都回退)。顺便说明一下,在容器管理的事务环境中,一个全局性的两阶段提交(Two-Phase Commit)事务上下文会被激活(参见本文后面的详细说明),这时参与全局性事务的数据源应当用XA版的数据库驱动程序构建。

相比之下,在createQueueSession方法的第一个参数中指定true建立的是JMS事务上下文:多个消息被视为一个工作单元。在消息的接收方,执行queueSession.commit()之前,即使已经收到了多个消息也不会给出确认信息;一旦queueSession.commit() 执行,它就确认收到了在此之前尚未提交的所有消息;消息发送方的情况也相似。

3.2 处理回退事件

如前所述,如果事务异常终止,收到的消息会被发回到原来的队列。接收消息的程序下一次再来处理该队列时,它还会再次得到同一个消息,而且这一次事务很有可能还是要失败,必须再次把消息发送回输入队列--如此不断重复,就形成了无限循环的情况。

为防止这种情况出现,我们可以设置监听端口上的Max_retry计数器,超过Max_retry计数器规定的值,接收程序就不会再收到该消息;或者对于推式的会话,消息不会再被传递。另外,在推式会话中,重新传递的事务会被设置JMSRedelivered标记,程序可以调用消息对象的 getJMSRedelivered方法来检查该标记。

消息通过QueueSender JMS对象发送,QueueSender对象利用QueueSession对象的createSender方法创建,每一个队列都要创建一个QueueSender对象:

queueSender = queueSession.createSender(requestQueue);

接下来创建一个消息(TextMessage类型),根据myMessage字符串的值设置消息的内容,myMessage变量作为输入参数传递给queueSession.createTextMessag方法。

textMsg = queueSession.createTextMessage(myMessage);

指定接收队列,告诉接收消息的程序要把应答放入哪一个队列:

textMsg.setJMSReplyTo(responseQueue);

最后,用Sender对象发送消息,然后停止并关闭连接。

queueSender.send(textMsg);
queueConnection.stop();
queueConnection.close();

发出消息之后,可以调用消息对象的getJMSMessageID方法获得JMS赋予消息的ID(即提取JMSMessageID消息头域),以后就可以通过这一ID寻找应答消息:

String messageID = message.getJMSMessageID();

如果有JMS连接池,释放后的会话不会被拆除,而是被返回给连接池以供重用。

3.3 关闭JMS对象

垃圾收集器不一定会及时关闭JMS对象,如果程序要创建大量短期生存的对象,可能会引起问题,至少会浪费大量宝贵的资源,所以显式地释放所有不用的资源是很重要的。

if (queueConn != null) {
queueConn.stop();
queueConn.close();
queueConn = null;
}

关闭队列连接对象将自动关闭所有利用该连接对象创建的对象。但个别JMS提供者例外,这时请按照下面代码所示的次序关闭所有JMS对象。

// 关闭JMS对象
if (queueReceiver != null) {
queueReceiver.close();
queueReceiver = null;
}
if (queueSender != null) {
queueSender.close();
queueSender = null;
}
if (queueSession != null) {
queueSession.close();
queueSession = null;
}
if (queueConn != null) {
queueConn.stop();
queueConn.close();
queueConn = null;
}

3.4 接收消息

消息接收方的处理逻辑也和发送方的相似。消息由JMS QueueReceiver对象接收,QueueReceiver对象建立在为特定队列创建的QueueSession对象的基础上。差别在于 QueueReceiver接收消息的方式--QueueReceiver对象能够按照伪同步或异步方式接收消息。下面的代码显示了如何用伪同步方式接收消息。

// 伪同步方式接收消息
import javax.jms.Message;
import javax.jms.TextMessage;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueConnection;
import javax.jms.QueueSender;
import javax.jms.Queue;
import javax.jms.Exception;
import javax.naming.InitialContext;
public class MyJMSReceiver {
private String responseMessage;
private String messageID;
private int replyRetCode = 1;
private QueueConnectionFactory queueConnectionFactory = null;
private Queue inputQueue = null;
private QueueConnection queueConnection = null;
private QueueSession queueSession = null;
private QueueReceiver queueReceiver = null;
private TextMessage textMsg = null;
public void processIncomingMessages() {
// 查找管理对象
InitialContext initContext = new InitialContext();
Context env = (Context) initContext.lookup("java:comp/env");
queueConnectionFactory = (QueueConnectionFactory)
env.lookup("tlQCF");
inputQueue = (Queue) env.lookup("tlQueue");
queueConnection = queueConnectionFactory.
createQueueConnection();
queueConnection.start();
queueSession=queueConnection.createQueueSession(true, 0);
queueReceiver = queueSession.createReceiver(inputQueue);
// 等一秒钟,看看是否有消息到达
TextMessage inputMessage = queueReceiver.receive(1000);
// 其他处理代码...
queueConnection.stop();
queueConnection.close();
}
}

下面分析一下上面的代码。消息由QueueReceiver对象执行receive方法接收。receive方法有一个参数,它指定了接收消息的等待时间(以毫秒计)。在上面的代码中,QueueReceiver对象在一秒之后超出期限,解除锁定,把控制返回给程序。如果调用receive方法时指定了等待时间,QueueReceiver对象在指定的时间内被锁定并等待消息,如果超出了等待时间仍无消息到达,QueueReceiver对象超时并解除锁定,把控制返回给程序。

接收消息的方法还有一个"不等待"的版本,使用这个方法时QueueReceiver对象检查是否有消息之后立即返回,将控制交还给程序。下面是一个例子:

TextMessage message = queueReceiver.receiveNoWait();

如果调用receive方法时不指定参数,QueueReceiver对象会无限期地等待消息。采用这种用法时应当非常小心,因为程序会被无限期地锁定。下面是一个无限期等待消息的例子:

TextMessage message = queueReceiver.receive();

不管等待期限的参数设置了多少,这都属于拉式消息传递,如前所述,这种消息传递方式的效率较低。不仅如此,这种方法还不适合于J2EE EJB层,不能用于EJB组件之内,原因稍后就会说明。不过,这种处理方式适合在Servlet、JSP和普通Java JMS应用中使用。

接收消息的第二种办法是异步接收。用异步接收方式时,QueueReceiver对象必须用 setMessageListener(class_name)方法注册一个MessageListener类,其中class_name参数可以是任何实现了onMessage接口方法的类。在下面这个例子中,onMessage方法由同一个类实现(为简单计,这里没有给出try/catch块的代码)。

● 注意:接下来的消息接收方式不适用于EJB组件,这些代码仅适用于Servlet、JSP和普通的Java JMS应用程序。

// 消息监听类的例子
import javax.jms.Message;
import javax.jms.TextMessage;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.Queue;
import javax.jms.Exception;
import javax.naming.InitialContext;
public class MyListenerClass implements javax.jms.MessageListener {
private int responseRetCode = 1;
private boolean loopFlag = true;
private QueueConnectionFactory queueConnectionFactory = null;
private Queue responseQueue = null;
private QueueConnection queueConnection = null;
private QueueSession queueSession = null;
private QueueSender queueSender = null;
public void prepareEnvironment(String myMessage) {
// 查找管理对象
InitialContext initContext = new InitialContext();
Context env = (Context) initContext.lookup("java:comp/env");
queueConnectionFactory = (QueueConnectionFactory)
env.lookup("tlQCF");
responseQueue = (Queue) env.lookup("tlResQueue");
queueConnection = queueConnectionFactory.
createQueueConnection();
queueSession = queueConnection.createQueueSession
(true, 0);
queueReceiver = queueSession.createReceiver
(responseQueue);
queueReceiver.setMessageListener(this)
queueConnection.start();
}

public void onMessage(Message message) {
// 希望收到一个文本消息...
if (message instanceof TextMessage) {
String responseText =
"确认消息传递:" + ((TextMessage) message).getText();
// 当收到一个以@字符开头的消息时,循环结束,
// MessageListener终止
if (responseText.charAt(0) == '@') {
loopFlag = 1; // 结束处理;
} else {t
// 继续处理消息
// 本例中我们知道应答消息的队列,并非真的要用到
//message.getJMSReplyTo
// 这只是一个如何获取应答消息队列的例子
Destination replyToQueue=message.getJMSReplyTo();
// 设置应答消息
TextMessage responseMessage =
responseSession.createTextMessage(responseText);
// 使CorrelationID等于消息ID,
//这样客户程序就能将应答消息和原来的请求
// 消息对应起来
messageID = message.getJMSMessageID();
responseMessage.setJMSCorrelationID(messageID);
// 设置消息的目的地
responseMessage.setJMSDestination(
replyToQueue)
queueSender.send(
responseMessage);
}
}
}
// 保持监听器活动
while (loopFlag) {
// 将控制转移到其他任务(休眠2秒)
System.out.println("正处于监听器循环之内...");
Thread.currentThread().sleep(2000);
}
// 当loopFlag域设置成flase时,结束处理过程
queueConn.stop();
queueConnection.close();
}

注册一个MessageListener对象时,一个实现了MessageListener逻辑的新线程被创建。我们要保持这个线程处于活动状态,因此使用了一个while循环,首先让线程休眠一定的时间(这里是2秒),将处理器的控制权转让给其他任务。当线程被唤醒时,它检查队列,然后再次进入休眠状态。当一个消息到达当前注册的MessageListener对象所监视的队列时,JMS调用MessageListener对象的 onMessage(message)方法,将消息作为参数传递给onMessage方法。

这是一种推式的消息接收方式,程序效率较高,但仍不适合在EJB组件之内使用。下面将探讨为什么这些接收消息的方式都不能用于EJB组件之内,然后给出解决办法。虽然这部分内容放入了P2P通信方式中讨论,但其基本思路同样适用于Pub/Sub通信方式。

3.5 消息驱动的Bean

在前文讨论JMS消息接收处理逻辑的过程中,我们看到的代码仅仅适用于Servlet、JSP以及普通的Java应用程序,但不适用于EJB,因为在JMS的接收端使用EJB存在一些技术问题。一般地,JMS程序的交互模式分两种:

■ 发送-遗忘:JMS客户程序发出消息,但不需要应答。从性能的角度来看,这是最理想的处理模式,发送程序不需要等待对方响应请求,可以继续执行自己的任务。
■ 同步请求-答复:JMS客户程序发出消息并等待答复。在JMS中,这种交互方式通过执行一个伪同步的接收方法(前文已经讨论)实现。然而,这里可能出现问题。如果EJB模块在一个事务上下文之内执行(通常总是如此),单一事务之内无法执行请求-答复处理,这是因为发送者发出一个消息之后,只有当发送者提交了事务,接收者才能收到消息。因此,在单一事务之内是不可能得到应答的,因为在一个未提交的事务上下文之内接收程序永远收不到消息,当然也不可能作出应答了。解决的办法是请求-答复必须通过两个不同的事务执行。

对于EJB来说,在JMS通信的接收端还可能出现另一个EJB特有的问题。在异步通信中,何时能够获得应答是不可预料的,异步通信的主要优点之一就是发送方能够在发出消息之后继续当前的工作,即使接收方当时根本不能接收消息也一样,但请求-答复模式却隐含地假定了EJB组件(假定是一个会话Bean)应该在发出消息之后等待答复。J2EE实际上是一个基于组件技术的事务处理环境,它的设计目标是处理大量短期生存的任务,却不是针对那些可能会被长期阻塞来等待应答的任务。

为了解决这个问题,Sun在 EJB 2.0规范中加入了一种新的EJB类型,即MDB。MDB是专门为JMS异步消息环境中(接收端)EJB组件面临的问题而设计的,其设计思路是把监听消息是否到达的任务从EJB组件移到容器,由于这个原因,MDB组件总是在容器的控制之下运行,容器代替MDB监听着特定的队列或主题,当消息到达该队列或者主题,容器就激活MDB,调用MDB的onMessage方法,将收到的消息作为参数传递给onMessage方法。

MDB是一种异步组件,它的工作方式和其他同步工作的EJB组件(会话Bean,实体Bean)不同,MDB没有Remote接口和Home接口,客户程序不能直接激活MDB,MDB只能通过收到的消息激活。MDB的另一个重要特点是它对事务和安全上下文的处理方式与众不同,MDB已经彻底脱离了客户程序,因此也不使用客户程序的事务和安全上下文。

发送JMS消息的远程客户程序完全有可能运行在不同的环境之中--非J2EE的环境,例如普通的Java应用程序,可能根本没有任何安全或事务上下文。因此,发送方的事务和安全上下文永远不会延续到接收方的MDB组件。由于MDB永远不会由客户程序直接激活,所以它们也永远不可能在客户程序的事务上下文之内运行。由于这个原因,下面这些事务属性对于MDB来说没有任何意义--Supports,RequiresNew,Mandatory,以及 None,因为这些事务属性隐含地意味着延用客户程序的事务上下文。MDB可以使用的事务属性只有两个:NotSupported和Required。如果一个MDB组件有NotSupported事务属性,则它的消息处理过程不属于任何事务上下文。

就象其他类型的EJB一样,MDB可能参与到两种类型的事务:Bean管理的事务,容器管理的事务。MDB组件的所有方法中,只有onMessage方法可以参与到事务上下文。如果让MDB参与到Bean管理的事务上下文,则表示允许MDB在onMessage方法之内开始和结束一个事务。这种策略的问题在于,收到的消息总是位于onMessage方法之内开始的事务之外(要让消息参与其中已经太迟了)。在这种情况下,如果由于某种原因必须回退,则消息必须手工处理。

如果让MDB参与到容器管理的事务上下文,情况就完全不同了。只要设置了Required事务属性,容器就可以在收到消息时开始一个事务,这样,消息就成为事务的一部分,当事务成功提交时,或者当事务被回退而消息被返回到发送队列时,都可以获得明确的确认信息。

事务可能在两种情形下回退:第一种情形是程序显式地调用setRollBackOnly方法,第二种情形是在onMessage方法之内抛出一个系统异常(注意,抛出应用程序异常不会触发回退动作)。当事务回退时,消息就被返回到原来的队列,监听器将再次发送消息要求处理。一般地,这种情况会引起无限循环,最后导致应用运行不正常。Max_retries属性可以用来控制监听器再次提取重发消息的次数(这个属性在配置监听器端口的时候设置),超过Max_retries规定的限制值之后,监听器将停止处理该消息(显然,这算不上最理想的处理方式)。

如果用WebSphere MQ作为JMS提供者,还有一种更好的解决办法。我们可以配置WebSphere MQ,使其尝试一定的次数之后停止发送同一消息,并将消息放入一个特定的错误队列或Dead.Letter.Queue。记住,MDB是无状态的组件,也就是说它不会在两次不同的方法调用之间维持任何状态信息(就象Web服务器处理HTTP请求的情况一样),同时,由于客户程序永远不会直接调用MDB组件,所以MDB组件不能识别任何特定的客户程序,在这个意义上,可以认为MDB组件是"匿名"运行的。所有MDB组件都必须实现javax.ejb.MessageDrivenBean接口和javax.jms.MessageListener接口。

除了onMessage方法之外,MDB还有其他几个回调方法--由容器调用的方法:

■ ejbCreate方法:容器调用该方法来创建MDB的实例。在ejbCreate方法中可以放入一些初始化的逻辑。

■ setMessageDrivenContext方法:当Bean第一次被加入到MDB Bean的缓冲池时容器将调用该方法。setMessageDrivenContext方法通常用来捕获MessageDrivenContext,并将 setMessageDrivenContext保存到类里面的变量,例如:

public void setMessageDrivenContext(java.ejb.
MessageDrivenContext mdbContext) {
messageDrivenContext = mdbContext;
}

■ ejbRemove方法:容器把Bean从缓冲池移出并拆除时会调用该方法。一般地,ejbRemove方法会执行一些清理操作。

一般而言,在onMessage方法之内执行业务逻辑是不推荐的,业务方面的操作最好委派给其他EJB组件执行。

MDB容器会自动控制好并发处理多个消息的情形。每一个MDB实例处理一个消息,在onMessage方法把控制返回给容器之前,不会要求MDB同时处理另一个消息。如果有多个消息必须并行处理,容器会激活同一MDB的多个实例。

从WebSphere 5.0开始,开发环境(WSAD 5.0)和运行环境(WAS 5.0)都开始全面支持MDB。

下面的代码片断显示了一个MDB的概念框架。

// MDB概念框架
// package 声明略
import javax.jms.Message;
import javax.jms.MapMessage;
import javax.naming.InitialContext;
import java.util.*;
public class LibraryNotificationBean
implements javax.ejb.MessageDrivenBean,
javax.jms.MessageListener {
MessageDrivenContext messageDrivenContext;
Context jndiContext;
public void setMessageDrivenContext(MessageDrivenContext
msgDrivenContext) {
messageDrivenContext = msgDrivenContext;
try {
jndiContext = new InitialContext();
} catch (NamingException ne) {
throw new EJBException(ne);
}
}

public void ejbCreate() {
}
public void onMessage(Message notifyMsg) {
try {
MapMessage notifyMessage = (MapMessage) notifyMsg;
String bookTitle = (String) notifyMessage.
getString("BookTitle");
String bookAuthor = (String) notifyMessage.
getString("BookAuthor");
String bookCatalogNumber =
(String) notifyMessage.getString
("bookCatalogNumber");
Integer bookQuantity =
(Integer) notifyMessage.getInteger("BookQuantity");
// 处理消息...(调用其他EJB组件)
} catch (Exception e) {
throw new EJBException(e);
}
}
public void ejbRemove() {
try {
jndiContext.close();
jndiContext = null;
} catch (NamingException ne) {
// 异常处理
}
}
}

3.6 消息持久化

消息可以是持久性的,也可以是非持久性的,持久性的消息和非持久性的消息可以放入同一个队列。持久性的消息会被写入磁盘,即使系统出现故障也可以恢复。当然,正象其他场合的持久化操作一样,消息的持久化也会增加一定的开销,持久性消息大约要慢7%。控制消息持久化的办法之一是定义队列时设置其属性,如果没有显式地设置持久化属性,系统将采用默认值。另外,JMS应用程序本身也可以定义持久化属性:

■ PERSISTENCE(QDEF):继承队列的默认持久化属性值。

■ PERSISTENCE(PERS):持久性的消息。

■ PERSISTENCE(NON):非持久性的消息。

消息的持久性还可以通过消息属性头JMSDeliveryMode来调节,JMSDeliveryMode可以取值DeliveryMode.PERSISTENT或DeliveryMode.NON_PERSISTENT,前者表示消息必须持久化,后者表示消息不需持久化。在事务化会话中处理的消息总是持久性的。

3.7 消息选择器

JMS提供了从队列选取一个消息子集的机制,能够过滤掉所有不满足选择条件的消息。选择消息的条件不仅可以引用消息头的域,还可以引用消息属性的域。下面是如何利用这一功能的例子:

QueueReceiver queueReceiver =
queueSession.createReceiver(requestQueue,
"BookTitle = 'Windows 2000'");
QueueBrowser queueBrowser = queueSession.createBrowser
(requestQueue, "BookTitle = 'Windows 2000'
AND BookAuthor = 'Robert Lee'");

注意,字符串(例如'Windows 2000')被一个双引号之内的单引号对包围。

四、JMS Pub/Sub编程

Pub/Sub通信方式的编程与P2P通信编程相似,两者最主要的差别是消息发送的目的地对象。在Pub/Sub通信方式中,发布消息的目的地和消费消息的消息源是一个称为主题(Topic)的JMS对象。主题对象的作用就象是一个虚拟通道,其中封装了一个Pub/Sub的目的地(Destination)对象。

在P2P通信方式中,(发送到队列的)消息只能由一个消息消费者接收;但在Pub/Sub通信方式中,消息生产者发布到主题的消息可以分发到多个消息消费者,而且,消息的生产者和消费者之间的结合是如此宽松,以至于生产者根本不必了解任何有关消息消费者的情况,消息生产者和消费者都只要知道一个公共的目的地(也就是双方"交谈"的主题)。

由于这个原因,消息的生产者通常称为出版者,消息的消费者则相应地称为订阅者。出版者为某一主题发布的所有消息都会传递到该主题的所有订阅者,订阅者将收到它订阅的主题上的所有消息,每一个订阅者都会收到一个消息的副本。订阅可以是耐久性的(Durable)或非耐久性(Nondurable)。非耐久性的订阅者只能收到订阅之后才发布的消息。

耐久性订阅者则不同,它可以中断之后再连接,仍能收到在它断线期间发布的消息。Put/Sub通信方式中耐久性连接(在某种程度上)类似于P2P通信中的持久性消息/队列。出版者和订阅者永远不会直接通信,Pub/Sub代理的作用就象是一个信息发布中心,把所有消息发布给它们的订阅者。

● 注意:从WebSphere MQ 5.2开始,加装了MA88和MA0C扩展的WebSphere MQ可以当作JMS Pub/Sub通信代理。此外,WebSphere MQ Integrator也能够作为一个代理用。从MQ 5.3开始,MA88成了MQ基本软件包的一部分,因此只要在MQ 5.3的基础上安装MA0C就可以了。在MQ JMS环境中,要让Pub/Sub通信顺利运行,运行Pub/Sub代理的队列管理器上必须创建一组系统队列。

MQ JMS MA0C扩展模块提供了一个工具来构造所有必需的Pub/Sub系统队列,这个工具就是MQJMS_PSQ.mqsc,位于\java\bin目录之下。要构造Pub/Sub通信方式所需的系统队列,只需从上述目录执行命令:runmqsc < MQJMS_PSQ.mqsc。

多个主题可以组织成一种树形的层次结构,树形结构中主题的名称之间用一个斜杠(/)分隔--例如,Books/UnixBooks /SolarisBooks。如果要订阅一个以上的主题,可以在指定主题名字的时候使用通配符,例如,"Books/#"就是一个使用通配符的例子。

下面给出了一个JMS Pub/Sub通信的例子(为简单计,这里省略了try/catch代码块)。在这个例子中,Books/ UnixBooks/SolarisBooks主题的订阅者将收到所有发布到SolarisBooks的消息,Books/#主题的订阅者将收到所有有关 Books的消息(包括发布到UnixBooks和SolarisBooks主题的消息)。

// JMS Pub/Sub通信
import javsx.jms.*;
import javax.naming.*;
import javax.ejb.*;
public class PublisherExample implements javax.ejb.SessionBean {
private TopicConnectionFactory topicConnFactory = null;
private TopicConnection topicConnection = null;
private TopicPublisher topicPublisher = null;
private TopicSession topicSession = null;
private SessionContext sessionContext = null;
public void setSessionContext(SessionContext ctx) {
sessionContext = cts;
}
public void ejbCreate() throws CreateException {
InitialContext initContext = new InitialContext();
// 从JNDI查找主题的连接工厂
topicConnFactory =(TopicConnectionFactory)
initContext.lookup("java:comp/env/TCF");
// 从JNDI查找主题
Topic unixBooksTopic =
(Topic) initContext.lookup("java:comp/env/UnixBooks");
Topic javaBooksTopic =
(Topic) initContext.lookup("java:comp/env/JavaBooks");
Topic linuxBooksTopic =
(Topic) initContext.lookup("java:comp/env/LinuxBooks");
Topic windowsBooksTopic =
(Topic) initContext.lookup("java:comp/env/WindowsBooks");
Topic allBooksTopic =
(Topic) initContext.lookup("java:comp/env/AllBooks");
// 创建连接
topicConnection = topicConnFactory.createTopicConnection();
topicConn.start();
// 创建会话
topicSession =
topicConn.createTopicSession(false,
Session.AUTO_ACKNOWLEDGE);
}
public void publishMessage(String workMessage,
String topicToPublish) {
// 创建一个消息
TextMessage message = topicSession.createTextMessage
(workMessage); // 创建出版者,发布消息
if ((topicToPublish.toLowerCase()).equals("java")) {
TopicPublisher javaBooksPublisher =
topicSession.createPublisher(javaBooksTopic);
javaBooksPublisher.publish(message);
}
if ((topicToPublish.toLowerCase()).equals("unix")) {
TopicPublisher unixBooksPublisher =
topicSession.createPublisher(unixBooksTopic);
J2EE Enterprise Messaging 475 unixBooksPublisher.
publish(message);
}
if ((topicToPublish.toLowerCase()).equals("linux")) {
TopicPublisher linuxBooksPublisher =
topicSession.createPublisher(linuxBooksTopic);
linuxBooksPublisher.publish(message);
}
if ((topicToPublish.toLowerCase()).equals("windows")) {
TopicPublisher windowsBooksPublisher =
topicSession.createPublisher(windowsBooksTopic);
windowsBooksPublisher.publish(message);
}
TopicPublisher allBooksPublisher =
topicSession.createPublisher(allBooksTopic);
allBooksPublisher.publish(message);
}
public void ejbActivate() {
}
public void ejbPassivate() {
}
public void ejbRemove() {
// 清理工作...
if (javaBooksPublisher != null) {
javaBooksPublisher.close();
javaBooksPublisher = null;
}
if (unixBooksPublisher != null) {
unixBooksPublisher.close();
Chapter 9 476 unixBooksPublisher = null;
}
if (linuxBooksPublisher != null) {
linuxBooksPublisher.close();
linuxBooksPublisher = null;
}
if (windowsBooksPublisher != null) {
windowsBooksPublisher.close();
windowsBooksPublisher = null;
}
if (allBooksPublisher != null) {
allBooksPublisher.close();
allBooksPublisher = null;
}
if (topicSession != null) {
topicSession.close();
topicSession = null;
}
if (topicConnection != null) {
topicConnection.stop();
topicConnection.close();
topicConnection = null;
}
}

这段代码比较简单,想来不需要多余的解释了。唯一值得一提的地方是如何将一个消息发布到不同的主题:对于每一个特定的主题,分别创建一个对应的出版者,然后用它将消息发布到主题。

如果一个MDB组件只负责接收消息,把所有其他的消息处理操作都委托给专用业务组件(这意味着MDB之内不包含消息发送或发布逻辑),MDB的代码就相当于P2P通信方式中的处理代码,使用MDB唯一的改变之处是将监听端口从监听一个队列改为监听一个主题。有兴趣的读者可以自己试验一下双重监听的实现方式。

五、二阶段提交的事务

在企业级处理任务中,为了保证JMS或非JMS代码处理业务逻辑过程的完整性(对于一个单元的工作,要么成功提交所有的处理步骤,要么全部回退所有处理步骤),操作一般要在一个事务上下文之内进行。除了将消息放入队列的过程之外,如果还要向数据库插入记录(二阶段提交,要么全部成功,要么全部失败),事务上下文的重要性尤其突出。

为了支持二阶段提交,JMS 规范定义了下列XA版的JMS对象:XAConnectionFactory、XAQueueConnectionFactory、XASession、 XAQueueSession、XATopicConnectionFactory、XATopicConnection,以及 XATopicSession。另外,凡是参与全局事务的所有资源均应该使用其XA版。特别地,对于JDBC资源,必须使用JDBC XADataSource。最后一点是,全局事务由JTA TransactionManager控制。下面的代码显示了建立全局事务所需的步骤。

// 配置全局事务

// 从JNDI名称空间获取一个JTA TransactionManager
TransactionManager globalTxnManager =
jndiContext.lookup("java:comp/env/txt/txnmgr");
// 启动全局事务
globalTxnManager.begin();
// 获取事务对象
Transaction globalTxn = globalTxnManager.getTransaction();
// 获取XA数据资源
XADatasource xaDatasource = jndiContext.lookup
("java:comp/env/jdbc/datasource");

// 获取一个连接
XAConnection jdbcXAConn = xaDatasource.getConnection();
// 从XA连接获取XAResource
XAResource jdbcXAResource = jdbcXAConn.getXAResource();
// 在全局事务中"征募"XAResource
globalTxn.enlist(jdbcXAResource);
// 获取XA队列连接
XAQueueConnectionFactory xaQueueConnectionFactory =
JndiContext.lookup("java:comp/env/jms/xaQCF")
XAQueueConnection xaQueueConnection =
XaQueueConnectionFactory.createXAQueueConnection();
// 获取XA队列会话
XAQueueSession xaQueueSession = xaQueueConnection.
createXAQueueSession();
// 从会话获取XA资源
XAResource jmsXAResource = xaQueueSession.getXAResource();
// 在全局事务中"征募"该XAResource
globalTxn.enlist(jmsXAResource);
// 其他处理工作...
// 提交全局事务
globalTxn.commit();

总结:本文介绍了JMS及其在WSAD环境下的编程,探讨了JMS异步通信的主要优点,以及两种通信方式(P2P和Pub/Sub)、MDB、JMS事务、二阶段提交全局事务等方面的概念。希望本文的介绍对你有所帮助。

Continue reading WSAD环境下JMS异步通信全攻略

Pagination


Total views.

© 2013 - 2019. All rights reserved.

Powered by Hydejack v6.6.1