本文通過一個維修工與工具庫的例子形象的描述一下為什么要用依賴注入、它的工作原理是什么樣的, 然后根據這個類比一下ASP.NET Core 中的依賴注入, 從而深刻了解它的使用方法、注意事項以及回收機制等.
本文主要內容:
1.為什么要用依賴注入(DI)
2.容器的構建和規則
3.ASP.NET Core 2.0中的依賴注入
4.使用方法及需要注意的問題
5.服務的Dispose
6.我想換個容器
1.為什么要用依賴注入(DI)
什么是依賴注入就不說了, 為什么要使用呢?
軟件設計原則中有一個依賴倒置原則(DIP)講的是要依賴於抽象,不要依賴於具體,高層模塊不應該依賴於低層模塊, 二者應該依賴於抽象。簡單的說就是為了更好的解耦。而控制反轉(Ioc)就是這樣的原則的其中一個實現思路, 這個思路的其中一種實現方式就是依賴注入(DI)。
感覺有點繞, 舉個栗子:老李是一個維修工, 現在要出任務去維修, 得先去申領個扳手。
圖一
老李: "請給我一把可以可以擰7mm大小的六角螺絲的扳手.", 然后庫管老張就從倉庫里拿了一把這樣的大力牌扳手給老李。
在這個例子中, 維修工老李只要告訴庫管我要一個 "可以擰7mm大小的六角螺絲"的扳手即可, 他不用關心扳手的品牌和樣式, 也不用采購扳手,更不用關心這個扳手是怎么來的.而對於庫管, 他只需提供滿足這樣規則的一個扳手即可, 不用去關心老李拿着這個扳手之后去干什么。所以老李和老張都只是關心"可以擰7mm大小的六角螺絲的"這個規則即可, 也就是說, 如果后期倉庫里不再提供大力牌扳手, 而是提供了
這樣的大牛牌扳手, 無論換了什么牌子和樣式, 只要仍滿足這個規則, 老李仍然可以正常工作.它們定義了一個規則(比如接口IWrench7mm), 二者都依賴於這個規則, 然后倉庫無論提供大力牌(WrenchDaLi : IWrench7mm)還是大牛牌(WrenchDaNiu : IWrench7mm), 都不影響正常工作.
這就是依賴倒置原則(DIP), 不依賴於具體(牌子), 高層模塊(老李)不應該依賴於低層模塊(大力牌扳手), 二者應該依賴於抽象(IWrench7mm:可以擰7mm大小的六角螺絲)。如果直接由老李去獲取(new)大力牌扳手, 那么當業務改變要求采用大牛牌的時候, 我們就要去修改老李的代碼.為了解耦, 在本例中我們只要在配置中讓倉庫由原來的提供大力牌改為提供大牛牌即可。老李要使用的時候, 可以通過注入(構造器、屬性、方法)的方式, 將倉庫提供的扳手實例提供給老李使用。
2.容器的構建和規則
繼續上面的例子, 庫管老張為什么會提供給老李大力牌而不是大牛牌的扳手呢? 那是因為領導給了他一份構建倉庫的物品購置及發放清單:
A. 當有人要7mm的六角扳手的時候,給他一個大力牌的扳手, 當再有人來要的時候就再給另一把。
B. 但對於相機, 每個小組只能給一台, 小組內所有人共用這一台。
C. 卡車更是全單位只有一輛, 誰申請都是同一輛。
圖二
3.ASP.NET Core 2.0中的依賴注入
首先看一下下面的圖三
圖三
這就是ASP.NET Core 中默認的依賴注入方式, 對比一下圖二是不是很像?
上篇文章說要將Startup放大介紹一下, 那么打開Startup這個文件, 看一下里面的ConfigureServices方法。顧名思義, 這個方法是用來配置服務,
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); }
此方法接收一個IServiceCollection類型的參數, 查看它的定義, 被定義在Microsoft.Extensions.DependencyInjection這個NuGet包中, 功能就是依賴注入, 在ASP.NET Core中被廣泛使用.
①IServiceCollection
它正是圖三中的①IServiceCollection, 它是一個IList<ServiceDescriptor>類型的集合。也就是上門的維修工的例子中領導制定的清單, 而Startup中的ConfigureServices這個方法的作用就是讓我們作為"領導"來配置這個清單。方法中默認調用的services.AddMvc(), 是IServiceCollection的一個擴展方法 public static IMvcBuilder AddMvc(this IServiceCollection services); , 作用就是向這個清單中添加了一些MVC需要的服務,例如Authorization、RazorViewEngin、DataAnnotations等。
系統需要的添加好了, 剩下的就是我們把自己需要的用的添加進去了。 這里我們可以創建一個ServiceDescriptor然后把它添加到這個集合里, 系統①IServiceCollection也提供了AddSingleton、AddScoped和AddTransient這樣的方法, 三種方法定義了所添加服務的生命周期, 具體見②ServiceDescriptor.
當然我們可以在ConfigureServices中通過一堆AddXXX將服務添加到IServiceCollection, 但這樣好多堆在一起不易於修改和閱讀, 特別還有一些功能會包含好幾個服務的添加, 所以推薦像系統默認的 AddMvc() 這樣封裝到一個擴展方法中去。
現在來看一下清單中的內容。
②ServiceDescriptor
既然①IServiceCollection 是一個IList<ServiceDescriptor>, 那么ServiceDescriptor也就是這個集合中的內容了, 也就是倉庫中物品的描述.對照圖三中的②ServiceDescriptor看一下它的各個屬性。
A. Type ServiceType: 服務的類型 --7mm六角扳手
B. Type ImplementationType: 實現的類型 --大力牌扳手
C. ServiceLifetime Lifetime: 服務的生命周期 --若干(誰要都給一把新的)
D. object ImplementationInstance: 實現服務的實例
E: Func<IServiceProvider, object> ImplementationFactory: 創建服務實例的工廠
ServiceLifetime是一個枚舉, 上文說的AddSingleton、AddScoped和AddTransient就是對應這個枚舉, 分別為:
Singleton: 單例, 例子中的卡車, 全單位只有一輛, 誰調用都是返回這個實例。
Scoped: 區域內單例, 例子中的傻瓜相機, 每小組一台, 小組內誰要都是同一台, 不同小組的相機不同。
Transient: 臨時的 例子中的扳手和錘子, 誰要都給一把新的, 所有人的都不是同一把。
從這些屬性的介紹來看, ServiceDescriptor規定了當有人需要ServiceType這個類型服務的時候, 提供給他一個ImplementationType類型的實例, 其他幾個屬性規定了提供的方法和生命周期.
③IServiceProvider
③IServiceProvider 服務提供者,由①IServiceCollection的擴展方法BuildServiceProvider創建, 當需要它提供某個服務的時候, 它會根據創建它的①IServiceCollection中的對應的②ServiceDescriptor提供相應的服務實例.。它提供了⑤GetService、GetRequiredService、GetServices、GetRequiredServices這樣的幾個用於提供服務實例的方法,就像庫管老張一樣, 告訴他你需要什么服務的實例, 他會根據清單規定給你對應的工具。
GetService和GetRequiredService的區別:
維修工老李: "老張, 給我一架空客A380." -- GetService<IA380>();
老張: "這個沒有." -- return null;
維修工老李: "老張, 必須給我一架空客A380!" -- GetRequiredService<IA380>();
老張: "這個真TMD沒有." -- System.InvalidOperationException:“No service for type 'IA380' has been registered.”;
GetServices和GetRequiredServices這兩個加了"s"的方法返回對應的集合。
④IServiceScope
上文中的ServiceDescriptor的Lifetime屬性為Scoped的時候, IServiceProvider會為其創建一個新的區域④IServiceScope,
public interface IServiceScope : IDisposable { IServiceProvider ServiceProvider { get; } }
從上面的代碼可以看出它只是對IServiceProvider進行了一個簡單的封裝, 原始的IServiceProvider通過CreateScope()創建了一個IServiceScope, 而這個IServiceScope的ServiceProvider屬性將負責這個區域內的服務提供, 而Lifetime為Scoped的ServiceDescriptor創建的實例在本區域內是以"單例"的形式存在的.
在ASP.NET Core中, Lifetime為Scoped的實例在每次請求中只創建一次.
4.使用方法及需要注意的問題
對於上面的維修工的例子, ASP.NET Core的依賴注入還是有一些不一樣的地方, 比如用卡車 (全單位只有一輛, 誰借都是這一輛) 來類比單例, 只有一個確實沒問題, 但對於卡車, A把它借走了, B只有等他被還回來才能去借。 同樣標記為Scoped的傻瓜相機即使在小組內也是需要輪換使用的。 沒錯, 就是並發問題,對於ASP.NET Core的依賴注入提供的Singleton和Scoped的實例來說, 它是很有可能同時被多個地方獲取並調用的。通過下面的例子看一下這個問題, 順便鞏固一下上面的內容。
public interface ITest { Guid Guid { get; } string Name { get; set; } } public class Test : ITest { public Test() { this.Guid = Guid.NewGuid(); } public Guid Guid { get; } public string Name { get; set; } }
一個Test類繼承自ITest, 為了方便比較是不是同一個實例, 在構造方法里對它的Guid屬性賦一個新值, 然后將其注冊一下
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddTransient<ITest,Test>(); }
現在通過三種方法來獲取這個Test, Controller中如下
public class HomeController : Controller { private ITest _test; public HomeController( ITest test) { this._test = test; } public IActionResult Index() { ViewBag.Test = this._test; //構造方法獲取 ViewBag.TestFromContext = HttpContext.RequestServices.GetService<ITest>(); //通過HttpContext獲取 需要using Microsoft.Extensions.DependencyInjection return View(); } }
View中通過@inject ITest viewITest的方式獲取, 然后把他們的Guid值顯示出來:
@inject ITest viewITest
<ul>
<li>@ViewBag.Test.Guid</li>
<li>@ViewBag.TestFromContext.Guid</li>
<li>@viewITest.Guid</li>
</ul>
結果如下
ad79690e-1ee2-41bd-82f1-062de4c124b2 92cd97fc-7083-4b10-99e4-13b6b6926c16 cd0105f4-fa9d-4221-b395-af06798d96a2
說明三種方式獲取了三個不同的實例, 刷新一下頁面, 又變成了另外三個不同的值.
現在在startup文件中將原來的 services.AddTransient<ITest,Test>() 改為 services.AddSingleton<ITest,Test>() , 其他不變, 重新運行一下, 結果如下
dd4c952e-b64c-4dc8-af01-2b9d667cf190 dd4c952e-b64c-4dc8-af01-2b9d667cf190 dd4c952e-b64c-4dc8-af01-2b9d667cf190
發現三組值是一樣的, 說明獲得的是同一個實例, 在刷新一下頁面, 仍然是這三組值, 說明多次請求獲得的結果也是同一個實例.
再將 services.AddSingleton<ITest,Test>() 改為 services.AddScoped<ITest,Test>() , 重新運行, 這次結果是
ad5a600b-75fb-43c0-aee9-e90231fd510c ad5a600b-75fb-43c0-aee9-e90231fd510c ad5a600b-75fb-43c0-aee9-e90231fd510c
三組數字相同, 刷新一下, 又變成了另外三組一樣的值, 這說明在同一次請求里, 獲取的實例是同一個。
因為無論在Singleton還是Scoped的情況下, 可能在應用的多個地方同時使用同一個實例, 所以在程序設置的時候就要注意了, 如果存在像在上面的Test有個Name屬性提供了 { get; set; }的時候,多個引用者處理它的值, 會造成一些不可預料的錯誤。
5.服務的Dispose
對於每次請求, 我們最初配置的根IServiceProvider通過CreateScope()創建了一個新的IServiceScope, 而這個IServiceScope的ServiceProvider屬性將負責本次該次請求的服務提供, 當請求結束, 這個ServiceProvider的dispose會被調用, 同時它負責由它創建的各個服務。
在 1.0 版中,ServiceProvider將對所有 IDisposable
對象調用 dispose,包括那些並非由它創建的對象。
而在2.0中, ServiceProvider只調用由它創建的 IDisposable
類型的 Dispose
。 如果將一個實例添加到容器,它將不會被釋放。
例如: services.AddSingleton<ITest>(new Test());
6.我想換個容器
可以將默認的容器改為其他的容器, 比如Autofac,這需要將ConfigureServices方法由返回void改為IServiceProvider。
1 public IServiceProvider ConfigureServices(IServiceCollection services) 2 { 3 services.AddMvc(); 4 // Add other framework services 5 6 // Add Autofac 7 var containerBuilder = new ContainerBuilder(); 8 containerBuilder.RegisterModule<DefaultModule>(); 9 containerBuilder.Populate(services); 10 var container = containerBuilder.Build(); 11 return new AutofacServiceProvider(container); 12 }