侯 杰,王 靜,2,王樂琪
1(上海海洋大學 信息學院,上海 201306)2(農村農業部漁業信息重點實驗室,上海 201306)
E-mail:wangjing@shou.edu.cn
隨著智能手機、平板電腦等移動設備的普及,各類移動應用的發展愈加繁榮[1].與此同時,移動應用的體積越來越龐大,通過網絡接口獲取數據是移動應用展示內容的主要來源[2,3],因此移動應用中的網絡通訊模塊也變得越來越重要.由于移動應用與網絡接口的交互在移動應用開發中具有重要地位,并且在移動應用程序中,網絡接口請求的代碼塊非常多而且所在位置比較離散,所以移動應用和網絡接口的交互模塊在編寫和調試上具有較高的復雜度,影響著移動應用的開發效率.
目前存在很多成熟的框架可用于實現移動應用與網絡接口交互的開發,例如在安卓開發中可以采用Retrofit請求框架,后臺則常采用Struts2,SpringMVC等框架[4,5].這些框架對于移動開發中的網絡通訊模塊已經給出了可行的解決方案.在后臺服務器編寫接口,移動應用程序中編寫請求.對于每一個移動端的網絡接口調用,在服務器端對應已編寫好的url,實現對應請求參數的處理代碼模塊.這種明確的分工使得移動應用開發中的接口調試與協同開發變得困難和復雜.在沒有開發網絡接口之前,移動端開發時的UI顯示只能使用假數據填充,之后再改為網絡接口請求來的數據,并調試接口數據的適配問題,其中開發出錯率,以及開發復雜度都顯著提高.問題在于服務器端和移動端在開發模塊實現隔離的同時對交互數據也進行了隔離,因而難以實現協同開發.因而,涉及網絡請求的移動應用開發不能僅考慮移動端,如何讓移動端和服務器端相輔相成,配合恰當,在簡化移動端的同時可以讓服務器端也方便的開發接口,實現協同開發,從而降低開發復雜度,讓移動端和服務器端具有數據一致性的同時也具有模塊隔離性,是目前移動應用中網絡通訊模塊設計與開發面臨的主要問題.
本文采用面向對象思想對移動編程通訊接口設計,在服務器端引入指令驅動模型,通過JSON格式的請求指令實現請求數據和返回數據端到端的處理,使移動端與服務器端具備數據一致性,從而提高移動端與服務器端的協同開發效率,并且提高了應用程序的可擴展性和測試效率.進一步基于此架構,以Android 應用為例介紹具體開發過程和效果.
移動應用網絡接口開發是典型的前后端分離式的開發,移動應用想要訪問服務器資源,需要通過HTTP協議向指定的服務器傳遞信息,服務器根據應用請求調用相應的接口,并返回相應的結果.整個流程所涉及的操作為移動端定義請求接口方法,創造請求數據,填充URL和數據,向服務器發送請求,服務器調用指定URL的網絡接口處理請求,返回請求結果,移動端根據請求結果刷新UI.移動應用網絡接口的通用設計模式如圖1所示.

圖1 通用設計示意圖Fig.1 General design diagram
在這種設計模式下,移動端與服務器端交互時傳遞的數據較為復雜,并且特定URL的請求數據和返回數據必須事先約定.然而,請求數據和URL之間卻沒有明確的聯系,如果在發送請求時攜帶了錯誤的請求數據,移動端將不能接受到正常的返回結果.本模式中存在的另一個問題是接口對請求數據的邏輯處理耦合在接口之中,即使是使用MVC架構將邏輯處理模塊獨立起來,邏輯處理模塊所處理的數據仍然需要通過上層數據接收層傳遞給它,并沒有把網絡請求的發送與接收完全屏蔽,無法真正實現移動端像調用函數一樣調用接口,無法達到移動端與服務器端數據的一致性.因而,本文提出基于指令驅動模型對移動應用通訊接口的整個架構進行優化,確保請求數據與URL之間具有明確的聯系,而且能夠實現數據傳輸與數據處理的隔離.
本文提出的編程架構共分為四個模塊,構造請求指令,指令發送器,指令接收器,指令驅動模型,其中構造請求指令和指令驅動模型是一組,指令發送器和指令接收器是一組.整個流程如圖2所示.

