pringframework 2.0 與 ZK 混合開發實例
<script type="text/javascript">function StorePage(){d=document;t=d.selection?(d.selection.type!='None'?d.selection.createRange().text:''):(d.getSelection?d.getSelection():'');void(keyit=window.open('http://www.365key.com/storeit.aspx?t='+escape(d.title)+'&u='+escape(d.location.href)+'&c='+escape(t),'keyit','scrollbars=no,width=475,height=575,left=75,top=20,status=no,resizable=yes'));keyit.focus();}</script> Springframework 2.0 與 ZK 混合開發實例
什麼是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>