天天看點

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

繼上一篇文章之後,本文将介紹 Controller 和 Action 的一些較進階特性,包括 Controller Factory、Action Invoker 和異步 Controller 等内容。

本文目錄

開篇:示例準備

文章開始之前,我們先來了解一下一個請求的發出到Action方法處理後傳回結果的流程,請試着了解下圖:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

本文的重點是 controller factory 和 action invoker。顧名思義,controller factory 的作用是建立為請求提供服務的Controller執行個體;action invoker 的作用是尋找并調用Action方法。MVC架構為這兩者都提供了預設的實作,我們也可以對其進行自定義。

首先我們為本文要示範的示例做一些準備,把暫時想到的要用的 View、Controller 和 Action 都建立好。建立一個空的MVC應用程式,在Models檔案夾中添加一個名為 Result 的Model,代碼如下:

namespace MvcApplication2.Models {
    public class Result {
        public string ControllerName { get; set; }
        public string ActionName { get; set; }
    }
}      

在 /Views/Shared 檔案夾下添加一個名為 Result.cshtml 的視圖(不使用Layout),添加代碼如下:

...
<body>
    <div>Controller: @Model.ControllerName</div> 
    <div>Action: @Model.ActionName</div> 
</body>      

本文的所有Action方法将都使用這同一個View,目的是顯示被執行的Controller名稱和Action名稱。

然後我們建立一個名為Product的Controller,代碼如下:

public class ProductController : Controller {
        
    public ViewResult Index() {
        return View("Result", new Result {
            ControllerName = "Product",
            ActionName = "Index"
        });
    }

    public ViewResult List() {
        return View("Result", new Result {
            ControllerName = "Product",
            ActionName = "List"
        });
    }
}      

繼續添加一個名為Customer的Controller,代碼如下:

public class CustomerController : Controller {
        
    public ViewResult Index() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "Index"
        });
    }

    public ViewResult List() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "List"
        });
    }
}      

準備工作做好了,開始進入正題吧。

自定義 Controller Factory

Controller Factory,顧名思義,它就是建立 controller 執行個體的地方。想更好的了解Controller Factory是如何工作的,最好的方法就是自己去實作一個自定義的。當然,在實際的項目中我們很少會去自己實作,一般使用内置的就足夠。自定義一個Controller Factory需要實作 IControllerFactory 接口,這個接口的定義如下:

using System.Web.Routing; 
using System.Web.SessionState;

namespace System.Web.Mvc { 
    public interface IControllerFactory { 
        IController CreateController(RequestContext requestContext, string controllerName); 
        SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); 
        void ReleaseController(IController controller); 
    } 
}      

我們建立一個名為 Infrastructure 檔案夾,在這個檔案夾中建立一個名為 CustomControllerFactory 的類檔案,在這個類中我們将簡單的實作 IControllerFactory 接口的每個方法,代碼如下:

using System;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using MvcApplication2.Controllers;

namespace MvcApplication2.Infrastructure {
    public class CustomControllerFactory : IControllerFactory {

        public IController CreateController(RequestContext requestContext, string controllerName) {
            Type targetType = null;
            switch (controllerName) {
                case "Product":
                    targetType = typeof(ProductController);
                    break;
                case "Customer":
                    targetType = typeof(CustomerController);
                    break;
                default:
                    requestContext.RouteData.Values["controller"] = "Product";
                    targetType = typeof(ProductController);
                    break;
            }
            return targetType == null ? null : (IController)DependencyResolver.Current.GetService(targetType);
        }

        public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) {
            return SessionStateBehavior.Default;
        }

        public void ReleaseController(IController controller) {
            IDisposable disposable = controller as IDisposable;
            if (disposable != null) {
                disposable.Dispose();
            }
        }
    }
}      

先來分析一下這個類。

這裡最重要的方法是 CreateController,當MVC架構需要一個 Controller 來處理請求時調用該方法。它有兩個參數,一個是 RequestContext 對象,通過它我們可以得到請求相關的資訊;第二個參數是一個string類型的controller名稱,它的值來自于URL。這裡我們隻建立了兩個Controller,是以我們在 CreateController 方法中進行了寫死(寫死了controller的名稱),CreateController 方法的目的是建立Controller執行個體。

