您好,登錄后才能下訂單哦!
如何淺談WebService的版本兼容性設計,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
在現在大型的項目或者軟件開發中,一般都會有很多種終端, PC端比如Winform、WebForm,移動端,比如各種Native客戶端(iOS, Android, WP),Html5等,我們要滿足以上所有這些客戶端的需求,實現前后端的分離,一種最常見的做法是,編寫WebService API來為以上客戶端提供數據。近年來越來越多的企業或者網站支持Restfull方式的WebServiceAPI,比如當當網開源Dubbox,擴展Dubbo服務框架支持REST風格遠程調用,這個是Java版本的,在.NET中ServiceStack天生支持Restfull風格的WebService。以ServiceStack為基礎探討,淺談WebService的兼容性設計。
1.軟件的兼容性
在軟件持續更新升級的過程中,API 也是需要不斷更新,這時就需要考慮客戶端升級以及兼容性的問題。當前有很多用戶可能由于多種原因,尤其是Native用戶,不可能及時升級到***版,所以需要提供對老版本的API的向后兼容。在API設計之初,我們需要考慮一些問題以及解決方法。
后向兼容性(Backward_compatibility),或者向下兼容,是指對于給定的輸入,較老版本的產品或者技術,也能夠輸出相同的結果。如果一個產品或者API在設計之初就能夠為新的標準考慮,能夠滿足接收,讀取,查看舊的標準或者格式,那么這個產品就稱之為后向兼容,比如一些數據格式或者通訊協議,在新版本推出時都會充分考慮后向兼容問題。如果對一個產品的改進破壞了后向兼容性,則稱之為破壞性的改動(breaking change),相信大家都遇到過這種情況。
App長久沒更新,落后很多個版本之后,再次打開改App會提示升級到***版,或者直接幫你強制升級。
使用新版的TortoiseSVN打開老版本TortoiseSVN創建的工程的時候,會提示需要升級項目工程才能打開。
這種情況一般發生在版本的改動比較大,或者對較老版本的支持成本比較大,在這種情況下,一般還需要為客戶提供從老版本遷移到新版本的工具或者解決方案。
兼容性有很多種類型比如 API 的兼容, 二進制dll的兼容性,以及數據文檔的兼容。
關于API的兼容性其實涉及到API的設計。相關文檔和書籍有很多,關于API設計的書可以參考Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (2nd Edition) 和 How to Design a Good API and Why it Matters
本文主要探討WebService開發的API的向后兼容性。
2. WebService 的后向兼容性
在關于開發WebService框架上,這里不免又要談一下WCF和ServiceStack的設計理念和區別。
在ServiceStack中,鼓勵使用Message-based 式的設計,因為遠程服務調用是很耗時,我們應該盡量一次多傳輸需要處理的數據,而避免來回多次調用。在WCF中,通過一些工具,使得開發者能夠像調用本地方法一樣調用遠程方法,這樣會使人產生誤解,實際上調用遠程方法會比調用遠程方法慢上成千上萬倍。ServiceStack在設計之初就受Martine Flowler 的 Data Transfer Object 模式的啟發:
“ When you're working with a remote interface, such as Remote Facade (388), each call to it is expensive. As a result you need to reduce the number of calls, and that means that you need to transfer more data with each call. One way to do this is to use lots of parameters. However, this is often awkward to program - indeed, it's often impossible with languages such as Java that return only a single value. The solution is to create a Data Transfer Object that can hold all the data for the call. It needs to be serializable to go across the connection. Usually an assembler is used on the server side to transfer data between the DTO and any domain objects. ” |
在API的設計上WCF鼓勵將WebService作為普通的C#方法調用,這是一種基于普通的基于PRC 方式的調用。比如:
public interface IWcfCustomerService { Customer GetCustomerById(int id); List<Customer> GetCustomerByIds(int[] id); Customer GetCustomerByUserName(string userName); List<Customer> GetCustomerByUserNames(string[] userNames); Customer GetCustomerByEmail(string email); List<Customer> GetCustomerByEmails(string[] emails); }
以上WebService方法就是通過id,username或者email獲取用戶或者用戶列表。如果使用ServiceStack的基于Message-base風格的API設計,接口就是:
public class Customers : IReturn<List<Customer>> { public int[] Ids { get; set; } public string[] UserNames { get; set; } public string[] Emails { get; set; } }
在ServiceStack中,所有的請求信息都包裝在這個Customers的DTO中,他并不依賴于服務端方法的簽名。最簡單的好處在于使用message-base的設計在于wcf中的任意RPC組合都可以使用一個ServiceStack中的遠程消息組合,并且只需要服務端的一次實現。
閑話說了這么多,現在來看看如何設計WebService的后向兼容性。談到WebService,大家都會想到WCF,關于WCF的后向兼容,在Codeproject上,有人寫了三篇文章WCF Backwards Compatibility and Versioning Strategies(part1,part2,part3),由于ServiceStack僅支持Poco方式的請求參數,并且寫在Poco中的字段都是必須的,沒有WCF 中的對字段的 [DataMember(IsRequired = true)] 和 [DataMember(IsRequired = false)] 來標識字段是否可選,所以WCF支持的RPC方式的參數(Part1文章中的后向兼容算法)ServiceStack中無法做測試,這里對比做Part2文章中的測試。并且測試的時候,測試添加和移除字段對Service調用的影響。
建立測試之前,我們先建立一個基本的ServiceStack程序。 這個程序和前文中介紹一樣,是一個簡單的 ServiceStack序。
3. 基礎
使用ServiceStack創建服務,基本的工程結構有三個。
ServiceModel這一層主要是定義 WebService中的請求參數和返回參數DTO, Employ中的代碼如下:
namespace WebApplication1.ServiceModel { [Route("/Employ/{EmpId}/{EmpName}")] public class Employ : IReturn<EmployResponse> { public string EmpId { get; set; } public string EmpName { get; set; } } public class EmployResponse { public string EmpId { get; set; } public string EmpName { get; set; } } }
代碼定義了請求參數DTO Employ對象,約定了其返回類型為 EmployResponse,這里繼承IReturn< EmployResponse >是為了方便測試。 這里面指定了這個WebService的請求對象是Employ,返回對象是EmployResponse,并且通過’ /Employ/{EmpId}/{EmpName}’這樣的方式來調用服務為Employ對象賦值。
ServiceInterface這一層是服務實現層。里面的EmployServices直接繼承自ServiceStack中的Service對象。
namespace WebApplication1.ServiceInterface { public class EmployServices : Service { public EmployResponse Any(Employ request) { return new EmployResponse { EmpId = request.EmpId, EmpName = request.EmpName}; } } }
這里Any表示這個Restfull請求支持Post和Get兩種方式,請求參數類型Hello和返回值類型EmployResponse在Model中已經定義。我們不關心這個方法的名稱,因為可以通過Rest路由來進行訪問。
WebApplication和ConsoleApplicaiton是ServiceInterface的服務宿主層,我們可以使用ASP.NET 將服務部署到IIS上,也可以通過控制臺程序進行部署以方便測試。
Web宿主很簡單,我們定義一個類繼承自AppHostbase,并提供包含有服務的程序集即可:
namespace WebApplication1 { public class AppHost : AppHostBase { /// <summary> /// Default constructor. /// Base constructor requires a name and assembly to locate web service classes. /// </summary> public AppHost() : base("WebApplication1", typeof(EmployServices).Assembly) { } /// <summary> /// Application specific configuration /// This method should initialize any IoC resources utilized by your web service classes. /// </summary> /// <param name="container"></param> public override void Configure(Container container) { //Config examples //this.AddPlugin(new PostmanFeature()); //this.AddPlugin(new CorsFeature()); } } }
然后,在網站啟動的時候,在Application_Start方法中初始化即可:
namespace WebApplication1 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { new AppHost().Init(); } } }
現在我們就可以通過Web的方式來查看我們創建的service服務:
可以通過Post Http的方式采用Json格式調用WebService服務,比如我們可以構造Json格式,將內容Post到 地址,http://localhost:28553/json/reply/ Employ:
{"EmpId":"p1","EmpName":"zhangsan"}
返回值為:
{"EmpId":"p1","EmpName":"zhangsan"}
或者直接在地址欄里輸入:http://localhost:28553/Employ/p1/zhangshan
不過在開發的時候,我們通常采用***種方式,將參數序列化為json字符串,然后post到我們部署的地址上。
以上是服務端代碼,部署好了之后,客戶端需要進行調用,調用的時候,我們需要引用ServiceModel這里面的請求和返回值實體類型。在部署了WebService之后,我們也可以通過引用WebService的方式來進行引用字段。
新建一個控制臺應用程序,將上面的ServiceModel編譯為dll之后,拷貝到新建的控制臺程序下面,然后引用這個dll,客戶端調用代碼如下,我們采用了Json的方式傳送數據,當然您可以選擇其他的數據格式進行傳輸。代碼如下:
class Program { static void Main(string[] args) { Console.Title = "ServiceStack Console Client"; using (var client = new JsonServiceClient("http://localhost:28553/")) { EmployResponse employResponse = client.Send<EmployResponse>(new Employ { EmpId="1", EmpName="zhangshan"}); if (employResponse != null) { Console.WriteLine(string.Format("EmoplyId:{0},EmployName:{1}",employResponse.EmpId,employResponse.EmpName)); } } Console.ReadKey(); } }
把服務端代碼運行起來之后,然后運行上面的控制臺程序,輸出如下:
EmoplyId:p1,EmployName:zhangshan
4. 測試
4.1 Case 1 添加新字段
現在假設我們v1版本的API中Employ實體只有兩個字段,后來我們發現,在v2版本中,還需要添加一個Address字段,以表示該雇員的地址,于是我們修改了Model,添加了Address字段,在Request和Response中均修改了該字段,現在服務端代碼如下:
namespace WebApplication1.ServiceModel { [Route("/Employ/{EmpId}/{EmpName}")] public class Employ : IReturn<EmployResponse> { public string EmpId { get; set; } public string EmpName { get; set; } public string Address { get; set; } } public class EmployResponse { public string EmpId { get; set; } public string EmpName { get; set; } public string Address { get; set; } } } namespace WebApplication1.ServiceInterface { public class EmployServices : Service { public EmployResponse Any(Employ request) { return new EmployResponse { EmpId = request.EmpId, EmpName = request.EmpName, Address = request.Address }; } } }
然后編譯運行。需要注意的是,客戶端引用的ServiceModel這個dll,依然是之前的老版本的只有兩個字段的dll,現在再次運行客戶端,輸出如下:
EmoplyId:0,EmployName:zhangshan
結果和之前的一抹一樣,這表示,對新的API添加新的字段和在返回值中添加新的字段,不會對就有的WebService產生影響。
4.2 Case 2 :修改數據字段的類型
再后來,在V3版本中,我們發現EmpID應該是一個int型,于是我們將服務端的Employ實體的EmployID從string類型改為了int型,然后運行客戶端,因為在客戶端,我們傳給ID的是string類型的”p1”該類型不能直接轉換為int型,真實的輸出的結果是:
EmoplyId:0,EmployName:zhangshan
沒有報錯,但是不是我們期望的結果。客戶端將EmpolyID字段傳”p1”過去的時候,服務端該字段類型已經變更為了int,”p1”沒有轉換為int型,所以會使用int的默認初始值0代替。
4.3 Case 3:移除必要字段
現在我們編譯一下ServiceModel,然后拷貝到ServiceClint更新一下客戶端的dll引用,這樣客戶端就能夠獲取到Address這個字段了。如果是WebService的話,直接更新一下引用就可以。現在我們修改客戶端,請求的時候為Address賦值。
static void Main(string[] args) { Console.Title = "ServiceStack Console Client"; using (var client = new JsonServiceClient("http://localhost:28553/")) { EmployResponse employResponse = client.Send<EmployResponse>(new Employ { EmpId="p1", EmpName="zhangshan",Address="sh"}); if (employResponse != null) { Console.WriteLine(string.Format("EmoplyId:{0},EmployName:{1},Work at:{2}",employResponse.EmpId,employResponse.EmpName,employResponse.Address)); } } Console.ReadKey(); }
可以看到這是客戶端已經更新EmpId已經是int型了,如果在傳p1的話,會報錯。現在編譯運行,輸出結果應該是:
EmoplyId:1,EmployName:zhangshan,Work at:sh
現在,在V4版本中,我們發現v2版本中添加的工作地址Address這個字段不應該放在Employ中,所以在服務端將該字段移除,并進行了重新部署。客戶端端再次運行,結果如下:
EmoplyId:1,EmployName:zhangshan,Work at:
可以看到,服務端去除了Address字段后,服務端返回的原始數據中缺失Address元素,客戶端在反序列化的時候找不帶該字段就賦值為了null。
5. 總結
如果使用ServiceStack,在API的進化過程中,新版本的API可能較老版本的API會有如下修改:
添加,移除,或者重命名字段。
將字段的類型從int轉換為了double或者long,這種可以隱式轉換的類型。
將集合類型從List改為了HashSet。
將強類型集合改為了松散類型的List集合改為了松散的List里面的元素為Dictionary 。
添加了新的枚舉類型。
添加了可空的字段。
在客戶端序列化實體后,傳到服務端的時候,序列化工具會自動的處理以上類型的變更和不一致,如果字段對應不上,或者類型轉換不過去,則會使用服務端的字段的類型默認值替代。
對于API的兼容性策略,有以下幾個注意點:
5.1應該對現有API版本的進化來解決,而不是重新實現一個
僅添加了一個新字段的參數。因為,如果要同時維護多個不同版本的,但是實現相同功能的API可能會使得工作量巨大,而且容易出錯。在編寫***個版本的API之初,就應該遵守這一約定。同時編寫多個版本的實現相同或相似功能的API違反了DRY原則。
5.2要充分利用自建的序列化工具的版本功能優勢
一些序列化工具會在字段對應不上的時候,給字段附上改字段類型的默認值;能夠在相同結構的集合類型之間進行自動轉換;能夠進行類型的隱式轉換等等。比如,如果一個id在較老的api中使用的是int類型,那么在新版本中,我們可以直接將其更改為long類型就可以向后兼容。
增強現有服務對變化的防御性
在WCF中,使用DataContract可以自由添加或者移除字段而不會產生breaking change。這主要是在于其實現了IExtensibleDataObject接口。在返回類型的DTO對象上,如果實現該接口,也能實現該功能。在兼容舊版本的DTOs的時候,要做好充分測試,一般的:
不要修改已存在字段的數據類型,如果確實需要修改,添加另外一個屬性,并且根據具有的字段來判斷版本。
防御性編程,要判斷在老版本客戶端中那些字段可能不存在,所以不要強制認為一定需要存在。
保證只有一個唯一的全局命名空間。這個可以在程序的AssemblyInfo.cs中處理,一般的我們定義一個公共的AssemblyInfo,然后在各個DTO項目中,進行鏈接引用。
5.3 在DTO中添加版本控制信息
這個也是最容易想到和實現的。在大多數情況下,如果使用防御性編程的思想并且對API進行平滑演進的話,通常可以根據數據來推斷出客戶端的版本。但是在一些特殊情況下,服務端需要根據客戶端的特定版本來處理相應,因此可以在請求DTO中添加版本信息。舉例如下:
比如在最初發布Empoly這個服務的時候,沒有多想直接定義了下面這個請求的DTO:
class Employ { string Name { get; set; } }
然后由于某些原因應用的UI發生了變化,我們不想給客戶返回這個籠統的Name屬性,需要追蹤客戶端使用的版本,來考慮返回那個值,于是DTO修改為了如下,并且添加了新字段DisplayName和Age:
class Employ { Employ() { Version = 1; } int Version; string Name; string DisplayName; int Age; }
然而,經過會議討論,發現DisplayName仍然不太好,***能夠把它拆分成兩個字段,并且存儲Age不好,應該存儲DateOfBirth 于是我們的DTO變成了這樣:
class Employ { Employ() { Version = 2; } int Version; string Name; string DisplayName; string FirstName; string LastName; DateTime? DateOfBirth; }
到目前位置,客戶端有三個版本,他們給服務端發送請求如下:
V1版本:
client.Send<EmployResponse>(new Employ { Name = "zhangshan" });
V2版本:
client.Send<EmployResponse>(new Employ { Name = "zhangshan" DisplayName="Ms Zhang", Age=22 });
V3版本:
client.Send<EmployResponse>(new Employ { FirstName = "zhang" LastName="shan", DateOfBirth=new DateTime(1992,01,01) });
現在,服務端可以統一處理以上三個版本客戶端的請求:
public class EmployServices : Service { public EmployResponse Any(Employ request) { //V1: request.Version == 0; request.Name = "zhangshan"; request.DisplayName == null; request.Age = 0; request.DateOfBirth = null; //v2: request.Version == 2; request.Name == null; request.DisplayName == "Foo Bar"; request.Age = 18; request.DateOfBirth = null; //v3: request.Version == 3; request.Name == null; request.DisplayName == null; request.FirstName == "Foo"; request.LastName == "Bar"; request.Age = 0; request.DateOfBirth = new DateTime(1994, 01, 01); //..... } }
5.4 要遵循”最小化”原則
***一點應該是在實踐中的經驗總結。和我們再SQLServer中寫查詢條件一樣,千萬不要圖方便使用select * 來代替要用到的需要手動輸入的字段,除去效率原因,因為一旦字段查詢的字段順序發生變動,可能就會影響到解析。在設計DTO的可選值的時候,也需要考慮這樣的問題。這里舉一個例子:
比如我們要設計一個查找附件商戶的API,該API支持查找附件的酒店,餐飲以及不限。所以我們一般會在DTO中定義一個表示查找范圍的字段SearchScope,并定義一些枚舉值。
0-不限 1-酒店 2-餐飲
這里需要注意的是,如果我們在***個版本中,一開始就使用0表示不限的話,在實現中,比如在SQL語句中,我們通常會不會查詢的條件作出限制,這樣就正常的發出了***個版本。
然而,在第二個版本中,我們添加的對附近娛樂場所的支持,并且開發了對娛樂場所搜索結果的特殊頁面支持。于是,自然而然的考慮到在SearchScope中添加一個枚舉值表示娛樂,并且在DB也增加了對娛樂場所信息的存儲。
3- 娛樂
然后V2版本的接口順利發布了。這個時候,如果DTO中沒有版本信息,問題就來了,在V1版本中,當用戶搜索的時候,如果選擇不限,那么搜索結果中就會出現 “娛樂” 相關信息,但是點擊搜索出來的”娛樂”的結果的時候,其他頁面在V1版本的時候,沒有做相應的頁面處理。所以就會出現一些問題。所以發布V2版本的API的時候,需要修改V1版本的處理邏輯。
所以當初在設計V1版本的API 的時候,對于條件或者組合不太多的情況,對于”不限”這種場景,我們應該是傳所有支持的類別,比如傳”1,2”而不是”0”。或者在設計范圍的時候,設計成可以進行或”|”方式的枚舉值,比如設計成 0-不限,1-酒店,2-餐飲,4-娛樂等。這樣新版本發布后對叫老版本的API影響比較少。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。