1. 程式人生 > >從壹開始前後端分離【 .NET Core2.0 +Vue2.0 】框架之九 || 依賴注入IoC學習 + AOP介面程式設計初探

從壹開始前後端分離【 .NET Core2.0 +Vue2.0 】框架之九 || 依賴注入IoC學習 + AOP介面程式設計初探

更新

1、感謝@博友的提醒,目前是vue-cli腳手架是3.0.1,vue的版本還是2.5.17,下文已改,感謝糾錯!

3、感謝網友@ 的提醒,大家下載程式碼以後,如果自己要配置Repository.dll 和 service.dll 這兩個dll內容,比如修改 repository.dll 的資料庫連線字串,需要自己編譯後拷貝進Blog.Core\bin\Debug\netcoreapp2.1目錄下 ,因為現在已經解耦了,更新這連個專案不會同步到 api 層。

程式碼已上傳Github+Gitee,文末有地址

  說接上文,上回說到了《從壹開始前後端分離【 .NET Core2.0 Api + Vue 2.0 + AOP + 分散式】框架之八 || API專案整體搭建 6.3 非同步泛型+依賴注入初探

》,後來的標題中,我把倉儲兩個字給去掉了,因為好像大家對這個模式很有不同的看法,嗯~可能還是我學藝不精,沒有說到其中的好處,現在在學DDD領域驅動設計相關資料,有了好的靈感再給大家分享吧。

  到目前為止我們的專案已經有了基本的雛形,後端其實已經可以搭建自己的介面列表了,框架已經有了規模,原本應該說vue了,但是呢,又聽說近來Vue-cli已經從2.0升級到了3.0了,還變化挺大,前端大佬們,都不停歇呀。當然我還在學習當中,我也需要了解下有關3.0的特性,希望給沒有接觸到,或者剛剛接觸到的朋友們,有一些幫助,當然我這個不是隨波逐流,只是在眾多的博文中,給大家一個入門參考,屆時說3.0的時候,還是會說2.0的相關問題的。

  雖然專案整體可以運行了,但是我還有幾個小知識點要說下,主要是1、依賴注入和AOP相關知識;2、跨域代理等問題(因為Vue是基於Node開發的,與後臺API介面不在同一個地址);3、實體類的DTO相關小問題;4、Redis快取等;5、部署伺服器中的各種坑;雖然都是很小的知識點,我還是都下給大家說下的,好啦,開始今天的講解;

零、今天完成的綠色部分

 

一、依賴注入的理解和思考

說到依賴,我就想到了網上有一個例子,依賴注入和工廠模式中的相似和不同:

(1)原始社會裡,沒有社會分工。須要斧子的人(呼叫者)僅僅能自己去磨一把斧子(被呼叫者)。相應的情形為:Java程式裡的呼叫者自己建立被呼叫者。

(2)進入工業社會,工廠出現。斧子不再由普通人完畢,而在工廠裡被生產出來,此時須要斧子的人(呼叫者)找到工廠,購買斧子,無須關心斧子的製造過程。相應Java程式的簡單工廠的設計模式。

(3)進入“按需分配”社會,須要斧子的人不須要找到工廠,坐在家裡發出一個簡單指令:須要斧子。斧子就自然出如今他面前。相應Spring的依賴

注入

在上篇文章中,我們已經瞭解到了,什麼是依賴倒置、控制反轉(IOC),什麼是依賴注入(DI),網上這個有很多很多的講解,我這裡就不說明了,其實主要是見到這樣的,就是存在依賴

public class A : D
{

    public A(B b)
    {
        // do something   
    }
    C c = new C();
}

就比如我們的專案中的BlogController,只要是通過new 例項化的,都是存在依賴

public async Task<List<Advertisement>> Get(int id)
{
    IAdvertisementServices advertisementServices = new AdvertisementServices();
    return await advertisementServices.Query(d => d.Id == id);
}