在自定義的Cotroller Factory中,我們可以任意改變系統預設的行為,比如switch語句中的default節點:

requestContext.RouteData.Values["controller"] = "Product";      

它将路由的controller值改為Product,使得執行的cotroller并不是使用者所請求的controller。

在本系列的 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 文章中也講了一個用 Ninject 建立Controller Factory的例子,使用的是 ninjectKernel.Get(controllerType) 方法來建立Controller執行個體。這裡我們使用 MVC 架構提供的 DependencyResolver 類來建立:

return targetType == null ? null : (IController)DependencyResolver.Current.GetService(targetType);      

靜态的 DependencyResolver.Current 屬性傳回一個 IDependencyResolver 接口的實作,這個實作中定義了 GetService 方法,它根據 System.Type 對象(targetType)參數自動為我們建立 targetType 執行個體,和使用Ninject類似。

最後來看看實作 IControllerFactory 接口的另外兩個方法。

GetControllerSessionBehavior 方法,告訴MVC架構是否保留Session資料,這點放在文章後面講。

ReleaseController 方法,當controller對象不再需要時被調用,這裡我們判斷controller對象是否實作了IDisposable接口,實作了則調用 Dispose 方法來釋放資源。

CustomControllerFactory 類分析完了。和 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 講的示例一樣,要使用自定義的Controller Factory還需要在 Global.asax.cs 檔案的 Application_Start 方法中對自定義的 CustomControllerFactory 類進注冊,如下:

protected void Application_Start() {
    ...
    ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
}      

運作程式,應用程式根據路由設定的預設值顯示如下:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

你可以定位到任意 /xxx/xxx 格式的URL來驗證我們自定的 Controller Factory 的工作。

使用内置的 Controller Factory

 為了幫助了解Controller Factory是如何工作,我們通過實作IControllerFactory接口自定義了一個Controller Factory。在實際的項目中,我們一般不會這麼做,大多數情況我們使用内置的Controller Factory,叫 DefaultControllerFactory。當它從路由系統接收到一個請求後,從路由執行個體中解析出 controller 的名稱,然後根據名稱找到 controller 類,這個類必須滿足下面幾個标準:

  • 必須是public。
  • 必須是具體的類(非抽象類)。
  • 沒有泛型參數。
  • 類的名稱必須以Controller結尾。
  • 類必須(間接或直接)實作IController接口。

DefaultControllerFactory類維護了一個滿足以上标準的類的清單,這樣當每次接收到一個請求時不需要再去搜尋一遍。當它找到了合适的 controller 類,則使用Controller Activator(一會介紹)來建立Controller 類的執行個體。它内部是通過 DependencyResolver 類進行依賴解析建立 controller 執行個體的,和使用Ninject是類似的原理。

你可以通過繼承 DefaultControllerFactory 類重寫其中預設的方法來自定義建立 controller 的過程,下面是三個可以被重寫的方法:

  • GetControllerType,傳回Type類型,為請求比對對應的 controller 類,用上面定義的标準來篩選 controller 類也是在這裡執行的。
  • GetControllerInstance,傳回是IController類型,作用是根據指定 controller 的類型建立類型的執行個體。
  • CreateController 方法,傳回是 IController 類型,它是 IControllerFactory 接口的 CreateController 方法的實作。預設情況下,它調用 GetControllerType 方法來決定哪個類需要被執行個體化,然後把controller 類型傳遞給GetControllerInstance。

重寫 GetControllerInstance 方法,可以實作對建立 controller 執行個體的過程進行控制,最常見的是進行依賴注入。

在本系列的 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 文章中的示例就是一個對 GetControllerInstance 方法進行重寫的完整示例,在這就不重複示範了。

