概述
RIA和SOA是一对绝配。SOA强调把业务以接口方式向外界提供不关注前端的呈现,而RIA则强调用户体现,结合两者优势能够设计出用户体现良好、灵活的、易扩展、易集成的系统。要处理好RIA前端和SOA后端,需要搭建一个健壮的企业级通信层,该层职责:
- 负责处理RIA前端和SOA后端的数据交互。
- 封装SOA业务接口,便于开发调用。
- 采用异步通信方式,SOA业务接口请求返回时进行回调。
- SOA业务接口调用错误处理机制。
SOA业务接口的设计
- 不使用webservice,采用自定义数据格式,数据简单些。
- 针对Flex,返回格式采用xml数据格式方便写。
- 安全策略:用户调用登录接口后返回登录信息,其中还回信息中包含令牌(session),每次调用登录以外的业务接口都必须传入令牌参数作验证。同一用户,每次登录令牌值都会刷新,即:最后登录的获得使用权。
- 大部分业务接口采用GET方式调用,少数采用POST方式,后台应该统一处理GET,POST参数。
SOA通信格式的设计
业务接口调用成功返回格式(XML)
<?xml version="1.0" encoding="utf-8">
<rsp>
{业务所需的XML节点信息}
</rsp>
业务接口调用失败返回格式(XML)
<?xml version="1.0" encoding="utf-8">
<error_rsp>
<code>{错误码}</code>
<msg>{错误描述}</msg>
</error_rsp>
业务接口常用参数
- v:version,业务接口版本号,用于版本控制,同一接口根据不同版本号可以有不同的实现逻辑。
- f:format,返回格式,指定返回信息的格式,常用的有xml,json,Flex使用xml方便些。
- api:业务接口名称,用于区分接口类型。
- t:时间戳,用于解决客户端浏览器缓存问题,服务端不处理该参数。
- session:令牌/会话码,除了登录接口,其他接口一般都需要这个参数。
调用例子
-
登录成功
- Request: http://server1:2009/LCMS2/api.jhtm?api=lcms.user.login&v=2.0&f=xml&uname=Hunk&pwd=123
-
Reponse:
<?xml version="1.0" encoding="UTF-8" ?>
<rsp>
<uid>51</uid>
<urole>4</urole>
<session>cbff8aca-8b56-48d7-a740-534f3a84e575</session>
</rsp>
-
登录失败
- Request: http://server1:2009/LCMS2/api.jhtm?api=lcms.user.login&v=2.0&f=xml&uname=User1&pwd=123
-
Reponse:
<?xml version="1.0" encoding="UTF-8" ?>
<error_rsp>
<code>500</code>
<msg>用户不存在</msg>
</error_rsp>
-
获取用户列表
- Request: http://server1:2009/LCMS2/api.jhtm?api=lcms.user.get&v=2.0&f=xml&session=cbff8aca-8b56-48d7-a740-534f3a84e575&page_no=1&page_size=10
-
Reponse:totalResults是不分页时的查找总数量,返回第一页的item(用户),每页10行
<?xml version="1.0" encoding="UTF-8" ?>
<rsp>
<totalResults>37</totalResults>
<item><uid>51</uid><uname>hunk</uname><urole>4</urole></item>
<item><uid>48</uid><uname>hunk4</uname><urole>3</urole></item>
</rsp>
-
删除用户
- Request: http://server1:2009/LCMS2/api.jhtm?api=lcms.user.delete&v=2.0&f=xml&session=cbff8aca-8b56-48d7-a740-534f3a84e575&del_uid=36
-
Reponse:
<?xml version="1.0" encoding="UTF-8" ?>
<rsp>
<true />
</rsp>
HttpRequest类
该类用于封装请求参数,方便调用,声明为internal,只在包内使用,代码如下:
package cwn.lcms
{
import mx.collections.ArrayCollection;
internal class HttpRequest
{
//用于存放Get参数
private var _GetParams:ArrayCollection = new ArrayCollection();
//用于存放Post参数
private var _PostParams:ArrayCollection = new ArrayCollection();
//根路径
public var RootUrl:String;
private var _Disposed:Boolean = false;
public function HttpRequest(root:String = "")
{
RootUrl = root;
}
//需要对参数进行url加密
private function Encode(value:String):String
{
return encodeURIComponent(value);
}
public function AddGetParam(name:String, value:String):void
{
_GetParams.addItem(name + "=" + Encode(value));
}
public function AddPostParam(name:String, value:String):void
{
_PostParams.addItem(name + "=" + Encode(value));
}
public function GetUrl():String
{
var url:String = RootUrl;
if (url.lastIndexOf("?") < 0)
url += "?";
for (var i:int = 0; i < _GetParams.length; i++)
{
url += _GetParams[i] + "&";
}
if (_GetParams.length > 0)
url = url.substr(0, url.length - 1);
return url;
}
public function get HasPostData():Boolean
{
return _PostParams.length > 0;
}
public function GetPostData():String
{
if (!HasPostData)
return null;
var data:String = "";
for (var i:int = 0; i < _PostParams.length; i++)
{
data += _PostParams[i] + "&";
}
data = data.substr(0, data.length - 1);
return data;
}
public function Dispose():void
{
if (_Disposed)
return;
_Disposed = true;
_GetParams.removeAll();
_PostParams.removeAll();
_GetParams = null;
_PostParams = null;
}
}
}
各种数据类
定义各种业务接口相关的数据类,把XML节点转成Object,利用IDE智能感知,方便编码,以下是UserData类的定义。
package cwn.lcms.data
{
public class UserData
{
public var Id:String = "";
public var Name:String = "";
public var Password:String = "";
public var Session:String = "";
public var Role:int = 0;
public function UserData(xml:XML = null)
{
FromXml(xml);
}
public function FromXml(xml:XML):void
{
if (xml == null)
return;
Id = xml.uid;
Name = xml.uname;
Role = xml.urole;
Session = xml.session;
}
public function Clone():UserData
{
var data:UserData = new UserData();
data.Id = Id;
data.Name = Name;
data.Password = Password;
data.Session = Session;
data.Role = Role;
return data;
}
}
}
WebAPI类
该类是通信层最核心的类,以静态属性、方法为主,先定义一些主要的属性和工具方法,如下:
package cwn.lcms
{
import cwn.lcms.data.UserData;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.external.ExternalInterface;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLVariables;
import mx.collections.ArrayCollection;
public final class WebAPI
{
private static var _Debug:Boolean = false; //调试控制
private static var _HttpCache:ArrayCollection = new ArrayCollection(); //存放http请求
public static var Url:String = null; //根路径
public static var Session:String = null; //会话码
public static var SessionErrorFunction:Function; //会话错误回调方法
public static var CurrentUser:UserData = null; //当前登录用户数据
private static function RemoveCache(value:Object):void
{
var i:int = _HttpCache.getItemIndex(value);
if (i >= 0)
{
var loader:URLLoader = _HttpCache.removeItemAt(i) as URLLoader;
if (loader != null)
{
loader.removeEventListener(IOErrorEvent.IO_ERROR, IOErrorEventHandler);
loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, SecurityErrorHandler);
}
}
}
//执行HTTP请求并缓存请求,callbackXml:Function(result:XML):void
private static function Invoke(http:HttpRequest, callbackXml:Function = null):void
{
if (_Debug)
{
trace(http.GetUrl());
}
var loader:URLLoader = new URLLoader();
_HttpCache.addItem(loader);
loader.addEventListener(IOErrorEvent.IO_ERROR, IOErrorEventHandler);
loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, SecurityErrorHandler);
loader.addEventListener(Event.COMPLETE, function(event:Event):void
{
OnInvokeCallback(loader.data, callbackXml);
RemoveCache(loader);
});
var request:URLRequest = new URLRequest(http.GetUrl());
request.method = "GET";
if (http.HasPostData) //当有Post参数时,改用POST方式请求
{
request.method = "POST";
request.data = new URLVariables(http.GetPostData());
}
loader.load(request);
}
//HTTP请求成功回调,callbackXml:Function(result:XML):void
private static function OnInvokeCallback(response:Object, callbackXml:Function = null):void
{
try
{
if (_Debug)
{
trace("rsp:" + response);
}
var result:XML = XML(response);
if (result.localName() == "rsp") //调用业务接口成功
{
if (callbackXml != null)
callbackXml(result);
}
else if (result.localName() == "error_rsp") //调用业务接口错误
{
if (int(result.code) == 8 && SessionErrorFunction != null) //session错误处理
{
SessionErrorFunction();
LcmsError.ShowInfo("该用户已在其他机器上登录,请重新登录。");
}
else
LcmsError.ShowError(result); //输出错误信息
if (callbackXml != null)
callbackXml(null);
}
else
LcmsError.ShowInfo("[数据格式错误]/r/n" + response); //输出错误信息
}
catch (e:Error)
{
LcmsError.ShowInfo("[未知异常]/r/n" + e.message + "/r/n" + e.getStackTrace()); //输出错误信息
}
}
//HTTP请求错误处理
private static function SecurityErrorHandler(event:SecurityErrorEvent):void
{
LcmsError.ShowInfo("[无访问权限]/r/n" + event.text); //输出错误信息
RemoveCache(event.currentTarget);
}
//HTTP请求错误处理
private static function IOErrorEventHandler(event:IOErrorEvent):void
{
LcmsError.ShowInfo("[服务器连接失败]/r/n" + event.text); //输出错误信息
RemoveCache(event.currentTarget);
}
//创建HTTP请求,并设置必须的参数
private static function CreateRequest(api:String, version:String = "2.0", format:String = "xml"):HttpRequest
{
var http:HttpRequest = new HttpRequest();
http.RootUrl = Url;
http.AddGetParam("api", api);
http.AddGetParam("v", version);
http.AddGetParam("f", format);
var date:Date = new Date();
http.AddGetParam("t", date.time.toString());
return http;
}
}
}
然后,就可以根据业务需要封装所需的接口,下面简单介绍几个接口。
- 登录
public static function Login(name:String, password:String, callbackXml:Function = null):void
{
var http:HttpRequest = CreateRequest("lcms.user.login");
http.AddGetParam("uname", name);
http.AddGetParam("pwd", password);
Invoke(http, callbackXml);
http.Dispose();
}
- 获取用户列表
public static function GetUser(name:String = null, role:String = null, pageNO:String = "1", pageSize:String = "25", callbackXml:Function = null):void
{
var http:HttpRequest = CreateRequest("lcms.user.get");
http.AddGetParam("session", Session);
if (name != null)
http.AddGetParam("uname", name);
if (role != null)
http.AddGetParam("urole", role);
if (pageNO != null)
http.AddGetParam("page_no", pageNO);
if (pageSize != null)
http.AddGetParam("page_size", pageSize);
Invoke(http, callbackXml);
http.Dispose();
}
- 删除用户
public static function DeleteUser(id:String, callbackXml:Function = null):void
{
var http:HttpRequest = CreateRequest("lcms.user.delete");
http.AddGetParam("session", Session);
http.AddGetParam("del_uid", id);
Invoke(http, callbackXml);
http.Dispose();
}
使用WebAPI
- WebAPI初始化设置
private function SessionError():void
{
//跳转到登陆…
}
…
cwn.lcms.WebAPI.Url = "http://server1:2009/LCMS2/api.jhtm";
cwn.lcms.WebAPI.SessionErrorFunction = SessionError;
cwn.lcms.WebAPI.Session = null;
cwn.lcms.WebAPI.CurrentUser = null;
- 登录
private function LoginCallback(xml:Object):void
{
//…
cwn.lcms.WebAPI.Session = null;
cwn.lcms.WebAPI.CurrentUser = null;
if (xml == null)
return;
var user:UserData = new UserData(XML(xml));
//…
cwn.lcms.WebAPI.CurrentUser = user;
if (user.Session != "")
cwn.lcms.WebAPI.Session = user.Session;
//登陆成功跳转…
}
…
cwn.lcms.WebAPI.Login("Hunk", "123", LoginCallback);
- 获取用户列表
function GetUserCallback(xml:Object):void
{
if (xml == null)
return;
_Data.removeAll();//ArrayCollection
for each (var item:XML in xml.item)
{
var user:UserData = new UserData(item);
_Data.addItem(user);
}
//数据绑定
}
…
cwn.lcms.WebAPI.GetUser(null, null, "1", "25", GetUserCallback);
注:设计不唯一,上述是已实践的个案参考。
系列索引
后记
写Blog有一段时间,终于能完成一个完整系列,即使略带稚气,也要好好表彰下自己:-)。