使用依賴注入呢,有以下優點:

  • 傳統的程式碼,每個物件負責管理與自己需要依賴的物件,導致如果需要切換依賴物件的實現類時,需要修改多處地方。同時,過度耦合也使得物件難以進行單元測試。
  • 依賴注入把物件的創造交給外部去管理,很好的解決了程式碼緊耦合(tight couple)的問題,是一種讓程式碼實現鬆耦合(loose couple)的機制。
  • 鬆耦合讓程式碼更具靈活性,能更好地應對需求變動,以及方便單元測試。

舉個栗子,就是關於日誌記錄的

日誌記錄:有時需要除錯分析,需要記錄日誌資訊,這時可以採用輸出到控制檯、檔案、資料庫、遠端伺服器等;假設最初採用輸出到控制檯,直接在程式中例項化ILogger logger = new ConsoleLogger(),但有時又需要輸出到別的檔案中,也許關閉日誌輸出,就需要更改程式,把ConsoleLogger改成FileLogger或者NoLogger, new FileLogger()或者new SqlLogger() ,此時不斷的更改程式碼,就顯得心裡不好了,如果採用依賴注入,就顯得特別舒暢。

我有一個個人的理解,不知道恰當與否,比如我們平時食堂吃飯,都是食堂自己炒的菜,就算是配方都一樣,控制者(廚師)還是會有些變化,或者是出錯,但是肯德基這種,由總店提供的,店面就失去了控制,就出現了第三方(總店或者工廠等),這就是實現了控制反轉,我們只需要說一句,奧爾良雞翅,嗯就拿出來一個,而且全部分店的都一樣,我們不用管是否改配方,不管是否依賴哪些調理食材,哈哈。

二、常見的IoC框架有哪些

Autofac:貌似目前net下用的最多吧
Ninject:目前好像沒多少人用了
Unity:也是較為常見

微軟 core 自帶的IoC

其實.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自身的容器還是比較簡單,如果想要更多的功能和擴充套件,還是需要使用上邊上個框架。

三、較好用的IoC框架使用——Autofac

首先呢,我們要明白,我們注入是要注入到哪裡——Controller API層。然後呢,我們看到了在介面呼叫的時候,如果需要其中的方法,需要using兩個名稱空間

     [HttpGet("{id}", Name = "Get")]
        public async Task<List<Advertisement>> Get(int id)
        {
            IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用兩個名稱空間Blog.Core.IServices;Blog.Core.Services;

            return await advertisementServices.Query(d => d.Id == id);
        }

接下來我們就需要做處理:

1、在Nuget中引入兩個:Autofac.Extras.DynamicProxy(Autofac的動態代理,它依賴Autofac,所以可以不用單獨引入Autofac)、Autofac.Extensions.DependencyInjection(Autofac的擴充套件)

2、讓Autofac接管Starup中的ConfigureServices方法,記得修改返回型別IServiceProvider

     public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            #region 配置資訊
            //Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value;
            #endregion

            #region Swagger
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Version = "v0.1.0",
                    Title = "Blog.Core API",
                    Description = "框架說明文件",
                    TermsOfService = "None",
                    Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "[email protected]", Url = "https://www.jianshu.com/u/94102b59cc2a" }
                });

                //就是這裡

                #region 讀取xml資訊
                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
                var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//這個就是剛剛配置的xml檔名
                var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//這個就是Model層的xml檔名
                c.IncludeXmlComments(xmlPath, true);//預設的第二個引數是false,這個是controller的註釋,記得修改
                c.IncludeXmlComments(xmlModelPath);
                #endregion

                #region Token繫結到ConfigureServices
                //新增header驗證資訊
                //c.OperationFilter<SwaggerHeader>();
                var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, };
                c.AddSecurityRequirement(security);
                //方案名稱“Blog.Core”可自定義,上下一致即可
                c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme
                {
                    Description = "JWT授權(資料將在請求頭中進行傳輸) 直接在下框中輸入{token}\"",
                    Name = "Authorization",//jwt預設的引數名稱
                    In = "header",//jwt預設存放Authorization資訊的位置(請求頭中)
                    Type = "apiKey"
                }); 
                #endregion


            });
            #endregion

            #region Token服務註冊
            services.AddSingleton<IMemoryCache>(factory =>
             {
                 var cache = new MemoryCache(new MemoryCacheOptions());
                 return cache;
             });
            services.AddAuthorization(options =>
            {
                options.AddPolicy("Admin", policy => policy.RequireClaim("AdminType").Build());//註冊許可權管理,可以自定義多個
            });
            #endregion

            #region AutoFac
            
            //例項化 AutoFac  容器   
            var builder = new ContainerBuilder();

            //註冊要通過反射建立的元件
            builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>();

            //將services填充到Autofac容器生成器中
            builder.Populate(services);

            //使用已進行的元件登記建立新容器
            var ApplicationContainer = builder.Build();

            #endregion

            return new AutofacServiceProvider(ApplicationContainer);//第三方IOC接管 core內建DI容器
        }