現在我們知道 DefaultControllerFactory 通過 GetControllerType 方法拿到 controller 的類型後,它把類型傳遞給 GetControllerInstance 方法以擷取 controller 的執行個體。那麼,GetControllerInstance 又是如何來擷取執行個體的呢?這就需要講到另外一個 controller 中的角色了,它就是下面講的:Controller Activator。

Controller 的激活

當 DefaultControllerFactory 類接收到一個 controller 執行個體的請求時,在 DefaultControllerFactory 類内部通過 GetControllerType 方法來獲得 controller 的類型,然後把這個類型傳遞給 GetControllerInstance 方法以獲得 controller 的執行個體。

是以在 GetControllerInstance  方法中就需要有某個東西來建立 controller 執行個體,這個建立的過程就是 controller 被激活的過程。

預設情況下 MVC 使用 DefaultControllerActivator 類來做 controller 的激活工作,它實作了 IControllerActivator 接口,該接口定義如下:

public interface IControllerActivator { 
    IController Create(RequestContext requestContext, Type controllerType); 
}      

該接口僅含有一個 Create 方法,RequestContext 對象參數用來擷取請求相關的資訊,Type 類型參數指定了要被執行個體化的類型。DefaultControllerActivator 類中整個 controller 的激活過程就在它的 Create 方法裡面。下面我們通過實作這個接口來自定義一個簡單的 Controller Activator:

public class CustomControllerActivator : IControllerActivator {
    public IController Create(RequestContext requestContext, Type controllerType) {
        if (controllerType == typeof(ProductController)) {
            controllerType = typeof(CustomerController);
        }
        return (IController)DependencyResolver.Current.GetService(controllerType);
    }
}      

這個 CustomControllerActivator 非常簡單,如果請求的是 ProductController 則我們給它建立 CustomerController 的執行個體。為了使用這個自定的 Activator,需要在 Global.asax 檔案中的 Application_Start 方法中注冊 Controller Factory 時給 Factory 的構造函數傳遞我們的這個 Activator 的執行個體,如下:

protected void Application_Start() { 
    ...
    ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new CustomControllerActivator())); 
}       

運作程式,把URL定位到 /Product ,本來路由将指定到 Product controller, 然後 DefaultControllerFactory 類将請求 Activator 建立一個 ProductController 執行個體。但我們注冊了自義的 Controller Activator,在這個自定義的 Activator 建立 Controller 執行個體的的時候,我們做了一個“手腳”,改變了這種預設行為。當請求建立 ProductController 執行個體時,我們給它建立了CustomerController 的執行個體。結果如下:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

其實更多的時候,我們自定義 controller 的激活機制是為了引入IoC,和 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 講的通過繼承 DefaultControllerFactory 引入 IoC 是一個道理。

自定義 Action Invoker

 當 Controller Factory 建立好了一個類的執行個體後,MVC架構則需要一種方式來調用這個執行個體的 action 方法。如果建立的 controller 是繼承 Controller 抽象類的,那麼則是由 Action Invoker 來完成調用 action 方法的任務,MVC 預設使用的是 ControllerActionInvoker 類。如果是直接繼承 IController 接口的 controller,那麼就需要手動來調用 action 方法,見上一篇 [ASP.NET MVC 小牛之路]09 - Controller 和 Action (1) 。下面我們通過自定義一個 Action Invoker 來了解一下 Action Invoker 的運作機制。

建立一個自定義的 Action Invoker 需要實作 IActionInvoker 接口,該接口的定義如下:

public interface IActionInvoker { 
    bool InvokeAction(ControllerContext controllerContext, string actionName); 
}       

這個接口隻有一個 InvokeAction 方法。ControllerContext 對象參數包含了調用該方法的controller的資訊,string類型的參數是要調用的Action方法的名稱,這個名稱來源于路由系統。傳回值為bool類型,當actoin方法被找到并被調用時傳回true,否則傳回false。

下面是實作了IActionInvoker接口的 CustomActionInvoker 類:

using System.Web.Mvc;

