閱讀目錄:
- 1.開篇介紹
- 2.ASP.NET Routing 路由物件模型的位置
- 3.ASP.NET Routing 路由物件模型的入口
- 4.ASP.NET Routing 路由物件模型的內部結構
- 4.1UrlRoutingModule 物件內部結構
- 4.2RouteBase、Route、RouteCollection、RouteTable 路由核心物件模型
- 4.3RouteValueDictionary、RouteData、RequestContext 路由資料物件模型
- 4.4IRouteHandler 、IHttpHandler兩個介面之間的關係
- 5.UrlRoutingHandler 物件內部結構及擴充套件應用
1】開篇介紹
這篇文章讓我們愉快的學習一下ASP.NET中核心的物件模型Routing模組,為什麼說愉快呢,因為Routing正是建立在大家都比較熟悉的ASP.NET管道模型基礎之上的,所以相比其他一些陌生的概念會輕鬆很多,不過不要緊一回生二回熟;
ASP.NET Routing 系統是一切通過ASP.NET進行Uri訪問應用程式的基礎(並非物理檔案的直接對映);隨著Routing的出現,我們的WEB設計已經和以前大不一樣;越來越輕量級、簡單化,都通過簡便的Uri資源的方式進行處理,將精力放在業務的設計上;現在主流的Rest ful api 也都是建立在這樣的一種機制下的,然而我們的ASP.NETMVC也是一種通過獨立的Uri進行程式訪問處理的框架,所以也是建立在ASP.NET Routing;再者就是現在也比較熱門的ASP.NET技術(ASP.NETWEBAPI);都是建立在Routing框架之上,可見它還是蠻重要的;
所以這篇文章讓我們來分析一下Routing的工作原理,它為什麼能在不影響現有框架的基礎上提供這麼好的擴充套件性,真的讓人很想去一探究竟;目前非常可觀是我們都瞭解ASP.NET現有的框架知識,我們大概瞭解它肯定是在ASP.NET管道模型的哪個位置進行了相應的攔截;
下面我們帶著這個重要的線索來一點一點弄清楚它是如何為其他框架做支撐的,我最疑惑的是它是如何將WebPage和MVC進行很好的區分的 ,最關鍵的是它如何做到只提供一個介面讓後續的相關框架都能基於這個公共的Routing介面進行擴充套件的,它的物件模型肯定很巧妙;我們需要去搞懂它,才能有信心去繼續我們的ASP.NET相關框架的後續學習;
注意:全文使用Routing一詞替代ASP.NETRouting一詞,特此說明,以免概念混淆;
2】ASP.NETRouting路由物件模型的位置
問到ASP.NET最重要的擴充套件點在哪裡?我想我們都會異口同聲的說:在管道模型上,這也符合我們對此問題求解的一個基本思路;ASP.NET管道模型大家都懂的,在管道模型的相關事件中只要我們定義相關的事件就可以在管道的處理中插入自己的邏輯在裡面;管道的最後執行介面是IHttpHander型別,只有阻止原本預設的IHttpHander介面建立才有可能改變整個的處理流程;
圖2.1:
那麼Routing只有在阻止IHttpHander介面的建立前先執行,才能扭轉整個處理路線的機會,上圖中顯示的Application Event(2)(IHttpHander執行)意思是說只有在IHttpHander執行前的某個Application Event中進行Routing的執行才能在原本執行IHttpHander的地方執行其他定製的IHttpHander;而IHttpHander是ASP.NET框架的最終執行的介面,所以如果要想改變原本執行Page的Hander,需要提供自定義的IHttpHander介面物件;
換句話說,一切的執行入口其實在IHttpHander.ProcessRequest()方法中,但是現在矛盾的是ASP.NET Routing 卡在中間,它讓原本直接的處理流程變的有點撲簌迷離,它隔開了“ASP.NET基礎框架 " 與 "基於ASP.NET的應用框架 "(如:ASP.NETMVC\ASP.NETWEBAPI\自定義框架);
注意:“ASP.NET基礎框架”指ASP.NET本身的框架可以理解為傳統的WEBFROM;而“基於ASP.NET的應用框架”是指基於ASP.NET基礎框架而設計的如:MVC\WEBPAGE\WEBAPI之類的上層輕量級應用框架;
圖2.2:
其實這幅圖很明瞭的表示式了ASP.NETRouting的位置,它是用來為ASP.NET與ASP.NETMVC、ASP.NETWEBAPI承上啟下的關鍵紐帶;根據上面我們的分析思路,Routing是ASP.NET框架直接互動的物件模型,所以站在ASP.NET的角度它是不知道背後究竟發生了什麼事情,其實ASP.NETRouting已經在ASP.NETApplication某個生命事件中將原本的建立邏輯移花接木了;
3.】ASP.NETRouting路由物件模型的入口
Routing起到中間人的作用,將ASP.NET的相關邏輯透明包裝,我們雖然能在Routing的上層同樣可以使用相關的ASP.NET物件,但是概念已經發生了根本上的變化;我們可以隨意的引入自定義的IHttpHander實現類,根據前端傳過來的Uri進行策略執行,也就是說你完全可以定義一套自己內部使用的Uri規則和處理框架,建立在Routing基礎之上會很容易;
根據IHttpModule、IHttpHander 的相關的知識,我們很容易就能知道從哪裡可以找到Routing的入口線索,如果我們都沒有猜錯的話在系統的Web.config檔案中肯定有一個專門處理Routing的IHttpModule,利用來它將ASP.NETRouting物件植入到ASP.NET框架之中;
我們找到.NET Framework環境配置的地方:C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config 在該檔案中我們可以找到系統級別的配置資訊;
其實這裡面配置的都是系統級別的選項,而我們程式裡面使用的Web.config檔案只是用來配置跟應用程式相關的選項,這樣的好處是我們可以在應用程式級別很方便的改變系統的預設配置;
我們找到httpModules配置節,在倒數第二行發現一個name為UrlRoutingModule-4.0的IHttpModule配置,應該就是它了,最關鍵的是它的type資訊是System.Web.Routing.UrlRoutingModule 毋庸置疑了;
現在就好辦多了,我們只要順藤摸瓜就能找到UrlRoutingModule是如何工作的了,不過先不能急,還有些思路並不清晰,我們繼續慢慢分析;按照這樣的一個思路,基本上我們可以斷定UrlRoutingModule就是協調ASP.NETRouting框架的紐帶;
圖3.1:
此圖總結了我們到目前為止的一個基本思路,底層ASP.NET框架處理HTTP的物件化,然後通過ASP.NETRouting Module建立IHttpHandler介面物件,再然後就是執行IHttpHander介面,共三個步驟;
作為應用框架也就是最上層的程式碼,如何才能決定ASP.NETRouting框架在處理ASP.NET的呼叫的時候能使用自己的IHttpHander介面物件,這個問題就需要我們深入的看一下ASP.NETRouting路由物件的內部物件模型了;
4.】ASP.NETRouting路由物件模型的內部結構
這裡我將使用ASP.NETMVC作為應用框架來講解本例(目前我並不瞭解ASP.NETWEBAPI);那麼ASP.NETMVC作為應用層框架,是如何讓ASP.NETRouting幫助轉換IHttpHander介面的呢,這就不得不去分析Routing一些列的物件之間的組成關係及互相作用了;
根據3.】小節,我們已經瞭解ASP.NETRouting是使用UrlRoutingModuel物件來作為ASP.NET管道的監聽者,然後根據一系列的內部處理得出最終的IHttpHander介面物件;那麼要想搞清楚UrlRoutingModule是如何具體的協調這一切的,必須得深入的去分析原始碼才行,儘管我們只需要瞭解一個80%那也少不了這個環節;
注意:需要原始碼的朋友可以直接去一下站點獲取,微軟官方開源網站:http://www.codeplex.com/,開源中國:http://www.oschina.net/都可以找到原始碼;
4.1】UrlRoutingModule物件內部結構
首當其衝需要搞清楚的就是UrlRoutingModule物件,根據原始碼指示我們基本上能確定幾個基本的原理,首先UrlRoutingModule繼承自IHttpModule介面,訂閱了Application.PostResolveRequstCache事件,在該事件中主要是通過全域性路由物件表RouteTable物件獲取提供給上層使用的依賴注入介面IRouteHander介面;
【依賴注入介面】
這裡需要解釋一下什麼叫依賴注入介面,可以簡單的將依賴注入介面理解成提供給外界一個具體實現的機會;其實就是設計原則中的“依賴倒置原則”,在RouteData的內部不是直接依賴具體的物件;介面就是契約,提供一個介面就是約定雙方之間的契約;這裡是約定了Routing框架將使用IRouteHander介面來獲取最後的處理IHttpHander介面;
下面我們將對UrlRoutingModule物件進行分析,由於我們分析原始碼是想搞清楚物件模型之間的操作流程及關係,所以不可能分析所有的程式碼,我們的重點是搞清楚他們的執行順序及原理;由於UrlRoutingModule物件是導火線,它的出現將接二連三的牽連其他的物件出現,我們將分小節進行分析,交界處將一帶而過;
根據我們前面的分析思路,我們首先要找到UrlRoutingModule繫結Application事件的地方;
protected virtual void Init (HttpApplication application) { application.PostResolveRequestCache += PostResolveRequestCache; }
在PostResolverRequestCache方法中,我們將看到該方法呼叫了本地內部的一個同名方法:
void PostResolveRequestCache (object o, EventArgs e) { var app = (HttpApplication) o; PostResolveRequestCache (new HttpContextWrapper (app.Context)); }
然後例項化了一個HttpContextWrapper包裝物件,傳入該同名方法;
public virtual void PostResolveRequestCache (HttpContextBase context) { var rd = RouteCollection.GetRouteData (context); //(1)匹配RouteData物件,後面分析; var rc = new RequestContext (context, rd); //(2)封裝計算出來的RouteData物件和當前HttpRequest物件; IHttpHandler http = rd.RouteHandler.GetHttpHandler (rc); //(3)使用(1)步驟計算出來的當前RouteData物件中的RouteHander屬性獲取路由處理程式IHttpHander介面 context.Request.RequestContext = rc; context.RemapHandler (http); }
當然我已經省略了部分不太相關的程式碼,畢竟要想說清楚所有的程式碼一篇文章顯然是不夠的;上述程式碼中我用紅色標記出重要的部分;
首先是第一個重要點(1),匹配RouteData物件;其實就是我們在程式裡面配置的Url模板資料,當請求來的時候我們需要去根據當前請求的Url到路由表去匹配是否有符合當前Url的路由物件;
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
其實就是對應著本段程式碼的配置,這段程式碼處理後將是一個Route物件例項,而上面的RouteCollection就很好理解了,它是Route的強型別集合;
到目前為止,已經出現了好幾個跟Route相關的物件,沒關係,當我們將整條線分析到頭時將很清楚他們的作用;
第二個重要點(2),封裝RequestContext物件,其實我們從型別的名稱上就能確定它的用途,它是請求上下文,也是有界上下文;這裡面封裝了在下面獲取IHttpHander介面時將需要當作引數;
第三個重點(3),利用前面的匹配得到的RouteData物件,其實RouteData是路由資料的意思,那麼什麼叫路由資料:就是路由匹配成功後所生成的和路由相關的資料;還記得我們在3】節分析的原理嗎,UrlRoutingModule對上層提供基本的路由功能,但是具體的處理是在應用層面上;
那麼就是這裡通過RouteData.RouteHandler.GetHttpHandler(RequestContext requestContext) 方法獲取到的最終頂層應用處理器;
圖4.1:
上面的解釋可以使用這幅圖來簡單的表達;
UrlRoutingModule物件通過RouteData路由資料物件獲取IRouteHander介面,然後通過IRouteHander介面獲取最終的IHttpHander介面;
小結:其實可以將UrlRoutingModule物件理解成是ASP.NETRouting模組的基礎部分,而擴充套件的地方則在我們應用程式配置的地方,也就是我們通常在Global.asax.cs檔案中配置的路由資料;當我們在配置Route物件的時候其實已經指定了IRouteHander介面,然後這個介面會被放入RouteData同名屬性中,而不是作為零散的物件被UrlRoutingModule直接獲取;
4.2】RouteBase、Route、RouteCollection、RouteTable路由核心物件模型
在4.1 】節中,UrlRoutingModule是路由框架的基礎設施部分,內建於. NETFramework系統及ASP.NET配置之中web.config;在ASP.NET進行版本升級的時候該部分工作已經由系統自動幫我們升級,我們在使用的時候只需要建立ASP.NET3.5 SP1以上的版本都會自動擁有路由系統功能,因為根據微軟官方MSDN介紹,路由系統是在ASP.NET3.5 SP1中引入的;其實我們大部分使用的ASP.NET版本已經是4.5的,就算以前是2.0、3.0的版本也會陸續升級到最新的版本;因為新版本的框架提供了無數個讓你無法拒絕的優勢;
那麼當基礎部分有了之後我們能做到就是應用程式設計介面的程式設計,其實這部分才是我們接觸的地方;而這一小節我們將重點分析路由系統提供給我們應用層面的程式設計介面,也就是上面標題列出的幾個核心物件;
先基本介紹一下這幾個物件的意思和彼此之間的關係:
RouteBase:很明顯是Route的基類,提供了作為自定義路由物件的頂層抽象,所有的路由框架的內部均使用抽象的RouteBase為依賴;
Route:路由系統預設實現的路由物件繼承自RouteBase抽象基類,用來作為我們預設的路由配置物件,當然你可以可是實現自己的Route物件;
RouteCollection:Route作為單個Url的配置,那麼系統中肯定會有多個Url規則的配置,所以RouteCollection物件是表示Route的強型別集合,該類繼承自 Collection<RouteBase> 型別;所以RouteCollection是用來作為Route的集合管理用的;注意這裡的泛型Collection<T>中的RouteBase,再一次提醒我們要“依賴倒置”;
RouteTable:用來存放RouteCollection物件,路由表中有一系列的路由物件,而這一系列的物件就是RouteCollection管理的;在RouteTable中用Routes靜態屬性表示當前系統全域性的路由對映表;
這裡很明顯能看出來對路由的一層一層抽象,從簡單的Route表示一個路由對映,再到表示Route的集合RouteCollection,再到最後的RouteTable的,抽象的很OO;
為了讓大家對上面這些物件的解釋有一個直觀的認識,我們用一張圖來解釋他們如何關聯和執行流程;
圖4.2:
下面我們將深入到各個物件的內部去摸索一下他們之間的互動,我們根據這種引用關係來分析,首先是Route物件;
【Route、RouteBase】
Route物件繼承自RouteBase代表一個Url模板的配置,包括Url的模板的字串,如:api/order/102304,還有一些輔助性的內容,這不是本節的重點,我們只要知道它是用來做Url的配置即可; Route物件不是直接被我們例項化的,而是通過應用層的擴充套件方法進行例項化,為什麼要這麼做,其實這裡就是路由為什麼能轉到上層的關鍵點;
根據ASP.NETMVC中的路由集合擴充套件類,也就是System.Web.Mvc.RouteCollectionExtensions靜態類中的擴充套件方法,這些擴充套件方法就是用來包裝我們在應用ASP.NET的時候配置Route使用的;是否還記得我們第4】節的一開始介紹了一個依賴注入介面的原理,這裡將通過依賴注入介面達到外掛自定義實現的目的;
在Route原始碼中,我們將看到它有一個IRouteHander介面型別的屬性RouteHander;
public class Route : RouteBase { public IRouteHandler RouteHandler { get; set; } }
這個IRouteHandler介面型別的屬性就是我們ASP.NETMVC將要實現的一個IRouteHandler介面;而這個介面的定義:
public interface IRouteHandler { IHttpHandler GetHttpHandler (RequestContext requestContext); }
很簡單,就是為了建立出ASP.NET管道引擎最後執行的IHttpHandler介面; Route類有一個重寫了RouteBase的核心方法:
public override RouteData GetRouteData (HttpContextBase httpContext)
該方法是用來獲取當前路由的一些匹配資料的,關於RouteData在4.1】節介紹過,詳細我們將看下面關於對它的詳細分析,這裡將不做介紹了;
小結:其實Route物件還算簡單,關鍵的兩點就是GetRouteData方法和IRouteHander介面,前者是用來獲取當前路由匹配成功後的路由資訊,而後者是用來返回最終要執行的IHttpHandler介面;
【RouteCollection、RouteTable】
RouteCollecton和RouteTable物件比較簡單;我們先來看RouteCollection物件,首先你可能會有疑問,為什麼不用一個簡單的Collection型別的物件來存放Route例項,非要實現了一個RouteCollection;不看原始碼還真不知道它內部做了很多工作,首先最重要的就是執行緒併發情況下的Look機制;由於我們的RouteCollection物件是全域性靜態物件,會同時存在著多個執行緒併發的讀取這個物件,所以必須在對集合訪問的時候進行互斥控制;比如說這段程式碼:
public void Add (string name, RouteBase item) { lock (GetWriteLock ()) { base.Add (item); if (!String.IsNullOrEmpty (name)) d.Add (name, item); } }
在新增路由的時候首先鎖住寫入物件,然後才能安全的進行操作;我們接著RouteTable物件,這個物件最簡單,就是一個靜態屬性Routes用來存放全域性路由表;
public class RouteTable { static RouteTable () { Routes = new RouteCollection (); } public static RouteCollection Routes { get; private set; } }
當首次獲取Routes屬性時,會在靜態建構函式中例項化RouteCollection物件;
4.3】RouteValueDictionary、RouteData、RequestContext 路由資料物件模型
在第4.2】小節中,我們分析了路由系統的幾個核心物件,但是核心物件要想執行起來中間必須有一些資料封裝的物件為他們消除資料傳遞的問題;而這小節的三個核心物件真是路由系統能成功工作的必不可少的資料存放、資料傳輸容器的核心物件;
先基本介紹一下這幾個物件的意思和彼此之間的關係:
RouteValueDictionary:路由物件內部存放中間值使用的物件,比如Url模板的預設值,名稱空間,位址列傳過來的引數等等;當然也可以用來存放任何Key-Value形式的任何值;
RouteData:路由資料,用來包裝根據路由Url匹配成功後的路由資料封裝,最重要的是將IRouteHander介面傳遞到UrlRoutingModule中去;
RequestContext:請求上下文,將HttpRequest、RouteData包裝起來傳入IRouteHander介面獲取IHttpHander介面;因為IRouteHandler介面方法GetHttpHandler需要知道當前請求的一些資訊和根據當前Url處理後的路由資料才能計算出當前的IHttpHandler介面;
為了讓大家對上面這些物件的解釋有一個直觀的認識,我們用一張圖來解釋他們如何關聯和執行流程;
圖4.3:
下面詳細的分析每個物件的內部原理;
【RouteValueDictionary】
RouteValueDirctionary物件是在路由物件內部存放資料用的,比如:我們在配置路由的時候,可以指定一些預設值、名稱空間等等;
看RouteValueDictionary原始碼定義:
public class RouteValueDictionary : IDictionary<string, object>
該型別繼承自字典介面IDictionary<string,object>,繼承自字典介面而不是繼承自字典基類目的只是想使用字典的行為而不是它的預設實現;在RouteValueDictionary內部使用了一個Dictionary<string,object>型別作為最終容器;
Dictionary<string,object> d = new Dictionary<string,object> (CaseInsensitiveStringComparer.Instance);
在建構函式中使用了一個內部類CaseInsensitiveStringComparer進行Key的相等比較:
internal class CaseInsensitiveStringComparer : IEqualityComparer<string> { public static readonly CaseInsensitiveStringComparer Instance = new CaseInsensitiveStringComparer (); public int GetHashCode (string obj) { return obj.ToLower (CultureInfo.InvariantCulture).GetHashCode (); } public bool Equals (string obj1, string obj2) { return String.Equals (obj1, obj2, StringComparison.OrdinalIgnoreCase); } }
IEqualityComparer介面還是很不錯的,不過現在基本上不這麼用了,而是直接提供了一個Lambda做為比較函式;
【RouteData】
路由資料物件,它的大概意思我想大家應該知道了,上面提到過很多次,這裡就不介紹了;我們直接看一下RouteData內部核心程式碼段:
public RouteData (RouteBase route, IRouteHandler routeHandler) { Route = route; RouteHandler = routeHandler; DataTokens = new RouteValueDictionary (); Values = new RouteValueDictionary (); } public RouteValueDictionary DataTokens { get; private set; } public RouteBase Route { get; set; } public IRouteHandler RouteHandler { get; set; } public RouteValueDictionary Values { get; private set; }
通過建構函式我們能瞭解到,儲存了對Route物件的引用和IRouteHander介面的引用,為什麼將IRouteHandler作為建構函式引數,那是因為RouteBase根本沒有對IRouteHander介面的屬性定義;IRouteHandler介面在不在RouteBase或Route中不重要,因為Route可以是自定義的,這裡的強制性是在RouteData中,它的建構函式必須接受IRouteHandler型別介面;
我們接著看,在建構函式的下面兩行程式碼中分別是例項化了DataTokens、Values兩個屬性,而型別是RouteValueDictionary,這也剛好和我們上面分析的對上了;RouteValueDictionary是內部用來儲存這些零散鍵值對資料容器,在Route、RouteData還有其他地方均需要用到;就是因為RouteValueDictionary的Value是Object型別,所以可以用來存放任何型別的值,比較通用;
【RequestContext 】
RequestContext在上面也已經接觸很多次了,表示請求上下文,也就是跟當請求相關的所有資料都封裝在裡面;在後面的文章中,我們將接觸很多類似Context的物件,如:ControlContext,ViewContext之類的,都是用來控制上下文的邊界,而不是直接傳遞零散的引數;
4.4】IRouteHandler 、IHttpHander兩個介面之間的關係
IRouteHandler介面是路由框架起作用的核心,只有提供了IRouteHandler實現才能順利的得到背後的IHttpHandler介面;ASP.NETMVC提供了MvcRouteHandler物件來實現IRouteHandler介面,MvcRouteHandler在內部例項化實現了IHttpHandler介面的MvcHandler物件;MvcHandler然後通過RequestContext物件獲取RouteData物件,接著得到相應的Control資訊,進行後續的執行處理;
5.】UrlRoutingHandler 物件內部結構及擴充套件應用
在ASP.NETRouting路由框架中有一個很重要的IHttpHandler介面物件UrlRoutingHanlder,我想你肯定很疑惑,為什麼需要這樣一個物件;其實它的存在是為了提供給我們繞過UrlRoutingModule模組的機會;根據上面的詳細的分析,我們知道路由的入口在UrlRoutingModule,所有的路由相關的對映工作都在該類中完成,但是有時候我們很想繞過UrlRoutingModule進行簡單的處理或者效能方面的優化考慮,這就派上用場了;我能想到的使用場景目前來看是對ASP.NET第版本的專案做Url重寫是比較方便,首先我們的專案需要建立在低版本的ASP.NET之上,但是需要新增Url.ReWriter的功能,就需要我們自己去實現這樣的功能;
但是工作量和效能都很難控制好,如果使用這裡提供的UrlRoutingHandler進行實現就很方便了,UrlRoutingHandler給我們使用ASP.NETRouting框架的機會同時也不需要關心是否配置了UrlRoutingModule;
public abstract class UrlRoutingHandler : IHttpHandler
根據程式碼看出它是一個抽象類,直接實現IHttpHanlder介面,但是重要的是ProcessRequest方法;
protected virtual void ProcessRequest (HttpContextBase httpContext) { if (httpContext == null) throw new ArgumentNullException ("httpContext"); var rd = RouteCollection.GetRouteData (httpContext); if (rd == null) throw new HttpException ("The incoming request does not match any route"); if (rd.RouteHandler == null) throw new InvalidOperationException ("No IRouteHandler is assigned to the selected route"); RequestContext rc = new RequestContext (httpContext, rd); var hh = rd.RouteHandler.GetHttpHandler (rc); VerifyAndProcessRequest (hh, httpContext); } protected abstract void VerifyAndProcessRequest (IHttpHandler httpHandler, HttpContextBase httpContext);
該方法的邏輯跟UrlRoutingModule裡的PostResolveRequestCache方法是差不多的,都會通過全域性RouteCollection集合進行匹配當前的RouteData物件;那就足夠說明這個過程不會再通過UrlRoutingModule模組;方法的最後一行是執行一個模板方法:VerifyAndProcessRequest ,該方法是留給子類去實現的;
那麼這裡將路由和執行合在一起了,基類負責路由子類負責執行,很不錯的設計方法;
總結:這篇文章基本上介紹了跟路由相關的核心物件,但是還有一些其他輔助的類這裡並沒有進行講解,當然如果你有興趣可以自己去看看;這篇文章是為了讓我們能對路由的處理流程及結構有個瞭解,做到能在適當的時候進行擴充套件和查詢問題;
作者:王清培
出處:http://www.cnblogs.com/wangiqngpei557/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。