圖2 整體架構設計Fig.2 Overall architecture design
圖2中:
標注1,2,3:將需要發送給服務器的數據構造為指令
標注4,5,6:將服務器返回的數據用于刷新UI界面
標注7,8:客戶端與服務器之間傳遞指令和返回的數據
標注9,10:將指令送給指令驅動模型處理,得到返回值
標注11,12:服務器端與數據庫交互部分
移動應用要訪問服務器,需要通過HTTP協議向服務器發送信息,由于JSON格式[6,7]在傳輸效率上優于其它數據傳輸格式,因此本文在設計接口架構時,使用JSON數據格式來傳遞信息.網絡接口通過URL來訪問,URL可以視為一個導向,移動端在請求URL時需要攜帶相應的請求數據,通常一個URL對應著固定的請求數據和返回數據.由于URL與傳輸數據具有這種強關聯的關系,本文把URL視為操作碼,將URL與傳輸數據抽象為一個整體,即指令,把指令視為網絡請求傳輸的基本數據單位,指令包含了URL和傳輸數據所表達的信息.
在移動端構造請求指令后,通過指令發送器發送到服務器,服務器通過指令解析器解析出具體的指令,并把指令交給指令驅動模型處理,得到指令驅動模型的返回結果并返回給移動端.其中指令發送和指令解析是網絡數據傳輸過程,它們的任務就是傳遞指令,而與具體的URL和請求數據無關,這樣可以屏蔽掉網絡傳輸過程,讓程序的編寫和調試更加方便,也方便將請求接口函數化.指令驅動模型是服務器端與網絡傳輸無關的運行單元,在移動端構造的請求指令是可以直接用于對應指令驅動模型的,這就滿足了數據一致性,讓移動應用調用接口如同調用本地函數,這給程序的調試帶來了極大的便利.在這種模式下,當需要修改功能或者是添加新功能時,只需要修改指令驅動模型就可以完成,極大的提高了程序的可擴展性.
在整個網絡接口架構中,指令驅動模型和構造請求指令是一套完整的體系,獨立在網絡傳輸之外,如果部署到同一臺設備仍然可以執行,構造的請求指令就是要投入指令驅動模型中來得到返回結果,本文采用面向對象的思想對其進行設計.
在大多數需要與數據庫交互的程序中,基本上都是把數據庫的每張表看作一個類,表里的每一行看為一個對象,正是由于這種面向對象的思想,在程序與數據庫交互方面誕生了不少優秀的框架[8,9].而移動應用向服務器發送請求,最終也是對服務器數據庫進行一定的操作,這些操作是面向數據庫表的抽象數據類型的,因此移動應用的網絡請求可以根據具體操作的抽象數據類型來劃分,所以指令的操作碼部分應該包含類和對類的操作,指令的數據部分可能有很多參數,為了更好的組織代碼,提高代碼質量,對于操作碼中的每個操作,指令中都需要有對應所有參數的抽象數據類型.指令結構如圖3所示.事實上,對一個類的操作并不會很多,而且可能只是增刪改查,所以很多指令的操作數部分只需要類本身對象就足夠了.如果嚴格按照每個操作對應的參數創建抽象參數對象,在移動端編程時填充指令對象就會方便很多,且不易出錯,這在一定程度上可以提高開發效率,提高系統可擴展性.