namespace MvcApplication2.Infrastructure {
    public class CustomActionInvoker : IActionInvoker {
        public bool InvokeAction(ControllerContext controllerContext, string actionName) {
            if (actionName == "Index") {
                controllerContext.HttpContext.Response.Write("This is output from the Index action");
                return true;
            }
            else {
                return false;
            }
        }
    }
}      

這個 CustomActionInvoker 不需要關心實際被調用的Action方法。如果請求的是Index Action,這個 Invoker 通過 Response 直接輸出一個消息,如果不是請Index Action,則會引發一個404-未找到錯誤。

決定Controller使用哪個Action Invoker是由 Controller 中的 Controller.ActionInvoker 屬性來決定的,由它來告訴MVC目前的 controller 将使用哪個 Action Invoker 來調用 Action 方法。如下我們建立一個ActionInvokerController,并在它的構造函數中指定了 Action Invoker 為我們自定義的 Action Invoker:

namespace MvcApplication2.Controllers {
    public class ActionInvokerController : Controller {
        public ActionInvokerController() {
            this.ActionInvoker = new CustomActionInvoker();
        }
    }
}      

這個 controller 中沒有 Action 方法,它依靠 CustomActionInvoker 來處理請求。運作程式,将URL定位到 /ActionInvoker/Index 可見如下結果:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

如果将URL定位到 ActionInvoker 下的其他Action,則會傳回一個404的錯誤頁面。

我們不推薦去實作自己的Action Invoker。首先内置的Action Invoker提供了一些非常有用的特性;其次是缺乏可擴充性和對View的支援等。這裡隻是為了示範和了解MVC架構處理請求過程的細節。

使用内置的 Action Invoker

通過自定義 Action Invoker,我們知道了MVC調用 Action 方法的機制。我們建立一個繼承自 Controller 抽象類的 controller,如果不指定Controller.ActionInvoker,那麼MVC會使用内置預設的Action Invoker,它是 ControllerActionInvoker 類。它的工作是把請求比對到對應的 Action 方法并調用之,簡單說就是尋找和調用 Action 方法。

為了讓内置的 Action Invoker 能比對到 Action 方法,Action方法必須滿足下面的标準:

  • 必須是公共的(public)。
  • 不能是靜态的(static)。
  • 不能是System.Web.Mvc.Controller中存在的方法,或其他基類中的方法。如方法不能是 ToString 和 GetHashCode 等。
  • 不能是一個特殊的名稱。所謂特殊的名稱是方法名稱不能和構造函數、屬性或者事件等的名稱相同。

注意,Action方法也不能帶有泛型,如MyMethod<T>(),雖然 Action Invoker 能比對到,但會抛出異常。

内置的 Action Invoker 給我們提供了很多實用的特性,給開發帶來很大便利,下面兩節内容可以說明這一點。

給 Action 方法定義别名

預設情況下,内置的Action Invoker (ControllerActionInvoker)尋找的是和請求的 action 名稱相同的 action 方法。比如路由系統提供的 action 值是 Index,那麼 ControllerActionInvoker 将尋找一個名為 Index 的方法,如果找到了,它就用這個方法來處理請求。ControllerActionInvoker 允許我們對此行為進行調整,即可以通過使用 ActionName 特性對 action 使用别名,如下對 CustomerController 的 List action 方法使用 ActionName 特性:

public class CustomerController : Controller {
    ...
    [ActionName("Enumerate")]
    public ViewResult List() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "List"
        });
    }
}      

當請求 Enumerate Action 時,将會使用 List 方法來處理請求。下面是請求 /Customer/Enumerate 的結果:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

這時候對 /Customer/List 的請求則會無效,報“找不到資源”的錯誤,如下:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

使用 Action 方法别名有兩個好處:一是可以使用非法的C#方法名來作為請求的 action 名,如 [ActionName("User-Registration")]。二是,如果你有兩個功能不同的方法,有相同的參數相同的名稱,但針對不同的HTTP請求(一個使用 [HttpGet],另一個使用 [HttpPost]),你可以給這兩個方法不同的方法名,然後使用 [ActionName] 來指定相同的 action 請求名稱。

Action 方法選擇器

