一.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);// 这样,对象就出来了.
小结:
一.框架设计者一定要定义好框架的任务,限制具体程序员的行为,否则项目的可读性可维护性就是一句空话。
二.框架一定要完成主干的任务的流程,而具体程序员只负责枝节,换言之,具体程序员只该负责简单的规定好了的任务,如某函数的具体实现。
三.好的框架完成后,其他人应该能像填空一样完成任务,要让他们在完成任务时不需要思考具体的来龙去脉。
四.好的框架能让完成任务的程序员尽量平行,减少相互间的交流成本。实际上,框架和工厂流水线的设计某种程度上是相通的。
五.随着数据量和规模的增大,一些问题会逐渐显山露水,这就需要框架设计者有前瞻性的眼光。
六.如果框架已经不能满足需求,带来很多问题时,设计者需要有把前设计推到重来重新组建新框架的勇气和毅力,当断不断,修修补补,蹒跚前行,反受其害。