圖3 本文提出的指令格式Fig.3 Instruction format proposed in this paper
指令驅動模型是從面向對象的思想出發,它需要實現的功能是能夠執行相應的指令,返回執行結果.所謂指令驅動,就是模型內部的執行過程是靠指令來驅動的.指令驅動模型是處理請求指令的核心,所以模型在使用時應該具有良好的可擴展性,在設計指令驅動模型時需要充分考慮到程序是否具有低耦合高內聚的特性.
本文提出的指令驅動模型具有處理請求指令的功能,而對請求指令的處理是通過指令的操作碼實現對指令操作數的處理.由于設計指令時,指令能夠通過指令要操作的類進行劃分,指令驅動模型就可以根據這一特點,分析指令的操作碼然后把指令交到具體類的操作對象,由具體的操作對象對指令的操作數處理并返回處理結果.所以用于處理操作數的對象也會由類來劃分,這樣就會保證程序的可擴展性.當然,具體類的操作對象需要注冊到指令驅動模型中,以保證指令驅動模型在收到指令后可以正常運作.
因此,在驅動模型中需要一個具體操作對象的管理器來管理這些對象,在收到指令后可以分發給具體的操作對象去處理.由于指令是有格式的,具體的操作對象也具備一定的格式才能正常處理對應的指令.因此不是任何一個對象都可以通過指令驅動模型處理,也不是任何一個對象都可以注冊到指令驅動模型中作為具體操作對象.因此,本驅動模型中還具備一個分析器,不僅可以實現注冊操作對象時對操作對象的分析,而且能夠完成模型執行指令時對指令的分析,從而保證模型的健壯性.另外,本模型具備異常處理模塊,因開發時因為錯誤的使用模型而使程序不正常運行,能夠報出異常,并可以提示異常原因,從而提高開發效率.
在使用指令驅動模型編程時,只需要編寫不同類別的具體操作對象然后注冊到模型中.當然,操作對象必須可以處理相應指令,而指令和操作對象的關系就如函數的形參與函數的關系.因而,在本文設計的框架下,指令處理后的返回結果是任意的.如果把操作對象類比于函數,這里的返回結果任意性不是說函數的返回值不需要定義,而是可以將返回值定義為任何類型,也即是對于同一個操作碼,操作對象可以把返回值定義任何類型,在將結果返回時都可以正常接收返回數據.這是由于,如果調用者需要調用模型處理一條指令,在調用模型之前它是知道需要的返回結果類型的.操作對象返回結果的任意性可以提高工作效率,簡化代碼,減少不必要的前后臺數據協商.
在網絡數據傳輸中,本文所設計的指令發送器和指令接收器只需要完成指令的前后臺傳遞,具有傳輸JSON數據的功能.

圖4 網絡數據傳輸流程Fig.4 Transmission process of network data
在移動應用工程中,出于網絡通訊安全性的考慮,往往需要對指令進行加密,而且操作數具有不同的特性,如圖片資源和普通參數,因此需要對指令進行分類,此分類不同于將指令按操作碼的類別劃分,前者相當于指令的標簽,后者是指令中操作碼具有的性質.因此本研究在設計網絡數據傳輸時,需要指令發送器可以區分指令的標簽,根據指令的標簽信息對指令進行相應的操作,在向服務器發送指令時,攜帶著指令的標簽信息,構造擴展指令,方便服務器對指令的正確解析.依據擴展指令可以完成指令的發送和接收,然而在返回數據的傳輸方面,就會略顯復雜,由于一條指令的返回結果具有任意性,這就給返回數據的接收帶來了挑戰,為了方便返回數據的接收和解析,可以對指令好返回數據統一化處理,都采用擴展指令的形式封裝.事實上,指令和返回數據都是一種抽象數據類型,因此可以統一化處理,使用統一的擴展指令來傳遞,使網絡數據傳輸過程仍然具備前后端分離的特性.擴展指令由附加信息和指令/返回數據構成.
本文設計網絡數據傳輸的流程為指令發送器分析指令標簽,生成擴展指令發送到指令接收器,指令接收器解析出指令并傳遞到指令驅動模型,指令接收器拿到對應指令返回數據后生成擴展指令返回給指令發送器,指令發送器解析出返回數據.統一化處理指令和返回數據,屏蔽了網絡傳輸過程,提高了開發效率和程序的可擴展性,并且給程序的調試帶來了便利.網絡數據傳輸流程如圖4所示.
Java是一個廣泛使用的面向對象的網絡編程語言,其具有良好的可移植性,跨平臺性,安全性,被廣泛應用在各種場景的程序開發[10],本文使用Java編程語言面向Android應用實現基于指令驅動模型的移動應用通訊接口架構.
指令驅動模型是本文設計架構的基礎功能模塊,因此在實現架構前需要先實現指令驅動模型.指令驅動模型模塊需要實現的是指令的實現和驅動模型的實現.指令包含操作碼和操作數,操作碼可以根據要操作的實體類進行劃分,故把操作碼設計為“實體類.具體操作”,對同一個實體類操作的指令使用同一個指令類.在用Java實現時,對于每一個實體類都編寫一個對應的指令類并統一命名規范,指令類命名為“要操作實體類加后綴Come”,操作碼字段使用request.操作對象要對指令對象操作,需要有對應指令對象的屬性,把操作對象統一命名“實體類+Actor”.
指令驅動模型可以根據指令對象的request的實體類標識找到具體的操作對象,根據request的具體操作標識找到具體的操作方法,進而處理指令和返回結果.request字段與操作對象其中的方法存在映射關系,如圖5所示.驅動模型根據映射關系正確的處理指令,可以使用Java編程語言的高級特性-注解[11,12]和反射編程.通過在Actor類中添加相應的注解說明Come和Actor之間的映射關系.