我們經常會在 controller 中對多個 action 方法使用同一個方法名。在這種情況下,我們就需要告訴 MVC 怎樣在相同的方法名中選擇正确的 action 方法來處理請求。這個機制稱為 Action 方法選擇,它在基于識别方法名稱的基礎上,允許通過請求的類型來選擇 action 方法。MVC 架構可使用C#特性來做到這一點,是以這種作用的特性可以稱為 Action 方法選擇器。

内置 Action 方法選擇器

MVC提供了幾種内置的特性來支援 Action 方法選擇,它包括HttpGet、HttpPost、HttpPut 和 NonAction 等。這些選擇器從名字上很容易了解什麼意思,這裡就不解釋了。下面舉個 NonAction 的例子。在 CustomerController 中添加一個 MyAction 方法,然後應用 [NonAction] 特性,如下:

public class CustomerController : Controller {
    ...
    [NonAction]
    public ActionResult MyAction() {
        return View();
    }
}      

使用 [NonAction] 後,方法将不會被識别為 action 方法,如下是請求 /Customer/MyAction 的結果:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

當然我們也可以通過把方法聲明為 private 來告訴MVC它不是一個 action 方法。

自定義 Action 方法選擇器

除了使用内置的Action方法選擇器外,我們也可以自定義。所有的 action 選擇器都繼承自 ActionMethodSelectorAttribute 類,這個類的定義如下:

using System.Reflection; 

namespace System.Web.Mvc { 
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 
    public abstract class ActionMethodSelectorAttribute : Attribute { 
        public abstract bool IsValidForRequest(ControllerContext controllerContext,  MethodInfo methodInfo); 
    } 
}      

它是一個抽象類,隻有一個抽象方法:IsValidForRequest。通過重寫這個方法,可以判斷某個請求是否允許調用 Action 方法。

我們來考慮這樣一種情況:同一個URL請求,在本地和遠端請求的是不同的 action (如對于本地則繞過權限驗證可能需要這麼做)。那麼自定義一個本地的 Action 選擇器會是一個不錯的選擇。下面我們來實作這樣一個功能的 Action 選擇器:

using System.Reflection;
using System.Web.Mvc;

namespace MvcApplication2.Infrastructure {
    public class LocalAttribute : ActionMethodSelectorAttribute {
        public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
            return controllerContext.HttpContext.Request.IsLocal;
        }
    } 
}      

修改 CustomerController,添加一個LocalIndex 方法,并對它應用 “Index”别名,代碼如下:

public class CustomerController : Controller {
        
    public ViewResult Index() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "Index"
        });
    }

    [ActionName("Index")]
    public ViewResult LocalIndex() {
        return View("Result", new Result {
            ControllerName = "Customer",
            ActionName = "LocalIndex"
        });
    }
    ...          
}      

這時如果請求 /Customer/Index,這兩個 action 方法都會被比對到而引發歧義問題,程式将會報錯:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

這時候我們再對 LocalIndex 應用我們自定義的 Local 選擇器:

...
[Local]
[ActionName("Index")]
public ViewResult LocalIndex() {
    return View("Result", new Result {
        ControllerName = "Customer",
        ActionName = "Index"
    });
}
...      

程式在本地運作的時候則會比對到 LocalIndex action方法,結果如下:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

通過這個例子我們也發現,定義了選擇器特性的Action方法被比對的優先級要高于沒有定義選擇器特性的Action方法。

異步 Controller

對于 ASP.NET 的工作平台 IIS,它維護了一個.NET線程池用來處理用戶端請求。這個線程池稱為工作線程池(worker thread pool),其中的線程稱為工作線程(worker threads)。當接收到一個用戶端請求,一個工作線程從工作線程池中被喚醒并處理接收到的請求。當請求被處理完了後,工作線程又被這個線程池回收。這種線程程池的機制對ASP.NET應用程式有如下兩個好處:

  • 通過線程的重複利用,避免了每次接收到一個新的請求就建立一個新的線程。
  • 線程池維護的線程數是固定的,這樣線程不會被無限制地建立,減少了伺服器崩潰的風險。

