【 .NET Core 3.0 】框架之九 || 依赖注入 与 IoC
本文有配套視頻:https://www.bilibili.com/video/av58096866/?p=5
?前言
?
1、重要:如果你實現了解耦,也就是 api 層只引用了 IService 和 IRepository 的話,那每次修改 service 層,都需要清理解決方案,重新編譯項目,因為這個時候你的api層的dll,還是之前未修改的代碼。
?
2、重要+ :請注意,依賴注入的目的不是為了解耦,依賴注入是為了控制反轉,通俗來說,就是不用我們自己去 new 服務實例了,所以大家不需要一定去解耦(比如下文說到的我沒有引用 Service層 和 Repository層),我下一個DDD系列,依賴注入就沒有解耦,因為我用的是自帶的注入,不是Autofac的反射dll ,我解耦的目的,是為了讓大家更好的理解,服務是怎么注入到宿主容器里的。
?
說接上文,上回說到了《八 || API項目整體搭建 6.3 異步泛型+依賴注入初探》,后來的標題中,我把倉儲兩個字給去掉了,因為好像大家對這個模式很有不同的看法,嗯~可能還是我學藝不精,沒有說到其中的好處,現在在學DDD領域驅動設計相關資料,有了好的靈感再給大家分享吧。
雖然項目整體可以運行了,但是我還有幾個小知識點要說下,主要是1、依賴注入和AOP相關知識;2、跨域代理等問題(因為Vue是基于Node開發的,與后臺API接口不在同一個地址);3、實體類的DTO相關小問題;4、Redis緩存等;5、部署服務器中的各種坑;雖然都是很小的知識點,我還是都下給大家說下的,好啦,開始今天的講解;
零、今天完成的綠色部分
?
?
一、依賴注入的理解和思考
?
更新(19-04-17):如果想好好的理解依賴注入,可以從以下幾個方面入手:
1、項目之間引用是如何起作用的,比如為啥 api 層只是引用了 service 層,那為啥也能使用 repository 和 model 等多層的類?
2、項目在啟動的時候,也就是運行時,是如何動態 獲取和訪問 每一個對象的實例的?也就是 new 的原理
3、項目中有 n 個類,對應 m 個實例等,那這些服務,都放在了哪里?肯定每一個項目都有專屬自己的一塊。如果項目不啟動的話,內存里肯定是沒有這些服務的。
4、使用接口(面向抽象)的好處?
5、在項目后期,如何業務需要要全部修改接口的實現類,比如想把 IA = new A();全部? 改成 IA = new B();
6、反射的重要性,為什么要用到反射 dll ?
如果這些每一條自己都能說清楚,那肯定就知道依賴注入是干啥的了。
?
說到依賴,我就想到了網上有一個例子,依賴注入和工廠模式中的相似和不同:
(1)原始社會里,沒有社會分工。須要斧子的人(調用者)僅僅能自己去磨一把斧子(被調用者)。相應的情形為:軟件程序里的調用者自己創建被調用者。(2)進入工業社會,工廠出現。斧子不再由普通人完畢,而在工廠里被生產出來,此時須要斧子的人(調用者)找到工廠,購買斧子,無須關心斧子的制造過程。相應軟件程序的簡單工廠的設計模式。(3)進入“按需分配”社會,需要斧子的人不需要找到工廠,我們只需要去干活就行,斧子已經給我們自動準備好了,直接用就可以了。
?
?
首先,我們需要了解下什么是控制反轉IOC,舉個栗子,我在之前開發簡單商城的時候,其中呢,訂單模塊,有訂單表,那里邊肯定有訂單詳情表,而且呢訂單詳情表中還有商品信息表,商品信息表還關聯了價格規格表,或者其他的物流信息,商家信息,當然,我們可以放到一個大表里,可是你一定不會這么做,因為太龐大,所以必定分表,那必定會出現類中套類的局面,這就是依賴,比如上邊的,訂單表就依賴了詳情表,我們在實例化訂單實體類的時候,也需要手動實例詳情表,當然,EF框架中,會自動生成。不過倘若有一個程序員把詳情表實體類改錯了,那訂單表就崩潰了,哦不!我是遇到過這樣的情景。
?
怎么解決這個問題呢,就出現了控制反轉。網上看到一個挺好的講解:
1、沒有引入IOC之前,對象A依賴于對象B,那么對象A在初始化或者運行到某一點的時候,A直接使用new關鍵字創建B的實例,程序高度耦合,效率低下,無論是創建還是使用B對象,控制權都在自己手上。
2、軟件系統在引入IOC容器之后,這種情形就完全改變了,由于IOC容器的加入,對象A與對象B之間失去了直接聯系,所以,當對象A運行到需要對象B的時候,IOC容器會主動創建一個對象B注入到對象A需要的地方。
3、依賴注入,是指程序運行過程中,如果需要調用另一個對象協助時,無須在代碼中創建被調用者,而是依賴于外部的注入。Spring的依賴注入對調用者和被調用者幾乎沒有任何要求,完全支持對POJO之間依賴關系的管理。依賴注入通常有兩種:
這個就是依賴注入的方式。
什么是控制反轉(IoC)
Inversion of Control,英文縮寫為IoC,不是什么技術,而是一種設計思想。
???
簡單來說就是把復雜系統分解成相互合作的對象,這些對象類通過封裝以后,內部實現對外部是透明的,從而降低了解決問題的復雜度,而且可以靈活地被重用和擴展。IOC理論提出的觀點大體是這樣的:借助于“第三方”實現具有依賴關系的對象之間的解耦,如下圖:
?
大家看到了吧,由于引進了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個對象沒有了耦合關系,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“黏合劑”的作用,把系統中的所有對象粘合在一起發揮作用,如果沒有這個“黏合劑”,對象與對象之間會彼此失去聯系,這就是有人把IOC容器比喻成“黏合劑”的由來。
我們再來做個試驗:把上圖中間的IOC容器拿掉,然后再來看看這套系統:
?
????我們現在看到的畫面,就是我們要實現整個系統所需要完成的全部內容。這時候,A、B、C、D這4個對象之間已經沒有了耦合關系,彼此毫無聯系,這樣的話,當你在實現A的時候,根本無須再去考慮B、C和D了,對象之間的依賴關系已經降低到了最低程度。所以,如果真能實現IOC容器,對于系統開發而言,這將是一件多么美好的事情,參與開發的每一成員只要實現自己的類就可以了,跟別人沒有任何關系!
在上面文章中,我們已經了解到了,什么是依賴倒置、控制反轉(IOC),什么是依賴注入(DI),網上這個有很多很多的講解,我這里就不說明了,其實主要是見到這樣的,就是存在依賴
public class A : D { public A(B b) { // do something } C c = new C(); }?
就比如我們的項目中的BlogController,只要是通過new 實例化的,都是存在依賴
?
使用依賴注入呢,有以下優點:
- 傳統的代碼,每個對象負責管理與自己需要依賴的對象,導致如果需要切換依賴對象的實現類時,需要修改多處地方。同時,過度耦合也使得對象難以進行單元測試。 
- 依賴注入把對象的創造交給外部去管理,很好的解決了代碼緊耦合(tight couple)的問題,是一種讓代碼實現松耦合(loose couple)的機制。 
- 松耦合讓代碼更具靈活性,能更好地應對需求變動,以及方便單元測試。 
舉個栗子,就是關于日志記錄的
日志記錄:有時需要調試分析,需要記錄日志信息,這時可以采用輸出到控制臺、文件、數據庫、遠程服務器等;假設最初采用輸出到控制臺,直接在程序中實例化ILogger logger = new ConsoleLogger(),但有時又需要輸出到別的文件中,也許關閉日志輸出,就需要更改程序,把ConsoleLogger改成FileLogger或者NoLogger, new FileLogger()或者new SqlLogger() ,此時不斷的更改代碼,就顯得心里不好了,如果采用依賴注入,就顯得特別舒暢。
二、常見的IoC框架有哪些
1、Autofac+原生
我常用的還是原生注入和 Autofac 注入。
Autofac:貌似目前net下用的最多吧Ninject:目前好像沒多少人用了Unity:也是較為常見
微軟 core 自帶的 DI
其實.Net Core 有自己的輕量級的IoC框架,
ASP.NET Core本身已經集成了一個輕量級的IOC容器,開發者只需要定義好接口后,在Startup.cs的ConfigureServices方法里使用對應生
命周期的綁定方法即可,常見方法如下
services.AddTransient<IApplicationService,ApplicationService>//服務在每次請求時被創建,它最好被用于輕量級無狀態服務(如我們的Repository和ApplicationService服務)
services.AddScoped<IApplicationService,ApplicationService>//服務在每次請求時被創建,生命周期橫貫整次請求
services.AddSingleton<IApplicationService,ApplicationService>//Singleton(單例) 服務在第一次請求時被創建(或者當我們在ConfigureServices中指定創建某一實例并運行方法),其后的每次請求將沿用已創建服務。如果開發者的應用需要單例服務情景,請設計成允許服務容器來對服務生命周期進行操作,而不是手動實現單例設計模式然后由開發者在自定義類中進行操作。
?當然.Net Core自身的容器還是比較簡單,如果想要更多的功能和擴展,還是需要使用上邊上個框架。
?
2、三種注入的生命周期?
權重:
AddSingleton→AddTransient→AddScoped
AddSingleton的生命周期:
項目啟動-項目關閉 ? 相當于靜態類 ?只會有一個 ?
AddScoped的生命周期:
請求開始-請求結束 ?在這次請求中獲取的對象都是同一個?
AddTransient的生命周期:
請求獲取-(GC回收-主動釋放) 每一次獲取的對象都不是同一個
?
這里來個簡單的小DEMO:
1、定義四個接口,并分別對其各自接口實現,目的是測試Singleton,Scope,Transient三種,以及最后的 Service 服務:
public interface ISingTest { int Age { get; set; } string Name { get; set; } } public class SingTest: ISingTest { public int Age { get; set; } public string Name { get; set; } } //-------------------------- public interface ISconTest { int Age { get; set; } string Name { get; set; } } public class SconTest: ISconTest { public int Age { get; set; } public string Name { get; set; } } //-------------------------- public interface ITranTest { int Age { get; set; } string Name { get; set; } } public class TranTest: ITranTest { public int Age { get; set; } public string Name { get; set; } } //----------------------- public interface IAService { void RedisTest(); } public class AService : IAService { private ISingTest sing; ITranTest tran; ISconTest scon; public AService(ISingTest sing, ITranTest tran, ISconTest scon) { this.sing = sing; this.tran = tran; this.scon = scon; } public void RedisTest() { } }?
2、項目注入
?
?
3、控制器調用
?
4、開始測試,三種注入方法出現的情況?
請求SetTest?// GET api/values
?
AddSingleton的對象沒有變
AddScoped的對象沒有變化
AddTransient的對象發生變化
------------------------------------------------------------
請求?// GET api/values/5
?
AddSingleton的對象沒有變
AddScoped的對象發生變化
AddTransient的對象發生變化
?
注意:
由于AddScoped對象是在請求的時候創建的
所以不能在AddSingleton對象中使用
甚至也不能在AddTransient對象中使用
?
所以權重為
AddSingleton→AddTransient→AddScoped
?
不然則會拋如下異常
?
三、較好用的IoC框架使用——Autofac
首先呢,我們要明白,我們注入是要注入到哪里——Controller API層。然后呢,我們看到了在接口調用的時候,如果需要其中的方法,需要using兩個命名空間
?
接下來我們就需要做處理:
?
1、引入nuget包
在Nuget中引入兩個:Autofac.Extras.DynamicProxy(Autofac的動態代理,它依賴Autofac,所以可以不用單獨引入Autofac)、Autofac.Extensions.DependencyInjection(Autofac的擴展),注意是最新版本的。
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="5.0.0" /> <PackageReference?Include="Autofac.Extras.DynamicProxy"?Version="4.5.0"?/>?
?
2、配置容器,注入服務
在startup.cs 文件中,增加一個方法,用來配置Autofac 服務容器
首先我們創建一個接口和對應的實現類:
public interface IAdvertisementServices { int Test(); } public class AdvertisementServices : IAdvertisementServices { public int Test() { return 1; } }然后將這個服務,注入到Autofac 容器里:
public void ConfigureContainer(ContainerBuilder builder) { var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; //直接注冊某一個類和接口 //左邊的是實現類,右邊的As是接口 builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>(); //注冊要通過反射創建的組件 var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll"); var assemblysServices = Assembly.LoadFrom(servicesDllFile); builder.RegisterAssemblyTypes(assemblysServices) .AsImplementedInterfaces() .InstancePerLifetimeScope() .EnableInterfaceInterceptors(); }
?
這個時候我們就把 AdvertisementServices的new 實例化過程注入到了Autofac容器中,
這個時候要看明白,前邊的是實現類,后邊的是接口,順序不要搞混了。
?
3、使用服務工廠,將Autofac容器添加到Host
為什么要這么做呢,從上邊你也應該看到了,我們現在僅僅是配置了服務和容器,還沒有添加到我們的項目宿主里,那我們的controller就拿不到相應的服務。
我們需要在 Program.cs 文件中,配置 UseServiceProviderFactory
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseServiceProviderFactory(new AutofacServiceProviderFactory()) //<--NOTE THIS .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });?
4、使用構造函數方式來注入
依賴注入有三種方式(構造函數注入、屬性注入和方式注入),我們平時基本都是使用其中的構造函數方式實現注入,
大家還記得默認控制器 WeatherForecastController 么,當時我們說到了,已經有了一個依賴注入的用法,就是 ILogger,那現在我們繼續注入,
?
?
5、效果調試,已經成功
然后運行調試,發現在斷點剛進入的時候,接口已經被實例化了,達到了注入的目的。
?
注意:這里經常會遇到一個錯誤:None of the constructors found with ........,
查看你的service服務,是不是用了其他的倉儲repository,但是又缺少了構造函數。
?
?
如果沒有問題,大家就需要想想,除了 Autofac 還有沒有其他的不用第三方框架的注入方法呢?聰明如你,netcore 還真自帶了注入擴展。
?
6、NetCore 自帶的注入實現效果
當然,我們用 Asp.net core 自帶的注入方式也是可以的,也挺簡單的,這里先說下使用方法:
?
這個時候,我們發現已經成功的注入了,而且特別簡單,那為啥還要使用 Autofac 這種第三方擴展呢,我們想一想,上邊我們僅僅是注入了一個 Service ,但是項目中有那么多的類,都要一個個手動添加么,多累啊,答案當然不是滴~
?
?
四、整個 dll 程序集的注入
1、服務程序集注入方式 —— 未解耦
通過反射將 Blog.Core.Services 和 Blog.Core.Repository 兩個程序集的全部方法注入
修改如下代碼,注意這個時候需要在項目依賴中,右鍵,添加引用 Blog.Core.Services 層和 Repository 層 到項目中,如下圖,這個時候我們的程序依賴了具體的服務:
?
核心代碼如下,注意這里是 Load 模式(程序集名),還是在startup.cs 文件中,配置Autofac容器。
?
其他不變,運行項目,一切正常,換其他接口也可以,具體的就不細說。
到這里,Autofac依賴注入已經完成,基本的操作就是這樣,不過可能你還沒有真正體會到注入的好處,挑戰下吧,看看下邊的內容,將層級進行解耦試試!
?
?
?
2、程序集注入 —— 實現層級解耦
?
這是一個學習的思路,大家要多想想,可能會感覺無聊或者沒用,但是對理解項目啟動和加載,還是很有必要的。
1、項目最終只依賴抽象
最終的效果是這樣的:工程只依賴抽象,把兩個實現層刪掉,引用這兩個接口層。
?
?
2、配置倉儲和服務層的程序集輸出
將 Blog.Repository 層和 Service 層項目生成地址改成相對路徑,這樣大家就不用手動拷貝這兩個 dll 了,F6編譯的時候就直接生成到了 api 層 bin 下了:
“...\Blog.Core\bin\Debug\”
?
?
3、使用 LoadFile 加載服務層的程序集
?
這個時候,可能我們編譯成功后,頁面能正常啟動,證明我們已經把 Service 和 Repository 兩個服務層的所有服務給注冊上了,但是訪問某一個接口,還是會出現錯誤:
?
?
這個錯誤表示,我們的 SqlSugar 服務,沒有被注冊成功,那肯定就是我們的 Sqlsugar 程序集沒有正常的引用,怎么辦呢,直接在 api 層引用下就行。
?
?
?
?
4、解除Service層和Repository層之間的耦合
還記得Blog.Core.Services中的BaseServices.cs么,它還是通過new 實例化的方式在創建,仿照contrller,修改BaseServices并在全部子類的構造函數中注入:
public class BaseServices<TEntity> : IBaseServices<TEntity> where TEntity : class, new() { //public IBaseRepository<TEntity> baseDal = new BaseRepository<TEntity>(); public IBaseRepository<TEntity> baseDal;//通過在子類的構造函數中注入,這里是基類,不用構造函數 //... } public class AdvertisementServices : BaseServices<Advertisement>, IAdvertisementServices { IAdvertisementRepository dal; public AdvertisementServices(IAdvertisementRepository dal) { this.dal = dal; base.baseDal = dal; } }?
?
好啦,現在整個項目已經完成了相互直接解耦的功能,以后就算是Repository和Service如何變化,接口層都不用修改,因為已經完成了注入,第三方Autofac會做實例化的過程。
?
5、容器內查看注入的服務數據
如果你想看看是否注入到容器里成功了,可以直接看看容器 ApplicationContainer 的內容:
?
?
五、 無接口項目注入
1、接口形式的注入
上邊我們討論了很多,但是都是接口框架的,
比如:Service.dll 和與之對應的 IService.dll,Repository.dll和與之對應的 IRepository.dll,
這樣,我們在多層之間使用服務的話,直接將我們需要使用的 new 對象,注入到容器里,然后我們就可以使用相應的接口了,
比如:我們想在 controller 里使用AdvertisementServices 類,那我們就可以直接使用它的接口 IAdvertisementServices,這樣就很好的達到了解耦的目的,這樣我們就可以在API層,就輕松的把 Service.dll 給解耦了;
如果我們需要在 Service類里,使用 AdvertisementRepository ,我們就直接使用對應的接口 IAdvertisementRepository,這樣,我們就從 Service 層中,把倉儲層給解耦了。
?
?
2、如果沒有接口
案例是這樣的:
?如果我們的項目是這樣的,沒有接口,會怎么辦:
?
這樣的話,我們就不能使用上邊的接口注入模式了,因為我們上邊是把注入的服務,對應注冊給了接口了 .AsImplementedInterfaces() ,我們就無法實現解耦了,因為根本沒有了接口層,所以我們只能引用實現類層,這樣注入:
?
?
通過 builder.RegisterAssemblyTypes(assemblysRepository); 方法直接注入服務,沒有其他的東西。
?
3、如果是沒有接口的單獨實體類
?
?
六、同一接口多實現類注入
這里暫時沒有實例代碼,如果你正好需要,可以看看這個博友的栗子:https://www.cnblogs.com/fuyujian/p/4115474.html
我會在之后的時間寫個栗子放到這里。
?
?
?
七、CODE
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core
?
總結
以上是生活随笔為你收集整理的【 .NET Core 3.0 】框架之九 || 依赖注入 与 IoC的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 程序员后期,架构师发展路线!
- 下一篇: 10月数据库排行:Microsoft S