圖5 Come指令和Actor的映射關系Fig.5 Mapping relationship between come instruction and actor
通過定義注解(@Come,@Actor,@Action)然后使用在Actor類中,就可以通過類似與user.register來定位到UserActor中的register方法.當然UserActor需要提前注冊到驅動模型中.
定義指令驅動模型的控制器為Context類,實現為單例模式,Context類的方法應該有addActor():向Context注冊Actor類,getComeClass():通過Come對象的類名獲取類的字節碼對象,這個方法主要是和利用JSON解析指令相關,showWorks():此方法用于顯示出注冊到Context中的Actor類的所有指令到處理方法的映射路線,back(Come),此方法用于對接收Come對象并產生返回結果.
分析器作用是分析Come對象中request字段是否符合規范,以及注冊到Context的Actor類是否符合規范.如果不符合規范,交由異常處理模塊進行相應處理.操作對象管理器負責實例化Actor對象,并將收到的Come指令填充其中的Come屬性,根據Come指令的request字段調用@Action對應的方法,如果Actor對象中含有該方法返回值類型的引用,調用方法前會把返回值的引用實例化.
每個操作碼對應這一種返回數據類型,但是開發人員在使用驅動模型時是不希望總是寫強制類型轉化的,因此在實現Context的back()方法時,通過利用Java的泛型編程消除強制類型轉化步驟.
測試指令驅動模型:
public class TestCome {
private String request;
}
@Actor(name = “user”)
public class TestActor {
@Come
TestCome come;
TestBack testBack;
@Action(name = “test”)
TestBack getBack(){
testBack.setData(“Hello World!”);
return testBack;
}
@Action(name = “testList”)
List
List
return list;
}
}
//注冊TestActor
Context context=Context.getContext();
context.addActor(TestActor.class);
context.showWorks();
TestCome comeData=new TestCome();
//測試返回TestBack類型
comeData.setRequest(“user.test”);
TestBack backData=context.back(comeData);
//測試返回list類型
comeData.setRequest(“user.testList”);
List
在實現指令和返回數據的傳遞方面,擴展指令的定義非常重要,擴展指令包含附加信息和指令/返回數據,附加信息很少,只需要定義幾個字段即可,但是不同的指令和數據具有不同的數據類型,如果在擴展指令類中一一定義,會使擴展指令對象變的極為復雜,而且影響程序的可擴展性.所以本文實現的擴展指令對象中的指令或數據屬于字符串類型,字段定義為data,用來存放JSON格式的指令或數據,這樣就可以對指令和數據進行統一化實現.指令接收器接收到擴展指令后需要對指令反序列化,解析成Java對象,因此在擴展指令的附加信息中需要有指令類的類名,指令接收器就可以調用Context對象的getComeClass拿到指令的字節碼,進而將其解析為Java對象.本文把擴展指令定義為Body類,字段包括name,data和key,其中name是指令或返回數據的類名,data是指令或返回數據的JSON格式字符串,key用于請求驗證,擴展指令的傳遞同樣需要JSON數據傳輸格式.
指令接收器用servlet實現,定義一個servlet接收JSON字符串形式的Body,定義一個數據處理類,用于JSON解析和數據加密.指令接收器只負責將擴展指令Body中的Come通過數據處理對象解析出來,交給指令驅動模型處理,拿到返回數據再通過數據處理對象包裝成Body作為返回體.數據處理對象用來將數據對象化,進行token認證,數據加密和解密處理[13].這種模式下只需要創建一個servlet即可,具體的指令處理與指令接收隔離開,指令驅動模型與指令接收器是隔離的,可以極大的提高程序的易讀性和可擴展性.
對于指令發送器,本文采用Retrofit封裝網絡請求模塊,讓指令發送器負責將指令封裝為擴展指令Body,對其發送與接收,采用異步的請求處理方式,使用谷歌的Gson進行JSON數據解析.在具體請求代碼塊中構造請求指令,向指令發送器傳遞Come對象并接收返回對象,并且收到返回數據后可以通過doSucces和doFailure方法刷新UI界面.同樣的,指令發送器也利用數據處理對象將指令轉化為Body發送或者將接收到的Body中的返回數據轉化為Java對象,同時需要根據Come指令的類別(比如需要加密)對指令進行相應的處理.

