一.C/S兩端的任務(wù)分離
考慮到便于信息接收傳遞顯示的因素,交易系統(tǒng)和QQ類似,采用了傳統(tǒng)的C/S模式而不是B/S模式。Client端主要負(fù)責(zé)取得用戶輸入和數(shù)據(jù)顯示,而Server端分為DBServer和MsgServer兩個(gè),前者負(fù)責(zé)數(shù)據(jù)的持久化,后者負(fù)責(zé)消息的傳遞。撇開消息服務(wù)器MsgServer不談的話,數(shù)據(jù)傳遞主要發(fā)生在Client和DBServer之間。
二.C/S兩端的交互方式
由于C端只負(fù)責(zé)數(shù)據(jù)的輸入和顯示,它必然需要向DBServer端存取數(shù)據(jù),這就有一個(gè)信息載體和交互方式的問題。C端需要向DbServer(以下簡稱DS)傳遞的信息是多種多樣的,簡單命令行形式的數(shù)據(jù)肯定不行,類似JSON的線性形式不夠表現(xiàn)樹狀數(shù)據(jù),只有XML才有豐富的表現(xiàn)能力,它無論是簡單的線性數(shù)據(jù)還是復(fù)雜的樹狀數(shù)據(jù)都能容納,有了dom4j或是jdom的幫助,解析起來也很方便。交互方式上,由于C可能在廣域網(wǎng)中,還可能有防火墻的阻擋,這樣Socket長連接就受到一定程度的限制,要是采用WebService問題就解決了,因?yàn)閃ebService的底層協(xié)議還是http,也走80端口,不會(huì)被防火墻阻擋,這樣,DBServer就成了一臺(tái)放置在公網(wǎng)上的WebService服務(wù)器,為各個(gè)Client提供Webservice服務(wù)。
三.實(shí)現(xiàn)WebService的軟件選擇
備選有Axis1/2和XFire兩種方案,選擇的依據(jù)主要是效率。通過一段時(shí)間的使用,發(fā)現(xiàn)XFire的效率確實(shí)比Axis1/2高,估測(cè)同等調(diào)用只占后者的一半左右。其它的易用性,穩(wěn)定性等沒有成為選擇依據(jù),因?yàn)槿绻鸛Fire還不行再換其它的軟件也來得及,下面的設(shè)計(jì)保證了系統(tǒng)不會(huì)依賴于特定的WebService端軟件。
四.WebSevice端的對(duì)外接口設(shè)計(jì)
WebService的對(duì)外端口一般是由一個(gè)接口和一個(gè)實(shí)現(xiàn)類組成,實(shí)現(xiàn)類中的函數(shù)是具體實(shí)現(xiàn),接口是調(diào)用者和實(shí)現(xiàn)者共同遵守的規(guī)約;一般來說如果客戶端需要一個(gè)函數(shù)的話,那么服務(wù)器端的接口類要定義這個(gè)函數(shù),實(shí)現(xiàn)類實(shí)現(xiàn)這個(gè)函數(shù)。這樣的方式在交互簡單,數(shù)據(jù)量小的時(shí)侯沒有問題,且使用很方便,但量變引起質(zhì)變,如果交互復(fù)雜,需要的函數(shù)眾多,數(shù)據(jù)量與日俱增的話,問題就來了。其一,這回導(dǎo)致接口類和實(shí)現(xiàn)類函數(shù)越來越多,體積越來越大,對(duì)定位維護(hù)修改帶來很大的不變;其二,接口類和實(shí)現(xiàn)類常會(huì)被修改,而開發(fā)人員之間的協(xié)同等待甚至沖突就日益增多起來,阻滯了開發(fā)效率;其三,也是最重要的,系統(tǒng)的可擴(kuò)展性缺乏,難以動(dòng)態(tài)維護(hù),即使增加多個(gè)服務(wù)器分擔(dān)負(fù)載,也需要手動(dòng)修改大量的代碼。因此,這種傳統(tǒng)的方式在Demo版過后就被放棄了。
新的方式采用的單接口設(shè)計(jì),即接口類中只定義一個(gè)函數(shù),實(shí)現(xiàn)類實(shí)現(xiàn)這一個(gè)函數(shù),其內(nèi)部采用反射的方式具體調(diào)用在Spring上下文中定義好的Service類來取得結(jié)果,輸入的參數(shù)和返回值都是String,其實(shí)質(zhì)是XML形式的字符串。這樣做的好處是:其一,接口類和實(shí)現(xiàn)類從設(shè)計(jì)開始代碼就處于穩(wěn)定狀態(tài),以后極少維護(hù),不會(huì)越來越大;其二,自然消除了多個(gè)開發(fā)人員需要修改同一文件的沖突問題;其三,如果服務(wù)器負(fù)載過重,可以在實(shí)現(xiàn)類中根據(jù)輸入?yún)?shù)的內(nèi)容做一個(gè)分流,把一些任務(wù)分配到其它服務(wù)器上去,甚至可以采用前端一個(gè)分流服務(wù)器,后面一堆負(fù)責(zé)具體業(yè)務(wù)的服務(wù)器的形式。由于只有一個(gè)函數(shù),這樣修改起來也容易得多。事實(shí)上,采用了這種方式后,完成各個(gè)流程的程序員只負(fù)責(zé)前端表現(xiàn)輸入,后端的Service類等三個(gè)位置的代碼,相互間處于平行狀態(tài),基本沒有交叉,減少了沖突,提高了開發(fā)效率。下面是實(shí)現(xiàn)類的具體代碼。
五.WebService端實(shí)現(xiàn)類的具體代碼
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.apache.log4j.Logger;
import org.dom4j.DocumentException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
/**
* 此類中共有方法為WebService對(duì)外方法,其它方法為輔助此方法而使用
*
* 創(chuàng)建日期:2010-2-9 上午09:19:31
* 修改時(shí)間:2010-2-9 上午09:19:31
*/
public class ServiceImpl implements IService{
// 日志記錄器
private static Logger logger = Logger.getLogger(ServiceImpl.class);
/**
* 此函數(shù)將逐步進(jìn)行以下任務(wù)
* 1.在log文件中記錄請(qǐng)求的XML文本
* 2.解析文本,得到要訪問的類名,方法名,參數(shù)
* 3.使用反射調(diào)用類的方法
* 4.返回結(jié)果
* @throws InstantiationException
*/
public String getResponseXML(String requestXML){
logger.info("接收到客戶端的請(qǐng)求XML文本:"+requestXML);
// 新建一個(gè)包裝器
ResponseXMLPackager packager=new ResponseXMLPackager();
try {
// 使用解析器解析請(qǐng)求XML文本
RequestXMLParser parser=new RequestXMLParser(requestXML);
// 從解析器中獲取Service服務(wù)類
packager.setServiceName(parser.getServiceName());
// 從解析器中獲取方法名
packager.setMethodName(parser.getMethodName());
// 從解析器中獲取方法參數(shù)
packager.setArgs(parser.getArgs());
// 通過Spring得到實(shí)例
Object obj=SpringUtil.getBean(packager.getServiceName());
logger.info("在Spring上下文配置文件中找到了'"+packager.getMethodName()+"'對(duì)應(yīng)的bean.");
// 得到實(shí)例對(duì)應(yīng)的類
Class<?> cls=obj.getClass();
// 通過反射得到方法
Method method = cls.getMethod(packager.getMethodName(), new Class[] {String[].class});
logger.info("通過反射獲得了'"+packager.getMethodName()+"'對(duì)應(yīng)的方法.");
// 通過反射調(diào)用對(duì)象的方法
String methodResonseXML=(String)method.invoke(obj,new Object[] {packager.getArgs()});
logger.info("通過反射調(diào)用方法'"+packager.getMethodName()+"'成功.");
/**************************
* 設(shè)置狀態(tài),備注及方法反饋結(jié)果
**************************/
String remark="成功執(zhí)行類'"+packager.getServiceName()+"'的方法'"+packager.getMethodName()+"'";
packager.setStatus(ResponseXMLPackager.Status_Success);
packager.setRemark(remark);
packager.setMethodResonseXML(methodResonseXML);
logger.info(remark);
}catch (DocumentException e) {
// 解析不了從客戶端傳過來的XML文本時(shí)
String remark="無法解析客戶端的請(qǐng)求XML文本:"+requestXML+".";
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_CanNotParseRequestXML);
logger.error(remark);
}catch (NoSuchBeanDefinitionException e) {
// Spring找不到bean時(shí)
String remark="無法在Spring上下文定義文件appCtx.xml中找到id'"+packager.getServiceName()+"'對(duì)應(yīng)的bean.";
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_CanNotFoundServiceName);
logger.error(remark);
}
catch (NoSuchMethodException e) {
// 找不到方法時(shí)
String remark=("類'"+packager.getServiceName()+"'中沒有名為 ‘"+packager.getMethodName()+"’的方法,或是此方法非公有函數(shù),或是參數(shù)不是字符串?dāng)?shù)組形式.");
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_NotFoundSuchMethod);
logger.error(remark);
}catch (IllegalAccessException e) {
// 當(dāng)訪問權(quán)限不夠時(shí)
String remark=("訪問類'"+packager.getServiceName()+"'中名為 ‘"+packager.getMethodName()+"’的方法非法,可能原因是當(dāng)前方法(getResponseXML)對(duì)該方法的訪問權(quán)限不夠.");
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_CanNotAccessMethod);
logger.error(remark);
}catch (InvocationTargetException e) {
// 當(dāng)調(diào)用的函數(shù)拋出異常時(shí)
Exception tragetException=(Exception)e.getTargetException();
if(tragetException instanceof BreakException){
// 程序中斷,不能繼續(xù)進(jìn)行的情況.比如說用戶沒有操作權(quán)限,要找的目標(biāo)不存在等.
packager.setRemark(tragetException.getMessage());
packager.setStatus(ResponseXMLPackager.Status_Ng);
String remark=("執(zhí)行類'"+packager.getServiceName()+"'中名為 ‘"+packager.getMethodName()+"’的方法時(shí)被中斷,原因是:"+tragetException.getMessage()+".");
logger.warn(remark);
}
else{
// 程序運(yùn)行過程中拋出異常,如空指針異常,除零異常,主鍵約束異常等.
String remark=("執(zhí)行類'"+packager.getServiceName()+"'中名為 ‘"+packager.getMethodName()+"’的方法時(shí),該方法拋出了異常,異常類型為:"+tragetException.getClass().getName()+",異常信息是"+tragetException.getMessage()+".");
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_MethodThrowException);
logger.error(remark);
}
}
// 向客戶端返回響應(yīng)XML文本
return packager.toXML();
}
}
六.Service類中函數(shù)的輸入和輸出
從上面的代碼可見,客戶端傳過來是一個(gè)XML形式的文本,RequestXMLParser類負(fù)責(zé)從這段文本中解析出具體想調(diào)用的配置在Spring上下文中Service類的beanName,類中的具體函數(shù)名和函數(shù)的參數(shù),然后再用反射的方式調(diào)用之。為了調(diào)用方便,讓每個(gè)Service類的具體參數(shù)都是String[] 形式的(現(xiàn)在看如果采用類似JSON的形式更好一點(diǎn)),在內(nèi)部再獲得其實(shí)際數(shù)據(jù),這樣,來自客戶端的調(diào)用就能順利的到達(dá)目的函數(shù)中。函數(shù)運(yùn)行完畢后,傳出的也是一個(gè)XML形式的字符串,這是為了返回?cái)?shù)據(jù)的方便,到了客戶端后,再進(jìn)行解析變成領(lǐng)域?qū)ο箢愂纠O旅娲a是一個(gè)Service類中函數(shù)的例子:
/**
* 添加一個(gè)Tmp對(duì)象到數(shù)據(jù)庫
* @param args
* @return
* @throws Exception
*/
public String add(String[] args) throws Exception{
String name=args[0];
// 同名檢測(cè)
if(hasSameName(name)){
throw new BreakException("已經(jīng)有和"+name+"同名的對(duì)象存在了.");
}
int age=Integer.parseInt(args[1]);
float salary=Float.parseFloat(args[2]);
String picture=args[3];
Tmp tmp=new Tmp(name,age,salary,picture);
dao.create(tmp);
return tmp.toXML();
}
七.領(lǐng)域?qū)ο笈cXML之間的相互轉(zhuǎn)化
由于DB服務(wù)器和Client之間傳遞的是XML形式的文本,但內(nèi)部使用的都是領(lǐng)域?qū)ο螅敲矗虚g需要兩次轉(zhuǎn)化過程。以取得一個(gè)Tmp對(duì)象為例,在服務(wù)器端,dao從數(shù)據(jù)庫取得記錄后會(huì)形成Tmp領(lǐng)域?qū)ο蟮膶?shí)例,這個(gè)實(shí)例會(huì)轉(zhuǎn)化成XML傳到客戶端;客戶端得到這段XML文本會(huì)把它還原成領(lǐng)域?qū)ο蟆R韵麓a闡述了這兩個(gè)過程:
// 服務(wù)器端領(lǐng)域?qū)ο蟮幕悾膖oXML()函數(shù)使得實(shí)例轉(zhuǎn)化為XML,它的子類只要實(shí)現(xiàn)changePropertytoXML()這個(gè)抽象接口就能得到此項(xiàng)功能。
public abstract class BaseDomainObj{
// 領(lǐng)域?qū)ο蟮奈ㄒ蛔R(shí)別標(biāo)志
protected long id;
// 名稱
protected String name;
// 對(duì)象對(duì)應(yīng)的記錄被添加到數(shù)據(jù)庫的時(shí)間(入庫時(shí)間)
protected String addTime;
// 對(duì)象對(duì)應(yīng)的記錄最近被更新的時(shí)間(更新時(shí)間)
protected String refreshTime;
// 備注
protected String remark;
// 節(jié)點(diǎn)名
protected String nodeName;
// 記錄是否有效,若為false則說明無效,常改變此值來隱藏或是顯示一個(gè)對(duì)象
protected boolean valid;
/**
* 無參構(gòu)造函數(shù)
*/
public BaseDomainObj(){
this(0);
}
/**
* 指定id的構(gòu)造函數(shù)
* @param id
*/
public BaseDomainObj(long id){
this.id=id;
String currTime=getCurrTime();
addTime=currTime;
refreshTime=currTime;
valid=true;
remark="";
}
/**
* 將對(duì)象轉(zhuǎn)化為XML形式
* @return
*/
public String toXML() {
StringBuilder sb=new StringBuilder();
sb.append("<"+nodeName+">");
sb.append("<id>"+id+"</id>");
sb.append("<name>"+name+"</name>");
sb.append("<addTime>"+addTime+"</addTime>");
sb.append("<refreshTime>"+refreshTime+"</refreshTime>");
sb.append("<remark>"+remark+"</remark>");
sb.append("<valid>"+valid+"</valid>");
sb.append(changePropertytoXML());
sb.append("</"+nodeName+">");
return sb.toString();
}
/**
* 將屬性轉(zhuǎn)化為XML,強(qiáng)制子類實(shí)現(xiàn)
* @return
*/
protected abstract String changePropertytoXML();
/**
* 取得當(dāng)前時(shí)間
*/
private static String getCurrTime() {
Date date = new Date();
Format formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formatter.format(date);
}
/*************************
* 以下為setter/getter
*************************/
..
}
// 具體的Tmp對(duì)象,重點(diǎn)是changePropertytoXML()這個(gè)函數(shù)。
public class Tmp extends BaseDomainObj{
// 年齡
private int age;
// 薪水
private float salary;
/**
* 無參構(gòu)造函數(shù)
*/
public Tmp(){
this("",0,0.0f);
}
/**
* 三參數(shù)構(gòu)造函數(shù)
* @param name
* @param age
* @param salary
*/
public Tmp(String name,int age,float salary){
nodeName="Tmp";
this.name=name;
this.age=age;
this.salary=salary;
}
@Override
protected String changePropertytoXML() {
StringBuilder sb=new StringBuilder();
sb.append("<age>"+age+"</age>");
sb.append("<salary>"+salary+"</salary>");
return sb.toString();
}
/***************************
* 以下為setter/getter部分
***************************/
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
這樣,在得到一個(gè)Tmp對(duì)象的實(shí)例后,調(diào)用其toXML函數(shù)就能得到這個(gè)實(shí)例的XML形式表現(xiàn)文本。“六”中的函數(shù)就是這樣做的。
傳出的XML文本實(shí)例:
<Tmp>
<id>1</id>
<name>0</name>
<addTime>2010-02-15 23:39:06</addTime>
<refreshTime>2010-02-15 23:39:06</refreshTime>
<remark></remark>
<valid>true</valid>
<age>30</age>
<salary>15000.0</salary>
</Tmp>
上面這段文本傳回到客戶端后怎么再把它變成實(shí)例呢,有了Apache的BeanUtils包任務(wù)就簡單多了。下面請(qǐng)看客戶端的Tmp類及其基類:
// 客戶端Tmp類:
public class Tmp extends BaseDomainObj{
// 年齡
private String age;
// 薪水
private String salary;
@Override
public Object[] toArray() {
return new Object[]{id,name,age,salary,addTime,refreshTime,valid,remark};
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getSalary() {
return salary;
}
public void setSalary(String salary) {
this.salary = salary;
}
}
// Tmp類的基類:
public abstract class BaseDomainObj{
// 領(lǐng)域?qū)ο蟮奈ㄒ蛔R(shí)別標(biāo)志
protected String id;
// 名稱
protected String name;
// 對(duì)象對(duì)應(yīng)的記錄被添加到數(shù)據(jù)庫的時(shí)間(入庫時(shí)間)
protected String addTime;
// 對(duì)象對(duì)應(yīng)的記錄最近被更新的時(shí)間(更新時(shí)間)
protected String refreshTime;
// 備注
protected String remark;
// 記錄是否有效,若為false則不該進(jìn)入
protected String valid;
/**
* ?無參構(gòu)造函數(shù)
*/
public BaseDomainObj(){
}
/**
* 有參構(gòu)造函數(shù),使用此函數(shù)傳入一個(gè)XML,得到相應(yīng)對(duì)象
* @param xml
* @throws DocumentException
*/
public BaseDomainObj(String xml) throws DocumentException{
fromXML(xml);
}
/**
* 將對(duì)象轉(zhuǎn)化為數(shù)組形式,便于在表格中顯示
* @return
*/
public abstract Object[] toArray();
/**
* 使用BeanUtils將XML的節(jié)點(diǎn)轉(zhuǎn)化到屬性中
* @param xml
* @throws DocumentException
*/
@SuppressWarnings("unchecked")
public void fromXML(String xml) throws DocumentException{
Document doc=DocumentHelper.parseText(xml);
Element root=doc.getRootElement();
List<Element> elms=root.elements();
for(Element elm:elms){
try {
BeanUtils.setProperty(this,elm.getName(),elm.getText());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddTime() {
return addTime;
}
public void setAddTime(String addTime) {
this.addTime = addTime;
}
public String getRefreshTime() {
return refreshTime;
}
public void setRefreshTime(String refreshTime) {
this.refreshTime = refreshTime;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getValid() {
return valid;
}
public void setValid(String valid) {
this.valid = valid;
}
}
重要的是上面的黑體部分,只要我們保證XML的字段和Tmp對(duì)象中的字段是一一對(duì)應(yīng)的,fromXML函數(shù)就能保證完成XML到對(duì)象的轉(zhuǎn)換,對(duì)于負(fù)責(zé)具體業(yè)務(wù)的程序員,在代碼里如下做就可以了:
String objXML=“
”;// 從WebService端取出的Tmp對(duì)象XML文本
Tmp tmp=new Tmp(objXML);// 這樣,對(duì)象就出來了.
小結(jié):
一.框架設(shè)計(jì)者一定要定義好框架的任務(wù),限制具體程序員的行為,否則項(xiàng)目的可讀性可維護(hù)性就是一句空話。
二.框架一定要完成主干的任務(wù)的流程,而具體程序員只負(fù)責(zé)枝節(jié),換言之,具體程序員只該負(fù)責(zé)簡單的規(guī)定好了的任務(wù),如某函數(shù)的具體實(shí)現(xiàn)。
三.好的框架完成后,其他人應(yīng)該能像填空一樣完成任務(wù),要讓他們?cè)谕瓿扇蝿?wù)時(shí)不需要思考具體的來龍去脈。
四.好的框架能讓完成任務(wù)的程序員盡量平行,減少相互間的交流成本。實(shí)際上,框架和工廠流水線的設(shè)計(jì)某種程度上是相通的。
五.隨著數(shù)據(jù)量和規(guī)模的增大,一些問題會(huì)逐漸顯山露水,這就需要框架設(shè)計(jì)者有前瞻性的眼光。
六.如果框架已經(jīng)不能滿足需求,帶來很多問題時(shí),設(shè)計(jì)者需要有把前設(shè)計(jì)推到重來重新組建新框架的勇氣和毅力,當(dāng)斷不斷,修修補(bǔ)補(bǔ),蹣跚前行,反受其害。