你有把依赖注入玩坏?
【導讀】自從.NET Core給我們呈現了依賴注入,在我們項目中到處充滿著依賴注入,雖然一切都已幫我們封裝好,但站在巨人的肩膀上,除了憑眺遠方,我們也應平鋪好腳下的路
使用依賴注入不僅僅只是解耦,而且使代碼更具維護性,同時我們也可輕而易舉查看依賴關系,單元測試也可輕松完成,本文我們來聊聊依賴注入,文中示例版本皆為5.0。
淺談依賴注入
在話題開始前,我們有必要再提一下三種服務注入生命周期,?由淺及深再進行講解,基礎內容,我這里不再多述廢話
Transient(瞬時):每次對瞬時的檢索都會創建一個新的實例。
Singleton(單例):僅被實例化一次。此類型請求,總是返回相同的實例。
Scope(范圍):使用范圍內的注冊。將在請求類型的每個范圍內創建一個實例。
如果已用過.NET Core一段時間,若對上述三種生命周期管理的概念沒有更深刻的理解,我想有必要基礎回爐重塑下。為什么?至少我們應該得出兩個基本結論
其一:生命周期由短到長排序,瞬時最短、范圍次之、單例最長
只要做過Web項目,關于第一點就很好理解,首先我們只對瞬時和范圍作一個基本的概述,關于單例通過實際例子來闡述,我們理解會更深刻
若為瞬時:那么我們每次從容器中獲取的服務將是不同的實例,所以名為瞬時或短暫
若為范圍:在ASP.NET Core中,針對每個HTTP請求都會創建DI范圍,當在HTTP請求中(在中間件,控制器,服務或視圖中)請求服務,并且該服務注冊為范圍服務時,如果在請求中多次請求相同類型的請求,則使用相同實例。例如,如果在控制器,服務和視圖中注入了范圍服務,則將返回相同的實例。隨著另一個HTTP請求的流,使用了不同的實例,請求完成后,將處理(釋放)范圍
其二:被注入的服務應與注入的服務應具有相同或更長的生命周期
從概念上看貌似有點拗口,通過日常生活舉個栗子則秒懂,假設有兩個桶,一個小桶和一個大桶,我們能將小桶裝進大桶,但不能將大桶裝進小桶。
專業一點講,比如一個單例服務可以被注入瞬時服務,但是一個瞬時服務不能被注入單例服務,因為單例服務比瞬時服務生命周期更長,若瞬時服務被注入單例服務,那么勢必將延長瞬時服務生命周期,因違背大前提,將會引起異常
我們在Web中進行演示,然后在Startup中根據其接口名進行注冊,如下:
services.AddSingleton<ISingletonDemo1,?SingletonDemo1>(); services.AddScoped<IScopeDemo1,?ScopeDemo1>();從理論上講肯定是這樣,好像有點太絕對,抱著自我懷疑的態度,于是乎,我們在控制臺中驗證一下看看
static?void?Main(string[]?args) {var?services?=?new?ServiceCollection();services.AddSingleton<ISingletonDemo1,?SingletonDemo1>();services.AddScoped<IScopeDemo1,?ScopeDemo1>();services.BuildServiceProvider(); }然鵝并沒有拋出任何異常,注入操作都一樣,有點懵,看看各位看官能否給個合理的解釋,在控制臺中并不會拋出異常......
深談依賴注入
關于依賴注入基礎和使用準則,我建議大家去看看,還是有很多細節需要注意
依賴注入設計準則
https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines
在.NET?Core中使用依賴注入
https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage
比如其中提到一點,服務容器并不會創建服務,也就是說如下框架并沒有自動處理服務,需要我們開發人員自己負責處理服務的釋放
public?void?ConfigureServices(IServiceCollection?services) {services.AddSingleton(new?ExampleService());//?... }假設我們有一個控制臺命令行項目,我們通過引入依賴注入單例做一些操作
public?interface?ISingletonService {void?Execute(); }public?class?SingletonService?:?ISingletonService {public?void?Execute(){} }緊接著控制臺入口點演變成如下這般
若在執行Execute方法里面做了一些臨時操作,比如創建臨時文件,我們想在釋放時手動做一些清理,所以我們實現IDisposable接口,如下:
然后項目上線,我們可能會發現內存中大量充斥著該實例,從而最終導致內存泄漏,這是為何呢?
我們將服務注入到容器中,容器將會自動管理注入實例的釋放,根據如下可知
最終我們通過如下方式即可解決上述內存泄漏問題
是不是有點懵,接下來我們來深入探討三種類型生命周期釋放問題,尤其是單例,首先我們通過注入自增長來標識每一個注入服務,便于查看釋放時機對應標識
接下來則是定義瞬時、范圍、單例服務,并將其進行注入,如下:
最后在入口注入并調用相關服務,再加上最后打印結果,應該挺好理解的
static?void?Main(string[]?args) {var?services?=?new?ServiceCollection();services.AddSingleton<ICountService,?CountService>();services.AddSingleton<ISingletonService,?SingletonService>();services.AddScoped<IScopeSerivice,?ScopeSerivice>();services.AddTransient<ITransientService,?TransientService>();using?(var?serviceProvider?=?services.BuildServiceProvider()){using?(var?scope1?=?serviceProvider.CreateScope()){var?s1a1?=?scope1.ServiceProvider.GetService<IScopeSerivice>();s1a1.Say();var?s1a2?=?scope1.ServiceProvider.GetService<IScopeSerivice>();s1a2.Say();var?s1b1?=?scope1.ServiceProvider.GetService<ISingletonService>();s1b1.Say();var?s1c1?=?scope1.ServiceProvider.GetService<ITransientService>();s1c1.Say();var?s1c2?=?scope1.ServiceProvider.GetService<ITransientService>();s1c2.Say();Console.WriteLine("--------------------------------釋放分界線");}Console.WriteLine("--------------------------------結束范圍1");Console.WriteLine();using?(var?scope2?=?serviceProvider.CreateScope()){var?s2a1?=?scope2.ServiceProvider.GetService<IScopeSerivice>();s2a1.Say();var?s2b1?=?scope2.ServiceProvider.GetService<ISingletonService>();s2b1.Say();var?s2c1?=?scope2.ServiceProvider.GetService<ITransientService>();s2c1.Say();}Console.WriteLine("--------------------------------結束范圍2");}Console.ReadKey(); }我們描述下整個過程,通過容器創建一個scope1和scope2,并依次調用范圍、單例、瞬時服務,然后在scope和scope2結束時,釋放瞬時、范圍服務。最終在容器結束時,才釋放單例服務
從獲取、釋放以及打印結果來看,我們可以得出兩個結論
其一:每一個scope被釋放時,瞬時和范圍服務都會被釋放,且釋放順序為倒置
其二:單例服務在根容器釋放時才會被釋放
有了上述結論2不難解釋我們首先給出的假設控制臺命令行項目為何會導致內存泄漏,若非手動實例化,實例對象生命周期都將由容器管理,但在構建容器時,我們并未釋放(使用using),所以當我們手動實現IDisposable接口,通過實現Dispose方法進行后續清理工作,但并不會進入該方法,所以會導致內存泄漏
看到這里,我相信有一部分童鞋會有點大跌眼鏡,因為和沉浸在自我想象中的樣子不一致,實踐是檢驗真理的唯一標準,最后我們對依賴注入做一個總結
在容器中注冊服務,容器為了處理所有注冊實例,容器會跟蹤所有對象,即使是瞬時服務,也并不是檢索完后,就一次性進行釋放,它依然在容器中保持“活躍”狀態,同時我們也應防止GC釋放超出其范圍的瞬時服務
即使是瞬時服務也和作用域(scope)有關,通過引入作用域而進行釋放,否則根容器會一直保存其實例對象,造成巨大的內存損耗,甚至是內存泄漏
?????瞬時服務可作為注冊服務的首選方法,范圍和單例用于共享狀態
?????每一個scope被釋放時,瞬時和范圍服務都會被釋放,且釋放順序為倒置
?????單例服務從不與作用域關聯,它們與根容器關聯,并在處置根容器時處理。
總結
以上是生活随笔為你收集整理的你有把依赖注入玩坏?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Visual Studio将原生支持WS
- 下一篇: 如何向K8s,Docker-Compos