圖6 整體架構的實現Fig.6 Implementation of overall architecture
指令發送器和指令接收器交互只用Body進行,因此指令發送器只需編寫一個請求方法,這簡化了網絡請求編寫的復雜度.數據處理對象與指令接收器中的數據處理對象類似,實現數據對象之間的解析轉化,其中將返回Body中的放回數據解析成具體對象是一個難點,因為由JSON類型字符串解析成Java對象,需要有Java對象的字節碼對象或者對象類型,如果接收一個帶有泛型的Map或者List,運行時泛型就會被擦除,導致無法正確解析到想要的對象,針對此問題,本文通過Gson的TypeToken獲取具體對象的Type,Type type = new TypeToken
調用請求只需要構造Come指令,交給指令發送器,就可以獲得返回對象,進而進行相應的處理,處理的方法也是在UI線程中的,可直接刷新界面.這樣一來,就完成了Come指令到返回數據端到端的處理過程,提高程序的可讀性和開發效率.
在定義好擴展指令Body類,指令發送器,指令接收器之后,就在應用與服務器之間生成了一條數據傳輸線,在編寫請求時,只需要面向指令驅動模型進行編寫.測試時在數據傳輸線沒有錯誤的情況下,只需要對具體請求塊和指令驅動模型進行測試,具體請求塊的測試屬于客戶端獨立的測試,指令驅動模型屬于服務器端獨立的測試,從而可以提高測試效率.整體架構的實現如圖6所示.
具體請求塊:使用一個抽象類繼承GSON的TypeToken,使之具有getType方法,用于獲取真正的類型.
public abstract class DoBack
protected DoBack(Object object){
Api.getApi().go(object,this);
}
public abstract void doSuccess(T t);
public abstract void doFailure();}
指令發送器:使用Retrofit框架定義網絡請求接口傳輸Body的JSON字符串:
@FormUrlEncoded
@POST(“ActionService”)
Call
request(@Field(“data”)String body);創建Api類定義Retrofit的HttpClient,JSON參數解析等基本配置,getApi()得到單例對象Api,go(object,this)是請求方法,object是Come對象,用DoBack本身作為參數,用于準確接收想要的返回類型,配備返回數據的處理方法.go方法定義如下:
public
String data= BCB.createBodyString(t);
Call
call = getOwnApi().request(data);call.enqueue(new Callback
(){@Override
public void onResponse(Call
call,Response response){if(response.body()!= null){
Object obj=BCB.ComeOrBack(response.body(),doBack.getType());
if(obj == null)
doBack.doFailure();
else
doBack.doSuccess(obj);
} else {
doBack.doFailure();
}
}
@Override
public void onFailure(Call
call,Throwable t){doBack.doFailure();
}
});}
BCB是定義的數據處理對象,可以看到傳入doBack的doSuccess()方法的obj是解析好的返回數據對象,因此在具體請求塊就可以直接利用返回數據對象進行相應的刷新頁面.
指令接收器:
data=req.getParameter(“data”);
body=BCB.Body(data);
if(BCB.checkTime(body)==false){
onDefeat();
return;
}
String result=
BCB.createBodyString(
context.back(
BCB.ComeOrBack(body,context.getComeClass(body.getName()))));
if(result==null){
onDefeat();
return;
}
writer.write(result);
其中context是指令驅動模型的控制器,先調用getComeClass()得到傳到指令接收器的Come指令對象的字節碼文件,調用BCB解析出Come對象,調用context.back()得到返回數據對象,進而包裝為Body的JSON字符串返回.
樣例APP實現user的注冊登錄,登錄成功顯示新聞列表,數據庫連接使用Hibernate框架,user表字段uid,username,password.news表字段 nid,title,content.IDE使用AS和eclipse.
定義UserActor和NewsActor,注冊到指令驅動模型.兩個Actor面對的Come都是UserCome,UserCome中只有一個request字段和一個User對象.UserActor對應的指令是user.login和user.register,NewsActor對應的指令是news.getList.編寫APP頁面并加入具體請求塊,用于注冊的具體請求塊如下:
User user=new User();
user.setUsername(username.getText().toString());
user.setPassword(password.getText().toString());
UserCome userCome=new UserCome();
userCome.setRequest(“user.register”);
userCome.setUser(user);
DoBack
@Override
public void do Success(String s){
T;
}
@Override
public void doFailure(){
F;
}
};
獲取News的具體請求塊如下:
userCome.setRequest(“news.getList”);
DoBack> doBack1=new DoBack
>(userCome){
@Override
public void doSuccess(List
T;
}
@Override
public void doFailure(){
F;
}
};
登陸模塊與此類似.指令驅動模型編寫在Actor中使用Hibernate進行數據庫交互.例如user.register和news.getList如下所示:
@Actor(name = “user”)
public class UserActor {
@Come
UserCome userCome;
@Action(name = “register”)
String register(){
User user=
HibernateUtil.getobj(User.class,“username”,
userCome.getUser().getUsername());
if(user==null){
HibernateUtil.save(userCome.getUser());
return “注冊成功”;
}else {
return “用戶名已存在”;
}
}
}
@Actor(name = “news”)
public class NewsActor {
@Come
UserCome userCome;
@Action(name = “getList”)
List
List
HibernateUtil.listByHql(“from News”,null);
return list;
}
}
捏造UserCome對登錄、注冊和獲取新聞列表接口測試,測試結果如圖7所示.

