什么是ZK
利用ZK框架设计的web应用程序具备丰富的胖客户端特性和简单的设计模型。ZK包括一个基于AJAX可自动进行交互式操作的事件驱动引擎和一 套兼容XUL的组件。利用直观的事件驱动模型,你可以用具有XUL特性的组件来表示你的应用程序并通过由用户触发的监听事件来操作这些组件,就像开发桌面 应用程序一样简单。
先来点直观的感受:http://www.zkoss.org/zkdemo/userguide/
什么是Springframework 2.0
大名鼎鼎的Springframework相信没有人不知道吧,就在不久前,Interface21又推出了Spring 2.0版本,Spring2.0的发布恐怕算得上2006年Java社区的一件大事了。Spring2.0中一些新的特性,如:基于XML Schema语法的配置、JPA的支持、支持动态语言、异步JMS等诸多功能都非常不错,总体来说,Spring2.0将向未来的宏大目标又迈进了一大 步。
动机
因为工作需要,要写一个基于Web的JMS消息发送程序,当然,这对于技术人员来说,是小菜一碟,现实问题摆在面前,一是时间紧,二是由于客户 对技术方面一般,所以GUI的美观程度至关重要,怎么办呢?思前想后,决定使用Ajax技术,但我们也知道,如今Ajax的框架多如牛毛,我该选择哪一个 呢,无意中,通过Google找到了ZK。在看完她的在线演示后,被她华丽的外观,简洁实现方式所吸引。于是,就这样,我一边看着ZK技术手册,一边上路 了。
第一次重构 — Spring登场
ZK作为表现层技术,一般通过两种手段与业务层交互,一种方式是只使用ZK做表现层,当页面提交后,再由用户指定的servlet来处理具体的业务逻辑,另一种方式是通过象.NET的WebForm一样基于事件响应方式编程。例如:
<window title="event listener demo" border="normal" width="200px"> <label id="mylabel" value="Hello, World!"/> <button label="Change label"> <attribute name="onClick"> mylabel.value = "Hello, Event!" </attribute> </button> </window>
很明显,使用第二种方式会更加简单,更加容易理解,但问题也随之产生,因为每个事件处理都要使用大量类信息,随着业务逻辑的复杂性增加,每个 ZUL(ZK页面)也会变得相当的臃肿,怎么办呢?当然是使用Spring!!原因有二:一是可以将大量业务逻辑代码封装成bean,减少表现层的复杂 性,另一个好处是,由于业务场景需要处理JMS要关内容,通过Spring2.0对JMS强大的支持功能,也可以大大减少工作量。说干就干,通过研究尝 试,我发现在ZK中可以通过如下方式访问Spring的上下文:
…..
<zkscript>{
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
WebApplicationContext ctx = (WebApplicationContext)Executions.getCurrent().getDesktop().getWebApp().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
</zkscript>
…
这样就如鱼得水了,我可以任意使用用Spring管理的Bean了。
第二次重构 — Spring JMS发送
我们知道在Spring中处理JMS的发送一般来讲是通过配置的方式得到JmsTemplate,然后当要发送消息时,我们再创建一个匿名类,如下:
…. this.jmsTemplate.send(this.queue, new MessageCreator() { public Message createMessage(Session session) throws JMSException { return session.createTextMessage("hello queue world"); } }); …
通过分析,很显然,使用匿名类的原因就在于,只有在消息发送这一时刻才能决定发送什么类型的消息以及消息内容是什么,知道了这一点,其实我们可以写一个工具Bean类,来封装这个逻辑,来避免这个繁琐的过程,代码如下:
MessageCreatorBean.java
package bean; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.Map.Entry; import javax.jms.BytesMessage; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.Session; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.jms.core.MessageCreator; import org.zkoss.util.media.Media; public class MessageCreatorBean implements MessageCreator { private Media media; private Map properties; private String text; public void setText(String str) { text = str; } public String getText() { return text; } public void setMedia(Media m) { media = m; } public Media getMedia() { return media; } public void setProperties(Map map) { properties = map; } public Map getProperties() { return properties; } private createBinaryMessage(Session session ) throws JMSException { BytesMessage msg = null; byte[] bytes = null; try { bytes = media.getByteData(); } catch ( IllegalStateException ise ) { try { InputStream is = media.getStreamData(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[ 1000 ]; int byteread = 0; while ( ( byteread=is.read(buf) )!=-1) { baos.write(buf,0,byteread); } bytes = baos.toByteArray(); } catch ( IOException io ) { } } msg = session.createBytesMessage(); msg.writeBytes(bytes); properties.put("m_name",media.getName()); properties.put("m_format", media.getFormat()); properties.put("m_ctype", media.getContentType()); return msg; } private Message createTextMessage(Session session) throws JMSException { Message msg = session.createTextMessage(text); properties.put("m_name", (new Date()).getTime() + ".xml"); properties.put("m_format", "xml"); properties.put("m_ctype", "text/xml"); return msg; } public Message createMessage(Session session) throws JMSException { Message msg = null; if (properties==null) properties = new Properties(); if ( media == null ) { msg = createTextMessage(session); } else { msg = createBinaryMessage(session); } applyProperties(msg); return msg; } public void mergeProperties(Properties props) { if ( properties == null ) { properties = new Properties(); } if ( props != null ) { Set keys = props.keySet(); for ( Iterator it = keys.iterator(); it.hasNext(); ) { String key = (String)it.next(); properties.put(key, props.get(key)); } } } private void applyProperties(Message msg) throws JMSException { if (properties != null) { for (Object s : properties.keySet()) { msg.setStringProperty((String) s, (String) properties.get(s)); } } } }
配置Springframework Context:
<bean id="normalMessageCreator" class="com.bea.de.bean. MessageCreatorBean "/>
使用的时候我们就可以通过Spring来访问了。
… void send(Media media, Properties props) { WebApplicationContext ctx = (WebApplicationContext)Executions.getCurrent().getDesktop().getWebApp().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); JmsTemplate jt = (JmsTemplate)ctx.getBean("jmsTemplate"); Queue queue = (Queue)ctx.getBean("binaryQueue"); MessageCreatorBean mc = (MessageCreatorBean)ctx.getBean("binaryMessageCreator" ); Properties p = (Properties)ctx.getBean("messageProperties"); mc.mergeProperties(p); mc.mergeProperties(props); mc.setMedia(media); jt.send(queue,mc); } …
第三次重构—BSH(BeanShell)登场
虽然Spring与JMS发送问题解决了,但是还有一个潜在的问题,就是如果发送的消息类型或逻辑攺变了,我们不得不重写 MessageCreatorBean这个类,当然,这就引起了重编译部署的问题,怎么能不编译就可以攺变业务逻辑呢?我想到了Spring 2.0的新特性,对脚本语言的支持,Spring 2.0现在支持三种脚本语言:BSH、JRuby、JGroovy。这三种脚本语言使用起来大同小异,我选择了语法更贴近Java的BSH。过程如下:
package com.bea.de.scripting; import java.util.Map; ….. public interface MessageCreatorBean extends MessageCreator { public void setMedia(Media msg); public Media getMedia(); public void setProperties(Map map); public Map getProperties(); public Message createMessage(Session session) throws JMSException; public void mergeProperties(Properties props); }
文件名MessageCreatorBean.bsh
Media media; Map properties; String text; public void setText(String str) { text = str; } public String getText() { return text; } public void setMedia(Media m) { media = m; } public Media getMedia() { return media; } public void setProperties(Map map) { properties = map; } public Map getProperties() { return properties; } private createBinaryMessage(Session session ) throws JMSException { BytesMessage msg = null; byte[] bytes = null; try { bytes = media.getByteData(); } catch ( IllegalStateException ise ) { try { InputStream is = media.getStreamData(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[ 1000 ]; int byteread = 0; while ( ( byteread=is.read(buf) )!=-1) { baos.write(buf,0,byteread); } bytes = baos.toByteArray(); } catch ( IOException io ) { } } msg = session.createBytesMessage(); msg.writeBytes(bytes); properties.put("m_name",media.getName()); properties.put("m_format", media.getFormat()); properties.put("m_ctype", media.getContentType()); return msg; } private Message createTextMessage(Session session) throws JMSException { Message msg = session.createTextMessage(text); properties.put("m_name", (new Date()).getTime() + ".xml"); properties.put("m_format", "xml"); properties.put("m_ctype", "text/xml"); return msg; } public Message createMessage(Session session) throws JMSException { Message msg = null; if (properties==null) properties = new Properties(); if ( media == null ) { msg = createTextMessage(session); } else { msg = createBinaryMessage(session); } applyProperties(msg); return msg; } public void mergeProperties(Properties props) { if ( properties == null ) { properties = new Properties(); } if ( props != null ) { Set keys = props.keySet(); for ( Iterator it = keys.iterator(); it.hasNext(); ) { String key = (String)it.next(); properties.put(key, props.get(key)); } } } private void applyProperties(Message msg) throws JMSException { if (properties != null) { for (Object s : properties.keySet()) { msg.setStringProperty((String) s, (String) properties.get(s)); } } }
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:lang="http://www.springframework.org/schema/lang" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd"> …. <lang:bsh id="normalMessageCreator" script-source="classpath:bsh/MessageCreatorBean.bsh" script-interfaces="com.bea.de.scripting.MessageCreatorBean" refresh-check-delay="5000"/>
…..
解释:
script-source:具体业务逻辑的的脚本实现文件,当系统上线后,如果我们想修攺业务逻辑,只需修攺这个脚本就可以了,无需重编译类文件。
script-interface:业务接口,这个接口文件一定要在前期定好,不然如果要对接口修攺,就要重编译了。如果使用JGroovy就无需这个参数了。
refresh-check-delay:引擎每隔多长时间检查脚本状态,如果脚本被攺动就会自动编译。
第四次重构—Spring JMS接收
以往我们在实现JMS消息的接收时,往往是通过(MDB-消息EJB)或启用一个后台进程,等待JMS消息进行处理,代码量和复杂度都非常高, 因此,我想到了Spring对JMS Container的支持。也就是说,由Spring监控消息以及维护消息处理Bean。实现如下:
…. <lang:bsh id="normalMessageListener" script-source="classpath:bsh/DiskMessageListenerBean.bsh" script-interfaces="com.bea.de.scripting.DiskMessageListenerBean" refresh-check-delay="5000"> <lang:property name="basePath" value="${jms.listener.disk.normal}" /> </lang:bsh> <bean id="normalMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="concurrentConsumers" value="5" /> <property name="connectionFactory" ref="connectionFactory" /> <property name="destination" ref="receiveNormalQueue" /> <property name="messageListener" ref="normalMessageListener" /> </bean>
normalMessageListener:是一个实现了javax.jms. MessageListener接口的Bean,用来处理消息处理逻辑,我们可以看到,为了维护的方便,此处,我还是使用了BSH。
normalMessageListenerContainer:是一个用来维护消息处理Bean的容器。
第五次重构—Spring JMS消息Pooling机制
经过一系列的大手术,基本上完成了客户所需要的功能,但这时客户有了新的想法:客户的外部系统定期生成数据(以文件方式写入文件目录 ),然后,由信息平台将数据传出。当时第一想法就是使用Quartz,虽然Quartz功能强大,但总觉得其非出生名门,所以最终采用了JDK Timer支持,结合Spring的强大功能,实现了此功能。
代码如下:
<lang:bsh id="poolingMessageExecutor" script-source="classpath:bsh/PoolingMessageTimerTaskBean.bsh" script-interfaces="com.bea.de.scripting.MessageTaskExecutor" refresh-check-delay="5000"> <lang:property name="jmsTemplate" ref="jmsTemplate"/> <lang:property name="messageProperties" ref="messageProperties"/> <lang:property name="targetQueue" ref="binaryQueue"/> <lang:property name="basePath" value="${jms.pooling.disk}"/> <lang:property name="messageCreator" ref="binaryMessageCreator"/> </lang:bsh> <bean id="poolingMessageTimerTask" class="org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean"> <property name="targetObject" ref="poolingMessageExecutor" /> <property name="targetMethod" value="execute" /> </bean> <bean id="scheduledPoolingMessageTask" class="org.springframework.scheduling.timer.ScheduledTimerTask"> <property name="delay" value="10000" /> <property name="period" value="50000" /> <property name="timerTask" ref="poolingMessageTimerTask" /> </bean> <bean id="scheduler" class="org.springframework.scheduling.timer.TimerFactoryBean"> <property name="scheduledTimerTasks"> <list> <ref bean="scheduledPoolingMessageTask" /> </list> </property> </bean>
解释:
poolingMessageExecutor:是一个纯的POJO对象(这也是我选此方式的一个很大原因),当然,具体的逻辑还是由BSH完成。
poolingMessageTimerTask:此对象用来指明任务执行器的哪个函数进行具体的任务处理。
scheduledPoolingMessageTask:配置任务调度信息,如延时、时间间隔
scheduler:调度触发器
第五次重构—ZK代码精减
至此,全部功能实现完毕,由于Spring与ZK的出色表现,具然提供完成了任务,但回过头来看自己的代码,虽然有了Spring的帮助,但页面中的代码还是显得有些臃肿,因此,决定再次调整。
第一步:
调整的第一步就是把共性功能进行包装,然后将这些封装后的代码做成库的型式。例:
生成库文件:common.zs
… import org.springframework.web.context.WebApplicationContext; import com.bea.de.scripting.DiskMessageListenerBean; void send(Media media, Properties props) { …….. jt.send(queue,mc); } void send(String str, Properties props) { …… jt.send(queue,mc); }
其它ZUL页面调用时只需如下方式:
…. <zscript src="/lib/common.zs"/> ….
第二步:ZK组件化
通过分析,我发现有很多功能类型的页面,例如:由于发送消息的类型不同(二进制、文件等),所以我采用了不同的页面实现消息发送,但实际上有很 多功能是类似的,为什么我们不同将这些功能模块化呢?说干就干,我为消息发送制做了一个发送组件:Sender.zul,此页面与其它页面没有什么不同, 只是它可以接收参数,例如:如果我们想使用调用者传来的desc参数,就使用${arg.desc}。
代码如下:
文件名:Sender.zul
<?xml version="1.0" encoding="UTF-8"?> <vbox> <groupbox mold="3d" width="800px"> <caption label="控制面板"></caption> <window width="100%"> <zscript src="/lib/common.zs"/> <zscript> org.zkoss.util.media.Media media = null; boolean isText = true; void doUpload() { media = Fileupload.get(); if ( media == null ) return; if ( media.getFormat().equals("txt") || media.getFormat().equals("xml")) { String content = new String(media.getByteData()); msgTextbox.value = content; isText = true; } else { isText = false; msgTextbox.value = "上传文件名-->" + media.getName(); } msgTextbox.disabled=true; } void doSend() { String content = msgTextbox.value.trim(); Properties props = new Properties(); if ( msgTypeRadiogroup.selectedItem.value.equals( "P2P" ) ) { if ( hospitalListbox.selectedItem == null ) { Messagebox.show("请选择医院!"); hospitalListbox.focus(); return; } Set sel = hospitalListbox.getSelectedItems(); StringBuffer buf = new StringBuffer(); for ( Listitem item : sel ) { buf.append( item.getValue() ).append("|"); } String tmp = buf.toString(); String hospitals = tmp.substring(0,tmp.length()-1); props.put("MessageFor", "P2P"); props.put("MessageTarget",hospitals); } else { if ( diseaseListbox.selectedItem == null ) { Messagebox.show("请选择疾病类型!"); diseaseListbox.focus(); return; } props.put("MessageFor", "Report"); props.put("MessageTarget",diseaseListbox.selectedItem.value); } if ( content == null || content.equals("") ) { Messagebox.show("请输入消息内容!"); } else { if ( routingTypeRadiogroup.selectedItem.value.equals( "BodyRouting" ) ) { if ( !isText ) { Messagebox.show("不能基于流体文件路由,请选择-消息头路由-方式!!"); msgTextbox.focus(); return; } else { send( content, props ); } } else if ( media != null ) { send( media, props ); } else { media = new org.zkoss.util.media.AMedia(( new Date() ).getTime() + ".xml", "xml", "text/xml", content.getBytes()); send( media, props ); } Messagebox.show("发送成功"); msgTextbox.focus(); } msgTextbox.disabled=false; } void doClear() { msgTextbox.value=""; msgTextbox.disabled=true; media=null; msgTextbox.focus(); } </zscript> <grid> <rows> <row> <label value="文件路径"/> <hbox> <textbox /> <button label="上传文件" onClick="doUpload()"/> </hbox> </row> <row> <label value="路由类型"/> <radiogroup id="routingTypeRadiogroup"> <radio label="消息头路由" value="HeadRouting" checked="true"/> <radio label="消息体路由" value="BodyRouting"/> </radiogroup> </row> <row> <label value="消息内容" /> <textbox id="msgTextbox" cols="80" multiline="true" rows="20" value="${arg.content}"/> </row> <row> <label value="消息类型"/> <radiogroup id="msgTypeRadiogroup"> <radio label="点对点" value="P2P" checked="true" onCheck="p2pRow.visible=true;reportRow.visible=false;"/> <radio label="上报数据" value="Report" onCheck="p2pRow.visible=false;reportRow.visible=true"/> </radiogroup> </row> <row id="p2pRow"> <label value="XX" /> <zscript> ListModel hospitalModel = getListModel("P2P"); </zscript> <listbox checkmark="true" multiple="true" width="200px" id="hospitalListbox" itemRenderer="com.bea.de.ui.MapListItemRender" model="${hospitalModel}"> <listhead> <listheader label="XX名称"/> </listhead> </listbox> </row> <row id="reportRow" visible="false"> <label value="XX类型" /> <bandbox id="bd1"> <bandpopup> <zscript> ListModel diseaseModel = getListModel("Report"); </zscript> <listbox width="200px" id="diseaseListbox" onSelect="bd1.value=self.selectedItem.label; bd1.closeDropdown();" itemRenderer="com.bea.de.ui.MapListItemRender" model="${diseaseModel}"> <listhead> <listheader label="XX类型名称"/> </listhead> </listbox> </bandpopup> </bandbox> </row> <row> <label value="操作"/> <hbox> <button label="发 送" onClick="doSend();"/> <button label="清空" onClick="doClear();"/> </hbox> </row> </rows> </grid> </window> </groupbox> <groupbox mold="3d" open="false" width="800px"> <caption label="功能说明"></caption> <window border="normal" width="100%"> <include src="${arg.desc}"/> <!—这里就是接收参数的地方 --> </window> </groupbox> </vbox>
组件调用方代码如下:
<?xml version="1.0" encoding="UTF-8"?> <?component name="sender" macro-uri="/macros/Sender.zul"?> <window> <sender> <attribute name="content"><![CDATA[ xxxxx ]]></attribute> <attribute name="desc"><![CDATA[/descs/Sender.xhtml]]></attribute> </sender> </window>