一個請求是對應一個工作線程,如果MVC中的action對請求處理的時間很短暫,那麼工作線程很快就會被線程池收回以備重用。但如果執行action的工作線程需要調用其他服務(如調用遠端的服務,資料的導入導出),這個服務可能需要花很長時間來完成任務,那麼這個工作線程将會一直等待下去,直到調用的服務傳回才繼續工作。這個工作線程在等待的過程中什麼也沒做,資源浪費了。設想一下,如果這樣的action一多,所有的工作線程都處于等待狀态,大家都沒事做,而新的請求來了又沒人理,這樣就陷入了尴尬境地。

解決這個問題需要使用異步(asynchronous) Controller,異步Controller允許工作線程在等待(await)的時候去處理别的請求,這樣做減少了資源浪費,有效提高了伺服器的性能。

使用異步 Controller 需要用到.NET 4.5的新特性:異步方法。異步方法有兩個新的關鍵字:await 和 async。這個新知識點朋友們自己去網上找找資料看吧,這裡就不講了,我們把重點放在MVC中的異步 Controller 上。

在Models檔案夾中添加一個 RemoteService 類,代碼如下:

using System.Threading;
using System.Threading.Tasks;

namespace MvcApplication2.Models {

    public class RemoteService {

        public async Task<string> GetRemoteDataAsync() {
            return await Task<string>.Factory.StartNew(() => {
                Thread.Sleep(2000);
                return "Hello from the other side of the world";
            });
        }
    }
}      

然後建立一個名為 RemoteData 的 Controller,讓它繼承自 AsyncController 類,代碼如下:

using System.Web.Mvc;
using MvcApplication2.Models;
using System.Threading.Tasks;

namespace MvcApplication2.Controllers {
    public class RemoteDataController : AsyncController {
        public async Task<ActionResult> Data() {
            
            string data = await new RemoteService().GetRemoteDataAsync();
            Response.Write(data);
            
            return View("Result", new Result {
                ControllerName = "RemoteData",
                ActionName = "Data"
            });
        }
    }
}      

運作程式,URL 定位到 /RemoteData/Data,2秒後将顯示如下結果:

[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

當請求 /RemoteData/Data 時,Data 方法開始執行。當執行到下面代碼調用遠端服務時:

string data = await new RemoteService().GetRemoteDataAsync();      

工作線程開始處于等待狀态,在等待過程中它可能被派去處理新的用戶端請求。當遠端服務傳回結果了,工作線程再回來處理後面的代碼。這種異步機制避免了工作線程處于閑等狀态,盡可能的利用已被激活的線程資源,對提高MVC應用程式性能是很有幫助的。、

評論精選

提問 by 鹵鴿:

IActionInvoker是在Controller.Excute方法中被調用,主要是查詢具體的Action 處理方法,其實整個請求的過程都是從MvcHandler進行開始的。這是我的了解。不知正确否?

回答 by Liam Wang

是這麼個意思,但不太嚴謹。确切一點說,Excute 方法是(自定義或預設的)ActionInvoker 的入口函數。 ActionInvoker 必須實作 IActionInvoker 接口來查找和調用 Action 方法。本文沒有介紹 MvcHandler 的知識。MvcHandler 是處理Controller的開始,但在MvcHandler 之前還有一個MvcRouteHandler,當請求經過路由解析後,MvcRouteHandler 執行個體會生成一個 MvcHandler 執行個體,并把請求交給它。MvcHandler 會從Controller 工廠中擷取一個 Controller 執行個體,然後由 Controller 來具體地處理請求。

PS:這篇文章寫到一半便在草稿箱中沉睡了一個多月,10月份20多天都在内蒙出差,回來都沒什麼興緻繼續寫下去。希望朋友們多多支援,讓我有動力把這個系列寫完。

作者:精緻碼農-王亮

出處:http://cnblogs.com/willick

聯系:[email protected]

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。如有問題或建議,請多多賜教,非常感謝。

繼續閱讀