圖7 部分測試結果示意圖Fig.7 Illustration of some testing results
采用本文提出的框架進行開發,開發測試和擴展過程歸約到對指令驅動模型開發測試和擴展,屏蔽了網絡傳輸過程,提高開發效率.如果采用Retrofit(請求框架)+Struts2或SpringMVC(后臺框架)搭建應用框架,則需要對每個請求接口編寫請求方法,在請求時指向URL并攜帶參數,后臺編寫對應的Action類或Controller類來接受參數并進行處理.使用Struts2框架或者SpringMVC框架,前端后臺是通過網絡傳輸這一過程進行分割,通過URL傳遞參數進行溝通,這無疑會使開發過程脫離不了網絡傳輸,在編寫調試方面都會提高復雜度.表1給出了各框架在開發模式、編碼、測試、維護與擴展進行理論上的比較,并總結了應用各框架的開發復雜度和各框架的作用.

表1 各個框架詳細對比Table 1 Comparison with some common frameworks
在開發模式上,本文將URL和參數抽象為指令,結合指令發送器和接收器屏蔽了網絡傳輸過程,使接口的請求僅依賴指令驅動模型,這給編碼測試和擴展提供了便利.
本文提出了基于指令驅動模型對移動編程通信接口設計,把整個通信過程分解為構造請求指令,指令發送器,令接收器,指令驅動模型,進而產生了指令到網絡傳輸到指令驅動模型的開發框架,通過請求指令化,在保證模塊隔離性的同時也保證了前后臺數據的一致性,指令驅動模型成為實際數據交互的載體,保證程序的擴展性,提高測試效率.這種架構可以屏蔽具體網絡傳輸,降低數據耦合度,簡化移動端和服務器端的代碼編寫,根據該架構開發基于Android系統的 APP應用并對比了各個框架的使用情況,驗證了此架構的有效性和實用性.
在請求指令化后,指令就成為移動應用請求的基本數據單元,移動端的請求被抽象為一條條指令的發送.通常在移動應用中,一個界面可能具有很多可能要發送的指令,而且指令具有一定的邏輯關系.因此,可以在本文提出的編程架構基礎上進一步研究指令的對應概念,如指令的順序執行,分支結構和循環結構,以適應具體開發場景.