這個時候我們就把AdvertisementServices的new 例項化過程注入到了Autofac容器中

3、通過依賴注入的三種方式(構造方法注入、setter方法注入和介面方式注入)中的建構函式方式實現注入

在BlogController中,新增建構函式,並在Get方法中,去掉例項化過程;

      IAdvertisementServices advertisementServices;
        /// <summary>
        /// 建構函式
        /// </summary>
        /// <param name="advertisementServices"></param>
        public BlogController(IAdvertisementServices advertisementServices)
        {
            this.advertisementServices = advertisementServices;
        }

    [HttpGet(
"{id}", Name = "Get")] public async Task<List<Advertisement>> Get(int id) { //IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用兩個名稱空間Blog.Core.IServices;Blog.Core.Services; return await advertisementServices.Query(d => d.Id == id); }

4、然後執行除錯,發現在斷點剛進入的時候,介面已經被例項化了,達到了注入的目的。

5、這個時候,我們發現已經成功的注入了,Advertisement實體類到介面中,但是專案中有那麼多的類,都要一個個手動新增麼,答案當然不是滴~

四、通過反射將Blog.Core.Services和Blog.Core.Repository兩個程式集的全部方法注入

修改如下程式碼,注意這個時候需要在專案依賴中,右鍵,新增引用Blog.Core.Services到專案,或者把dll檔案拷貝到Blog.Core的bin資料夾中,否則反射會找不到。

       var builder = new ContainerBuilder();

            //註冊要通過反射建立的元件
            //builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>();

            var assemblysServices = Assembly.Load("Blog.Core.Services");
            builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已掃描程式集中的型別註冊為提供所有其實現的介面。
            var assemblysRepository = Assembly.Load("Blog.Core.Repository");
            builder.RegisterAssemblyTypes(assemblysRepository).AsImplementedInterfaces();

            //將services填充到Autofac容器生成器中
            builder.Populate(services);

            //使用已進行的元件登記建立新容器
            var ApplicationContainer = builder.Build();

其他不變,執行專案,一切正常,換其他介面也可以

 

到這裡,Autofac依賴注入已經完成,基本的操作就是這樣,別忙!現在還沒有真正的完成喲!現在只是把Service,Repository和API層之間解耦了,Service和Repository之間還沒有!

注意:文中和Git程式碼中,因為為了說明的方便,沒有把Api層的Service 層 和 Repository 層給去掉,大家手動去掉,我已經更新到git上了。

1、最終的效果是這樣的:工程只是依賴介面層

service 和 repository 之間也解耦

2、把service.dll 和 Repository.dll 兩個檔案拷貝到專案 的 bin \ debug \netcoreapp2.1 下

更新:感謝網友@ 的提醒,大家下載程式碼以後,如果自己要配置這兩個dll內容,比如修改 repository.dll 的資料庫連線字串,需要自己編譯後拷貝進去,因為現在已經解耦了,更新這兩個專案不會同步到 api 層。

更新:可以直接把相應類庫生成輸出路徑修改到 Blog.core層的bin\netcoreapp2.1資料夾下即可,就不能修改了 感謝@ 說明

更新:感謝@ 提出新方法,可以把生成路徑改成相對路徑即可

專案生成地址可以使用相對路徑,這樣每個人獲取下來就不需要改了
“...\Blog.Core\bin\Debug\”

3、然後在Autofac依賴注入的時候,出現載入程式集失敗的情況,可以修改如下:

        var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;//獲取專案路徑
            var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll");//獲取注入專案絕對路徑
            var assemblysServices = Assembly.LoadFile(servicesDllFile);//直接採用載入檔案的方法

4、這個時候肯定會遇到 Sqlsugar 載入失敗的問題,請在api層引用sugar nuget包

原因:在 Api層,引入 sqlSugarCore nuget 包,因為存在依賴

除非是把 sugar下的所有dll檔案都拷貝進去,
其實這樣也是可以的,只要把第三方的nuget包生成的dll檔案全部拷貝就行,你可以看下,sqlsugar依賴了很多dll

更新 2018-10-24 

注意:如果採用上邊的方法,把 service 和 Repository 層雖然解耦了,但是必須採用 LoadFile( dll 檔案) 的形式,這樣就導致了,在 startup.cs 啟動中,無法給其他類庫中的靜態屬性賦值的能力,比如:

            BaseDBConfig.ConnectionString = "資料庫連線字串";

這個在 startup.cs 的ConfigureServices 方法中,是無法生效的。解決辦法:

1、不解耦,還是採用普通辦法,引用兩個層,用 Assembly.Load("Blog.Core.Services") 方式;

2、按照上邊解耦,但是資料庫連字串配置,需要在 Repostory 層

// public static string ConnectionString { get; set; }

public static string ConnectionString = Appsettings.app(new string[] { "AppSettings", "RedisCaching", "ConnectionString" });//獲取連線字串

還記得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會做例項化的過程。

五、 當然,你也可以直接將一個類進行注入,而不一定要繼承介面的方式;

//1. 定義一個服務,包含一個方法
public class PeopleService
{
    public string Run(string m) { return m; }
}
//2. 寫一個擴充套件方法用來注入服務,與直接在ConfigureServices返回是一個道理
namespace Haos.Develop.CoreTest
{
    public static class Extension
    {
        public static IServiceCollection AddTestService(this IServiceCollection service)
        {
            return service.AddScoped(factory => new PeopleService());
        }
    }
}

//3. 回到Startup類中找到ConfigureServices方法新增如下程式碼
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(); 
services.AddTestService();//將上邊的方法注入 }
//4.我們可以採用建構函式方式來注入和直接獲取 public PeopleService People; public HomeController(PeopleService people)//建構函式 { People = people; } public JsonResult Test() { People.Run("111");
return Json(""); }

