一.C/S兩端的任務分離
考慮到便于信息接收傳遞顯示的因素,交易系統和QQ類似,采用了傳統的C/S模式而不是B/S模式。Client端主要負責取得用戶輸入和數據顯示,而Server端分為DBServer和MsgServer兩個,前者負責數據的持久化,后者負責消息的傳遞。撇開消息服務器MsgServer不談的話,數據傳遞主要發生在Client和DBServer之間。
二.C/S兩端的交互方式
由于C端只負責數據的輸入和顯示,它必然需要向DBServer端存取數據,這就有一個信息載體和交互方式的問題。C端需要向DbServer(以下簡稱DS)傳遞的信息是多種多樣的,簡單命令行形式的數據肯定不行,類似JSON的線性形式不夠表現樹狀數據,只有XML才有豐富的表現能力,它無論是簡單的線性數據還是復雜的樹狀數據都能容納,有了dom4j或是jdom的幫助,解析起來也很方便。交互方式上,由于C可能在廣域網中,還可能有防火墻的阻擋,這樣Socket長連接就受到一定程度的限制,要是采用WebService問題就解決了,因為WebService的底層協議還是http,也走80端口,不會被防火墻阻擋,這樣,DBServer就成了一臺放置在公網上的WebService服務器,為各個Client提供Webservice服務。
三.實現WebService的軟件選擇
備選有Axis1/2和XFire兩種方案,選擇的依據主要是效率。通過一段時間的使用,發現XFire的效率確實比Axis1/2高,估測同等調用只占后者的一半左右。其它的易用性,穩定性等沒有成為選擇依據,因為如果XFire還不行再換其它的軟件也來得及,下面的設計保證了系統不會依賴于特定的WebService端軟件。
四.WebSevice端的對外接口設計
WebService的對外端口一般是由一個接口和一個實現類組成,實現類中的函數是具體實現,接口是調用者和實現者共同遵守的規約;一般來說如果客戶端需要一個函數的話,那么服務器端的接口類要定義這個函數,實現類實現這個函數。這樣的方式在交互簡單,數據量小的時侯沒有問題,且使用很方便,但量變引起質變,如果交互復雜,需要的函數眾多,數據量與日俱增的話,問題就來了。其一,這回導致接口類和實現類函數越來越多,體積越來越大,對定位維護修改帶來很大的不變;其二,接口類和實現類常會被修改,而開發人員之間的協同等待甚至沖突就日益增多起來,阻滯了開發效率;其三,也是最重要的,系統的可擴展性缺乏,難以動態維護,即使增加多個服務器分擔負載,也需要手動修改大量的代碼。因此,這種傳統的方式在Demo版過后就被放棄了。
新的方式采用的單接口設計,即接口類中只定義一個函數,實現類實現這一個函數,其內部采用反射的方式具體調用在Spring上下文中定義好的Service類來取得結果,輸入的參數和返回值都是String,其實質是XML形式的字符串。這樣做的好處是:其一,接口類和實現類從設計開始代碼就處于穩定狀態,以后極少維護,不會越來越大;其二,自然消除了多個開發人員需要修改同一文件的沖突問題;其三,如果服務器負載過重,可以在實現類中根據輸入參數的內容做一個分流,把一些任務分配到其它服務器上去,甚至可以采用前端一個分流服務器,后面一堆負責具體業務的服務器的形式。由于只有一個函數,這樣修改起來也容易得多。事實上,采用了這種方式后,完成各個流程的程序員只負責前端表現輸入,后端的Service類等三個位置的代碼,相互間處于平行狀態,基本沒有交叉,減少了沖突,提高了開發效率。下面是實現類的具體代碼。
五.WebService端實現類的具體代碼
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對外方法,其它方法為輔助此方法而使用
*
* 創建日期:2010-2-9 上午09:19:31
* 修改時間:2010-2-9 上午09:19:31
*/
public class ServiceImpl implements IService{
// 日志記錄器
private static Logger logger = Logger.getLogger(ServiceImpl.class);
/**
* 此函數將逐步進行以下任務
* 1.在log文件中記錄請求的XML文本
* 2.解析文本,得到要訪問的類名,方法名,參數
* 3.使用反射調用類的方法
* 4.返回結果
* @throws InstantiationException
*/
public String getResponseXML(String requestXML){
logger.info("接收到客戶端的請求XML文本:"+requestXML);
// 新建一個包裝器
ResponseXMLPackager packager=new ResponseXMLPackager();
try {
// 使用解析器解析請求XML文本
RequestXMLParser parser=new RequestXMLParser(requestXML);
// 從解析器中獲取Service服務類
packager.setServiceName(parser.getServiceName());
// 從解析器中獲取方法名
packager.setMethodName(parser.getMethodName());
// 從解析器中獲取方法參數
packager.setArgs(parser.getArgs());
// 通過Spring得到實例
Object obj=SpringUtil.getBean(packager.getServiceName());
logger.info("在Spring上下文配置文件中找到了'"+packager.getMethodName()+"'對應的bean.");
// 得到實例對應的類
Class<?> cls=obj.getClass();
// 通過反射得到方法
Method method = cls.getMethod(packager.getMethodName(), new Class[] {String[].class});
logger.info("通過反射獲得了'"+packager.getMethodName()+"'對應的方法.");
// 通過反射調用對象的方法
String methodResonseXML=(String)method.invoke(obj,new Object[] {packager.getArgs()});
logger.info("通過反射調用方法'"+packager.getMethodName()+"'成功.");
/**************************
* 設置狀態,備注及方法反饋結果
**************************/
String remark="成功執行類'"+packager.getServiceName()+"'的方法'"+packager.getMethodName()+"'";
packager.setStatus(ResponseXMLPackager.Status_Success);
packager.setRemark(remark);
packager.setMethodResonseXML(methodResonseXML);
logger.info(remark);
}catch (DocumentException e) {
// 解析不了從客戶端傳過來的XML文本時
String remark="無法解析客戶端的請求XML文本:"+requestXML+".";
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_CanNotParseRequestXML);
logger.error(remark);
}catch (NoSuchBeanDefinitionException e) {
// Spring找不到bean時
String remark="無法在Spring上下文定義文件appCtx.xml中找到id'"+packager.getServiceName()+"'對應的bean.";
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_CanNotFoundServiceName);
logger.error(remark);
}
catch (NoSuchMethodException e) {
// 找不到方法時
String remark=("類'"+packager.getServiceName()+"'中沒有名為 ‘"+packager.getMethodName()+"’的方法,或是此方法非公有函數,或是參數不是字符串數組形式.");
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_NotFoundSuchMethod);
logger.error(remark);
}catch (IllegalAccessException e) {
// 當訪問權限不夠時
String remark=("訪問類'"+packager.getServiceName()+"'中名為 ‘"+packager.getMethodName()+"’的方法非法,可能原因是當前方法(getResponseXML)對該方法的訪問權限不夠.");
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_CanNotAccessMethod);
logger.error(remark);
}catch (InvocationTargetException e) {
// 當調用的函數拋出異常時
Exception tragetException=(Exception)e.getTargetException();
if(tragetException instanceof BreakException){
// 程序中斷,不能繼續進行的情況.比如說用戶沒有操作權限,要找的目標不存在等.
packager.setRemark(tragetException.getMessage());
packager.setStatus(ResponseXMLPackager.Status_Ng);
String remark=("執行類'"+packager.getServiceName()+"'中名為 ‘"+packager.getMethodName()+"’的方法時被中斷,原因是:"+tragetException.getMessage()+".");
logger.warn(remark);
}
else{
// 程序運行過程中拋出異常,如空指針異常,除零異常,主鍵約束異常等.
String remark=("執行類'"+packager.getServiceName()+"'中名為 ‘"+packager.getMethodName()+"’的方法時,該方法拋出了異常,異常類型為:"+tragetException.getClass().getName()+",異常信息是"+tragetException.getMessage()+".");
packager.setRemark(remark);
packager.setStatus(ResponseXMLPackager.Status_MethodThrowException);
logger.error(remark);
}
}
// 向客戶端返回響應XML文本
return packager.toXML();
}
}
六.Service類中函數的輸入和輸出
從上面的代碼可見,客戶端傳過來是一個XML形式的文本,RequestXMLParser類負責從這段文本中解析出具體想調用的配置在Spring上下文中Service類的beanName,類中的具體函數名和函數的參數,然后再用反射的方式調用之。為了調用方便,讓每個Service類的具體參數都是String[] 形式的(現在看如果采用類似JSON的形式更好一點),在內部再獲得其實際數據,這樣,來自客戶端的調用就能順利的到達目的函數中。函數運行完畢后,傳出的也是一個XML形式的字符串,這是為了返回數據的方便,到了客戶端后,再進行解析變成領域對象類示例。下面代碼是一個Service類中函數的例子:
/**
* 添加一個Tmp對象到數據庫
* @param args
* @return
* @throws Exception
*/
public String add(String[] args) throws Exception{
String name=args[0];
// 同名檢測
if(hasSameName(name)){
throw new BreakException("已經有和"+name+"同名的對象存在了.");
}
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();
}
七.領域對象與XML之間的相互轉化
由于DB服務器和Client之間傳遞的是XML形式的文本,但內部使用的都是領域對象,那么,中間需要兩次轉化過程。以取得一個Tmp對象為例,在服務器端,dao從數據庫取得記錄后會形成Tmp領域對象的實例,這個實例會轉化成XML傳到客戶端;客戶端得到這段XML文本會把它還原成領域對象。以下代碼闡述了這兩個過程:
// 服務器端領域對象的基類,它的toXML()函數使得實例轉化為XML,它的子類只要實現changePropertytoXML()這個抽象接口就能得到此項功能。
public abstract class BaseDomainObj{
// 領域對象的唯一識別標志
protected long id;
// 名稱
protected String name;
// 對象對應的記錄被添加到數據庫的時間(入庫時間)
protected String addTime;
// 對象對應的記錄最近被更新的時間(更新時間)
protected String refreshTime;
// 備注
protected String remark;
// 節點名
protected String nodeName;
// 記錄是否有效,若為false則說明無效,常改變此值來隱藏或是顯示一個對象
protected boolean valid;
/**
* 無參構造函數
*/
public BaseDomainObj(){
this(0);
}
/**
* 指定id的構造函數
* @param id
*/
public BaseDomainObj(long id){
this.id=id;
String currTime=getCurrTime();
addTime=currTime;
refreshTime=currTime;
valid=true;
remark="";
}
/**
* 將對象轉化為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();
}
/**
* 將屬性轉化為XML,強制子類實現
* @return
*/
protected abstract String changePropertytoXML();
/**
* 取得當前時間
*/
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對象,重點是changePropertytoXML()這個函數。
public class Tmp extends BaseDomainObj{
// 年齡
private int age;
// 薪水
private float salary;
/**
* 無參構造函數
*/
public Tmp(){
this("",0,0.0f);
}
/**
* 三參數構造函數
* @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;
}
}
這樣,在得到一個Tmp對象的實例后,調用其toXML函數就能得到這個實例的XML形式表現文本。“六”中的函數就是這樣做的。
傳出的XML文本實例:
<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>
上面這段文本傳回到客戶端后怎么再把它變成實例呢,有了Apache的BeanUtils包任務就簡單多了。下面請看客戶端的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{
// 領域對象的唯一識別標志
protected String id;
// 名稱
protected String name;
// 對象對應的記錄被添加到數據庫的時間(入庫時間)
protected String addTime;
// 對象對應的記錄最近被更新的時間(更新時間)
protected String refreshTime;
// 備注
protected String remark;
// 記錄是否有效,若為false則不該進入
protected String valid;
/**
* ?無參構造函數
*/
public BaseDomainObj(){
}
/**
* 有參構造函數,使用此函數傳入一個XML,得到相應對象
* @param xml
* @throws DocumentException
*/
public BaseDomainObj(String xml) throws DocumentException{
fromXML(xml);
}
/**
* 將對象轉化為數組形式,便于在表格中顯示
* @return
*/
public abstract Object[] toArray();
/**
* 使用BeanUtils將XML的節點轉化到屬性中
* @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對象中的字段是一一對應的,fromXML函數就能保證完成XML到對象的轉換,對于負責具體業務的程序員,在代碼里如下做就可以了:
String objXML=“
”;// 從WebService端取出的Tmp對象XML文本
Tmp tmp=new Tmp(objXML);// 這樣,對象就出來了.
小結:
一.框架設計者一定要定義好框架的任務,限制具體程序員的行為,否則項目的可讀性可維護性就是一句空話。
二.框架一定要完成主干的任務的流程,而具體程序員只負責枝節,換言之,具體程序員只該負責簡單的規定好了的任務,如某函數的具體實現。
三.好的框架完成后,其他人應該能像填空一樣完成任務,要讓他們在完成任務時不需要思考具體的來龍去脈。
四.好的框架能讓完成任務的程序員盡量平行,減少相互間的交流成本。實際上,框架和工廠流水線的設計某種程度上是相通的。
五.隨著數據量和規模的增大,一些問題會逐漸顯山露水,這就需要框架設計者有前瞻性的眼光。
六.如果框架已經不能滿足需求,帶來很多問題時,設計者需要有把前設計推到重來重新組建新框架的勇氣和毅力,當斷不斷,修修補補,蹣跚前行,反受其害。