上邊這個方法採用的是單獨註冊一個擴充套件方法來注入服務,和我們的直接return是一樣的,感興趣的可以試一試。

 同時可以參考這個網友的:

https://www.cnblogs.com/stulzq/p/6880394.html

六、簡單瞭解通過AOP切面實現日誌記錄

什麼是AOP?引用百度百科:AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。實現AOP主要由兩種方式,

一種是編譯時靜態植入,優點是效率高,缺點是缺乏靈活性,.net下postsharp為代表者(好像是付費了。。)。

另一種方式是動態代理,優點是靈活性強,但是會影響部分效率,動態為目標型別建立代理,通過代理呼叫實現攔截。

AOP能做什麼,常見的用例是事務處理、日誌記錄等等。

常見的AOP都是配合在Ioc的基礎上進行操作,上邊咱們講了Autofac這個簡單強大的Ioc框架,下面就講講Autofac怎麼實現AOP。Autofac的AOP是通過Castle(也是一個容器)專案的核心部分實現的,名為Autofac.Extras.DynamicProxy,顧名思義,其實現方式為動態代理。當然AOP並不一定要和依賴注入在一起使用,自身也可以單獨使用。

網上有一個博友的圖片,大概講了AOP切面程式設計

 

 七、結語

  今天的文章呢,主要說了依賴注入IoC在專案中的使用,從上邊的說明中,可以看到,最大的一個優點就是實現解耦的作用,最明顯的就是,在Controller中,不用在例項服務層或者業務邏輯層了,不過還是有些缺點的,缺點之一就是會佔用一定的時間資源,效率稍稍受影響,不過和整體框架相比,這個影響應該也是值得的。

  明天,我們繼續將面向切面程式設計AOP中的,日誌記錄和麵向AOP的快取使用。

八、CODE