java併發編程 –併發問題的根源及主要解決方法

目錄

  • 併發問題的根源在哪
    • 緩存導致的可見性
    • 線程切換帶來的原子性
    • 編譯器優化帶來的有序性
  • 主要解決辦法
    • 避免共享
    • Immutability(不變性)
    • 管程及其他工具

併發問題的根源在哪

首先,我們要知道併發要解決的是什麼問題?併發要解決的是單進程情況下硬件資源無法充分利用的問題。而造成這一問題的主要原因是CPU-內存-磁盤三者之間速度差異實在太大。如果將CPU的速度比作火箭的速度,那麼內存的速度就像火車,而最慘的磁盤,基本上就相當於人雙腿走路。

這樣造成的一個問題,就是CPU快速執行完它的任務的時候,很長時間都會在等待磁盤或是內存的讀寫。

計算機的發展有一部分就是如何重複利用資源,解決硬件資源之間效率的不平衡,而後就有了多進程,多線程的發展。並且演化出了各種為多進程(線程)服務的東西:

  • CPU增加緩存機制,平衡與內存的速度差異
  • 增加了多個概念,CPU時間片,程序計數器,線程切換等,用以更好得服務併發場景
  • 編譯器的指令優化,希望在內部充分利用硬件資源

但是這樣一來,也會帶來新的併發問題,歸結起來主要有三個。

  • 由於緩存導致的可見性問題
  • 線程切換帶來的原子性問題
  • 編譯器優化帶來的有序性問題

我們分別介紹這幾個:

緩存導致的可見性

CPU為了平衡與內存之間的性能差異,引入了CPU緩存,這樣CPU執行指令修改數據的時候就可以批量直接讀寫CPU緩存的內存,一個階段后再將數據寫回到內存。

但由於現在多核CPU技術的發展,各個線程可能運行在不同CPU核上面,每個CPU核各有各自的CPU緩存。前面說到對變量的修改通常都會先寫入CPU緩存,再寫回內存。這就會出現這樣一種情況,線程1修改了變量A,但此時修改后的變量A只存儲在CPU緩存中。這時候線程B去內存中讀取變量A,依舊只讀取到舊的值,這就是可見性問題。

線程切換帶來的原子性

為了更充分得利用CPU,引入了CPU時間片時間片的概念。進程或線程通過爭用CPU時間片,讓CPU可以更加充分得利用。

比如在進行讀寫磁盤等耗時高的任務時,就可以將寶貴的CPU資源讓出來讓其他線程去獲取CPU並執行任務。

但這樣的切換也會導致問題,那就是會破壞線程某些任務的原子性。比如java中簡單的一條語句count += 1。

映射到CPU指令有三條,讀取count變量指令,變量加1指令,變量寫回指令。雖然在高級語言(java)看來它就是一條指令,但實際上確是三條CPU指令,並且這三條指令的原子性無法保證。也就是說,可能在執行到任意一條指令的時候被打斷,CPU被其他線程搶佔了。而這個期間變量值可能會被修改,這裏就會引發數據不一致的情況了。所以高併發場景下,很多時候都會通過鎖實現原子性。而這個問題也是很多併發問題的源頭。

編譯器優化帶來的有序性

因為現在程序員編寫的都是高級語言,編譯器需要將用戶的代碼轉成CPU可以執行的指令。

同時,由於計算機領域的不斷髮展,編譯器也越來越智能,它會自動對程序員編寫的代碼進行優化,而優化中就有可能出現實際執行代碼順序和編寫的代碼順序不一樣的情況。

而這種破壞程序有序性的行為,在有些時候會出現一些非常微妙且難以察覺的併發編程bug。

舉個簡單的例子,我們常見的單例模式是這樣的:

public class Singleton {
 
 private Singleton() {}

 private static Singleton sInstance;

 public static Singleton getInstance() {

    if (sInstance == null) {	//第一次驗證是否為null
      synchronized (Singleton.class) {   //加鎖
        if (sInstance == null) {	  //第二次驗證是否為null
          sInstance = new Singleton();  //創建對象
                 }
             }
         }
    return sInstance;
    }

}

即通過兩段判斷加鎖來保證單例的成功生成,但在極小的概率下,可能會出現異常情況。原因就出現在sInstance = new Singleton();這一行代碼上。這行代碼,我們理解的執行順序應該是這樣:

  1. 為Singleton象分配一個內存空間。
  2. 在分配的內存空間實例化對象。
  3. 把Instance 引用地址指向內存空間。

但在實際編譯的過程中,編譯器有可能會幫我們進行優化,優化完它的順序可能變成如下:

  1. 為Singleton對象分配一個內存空間。
  2. 把instance 引用地址指向內存空間。
  3. 在分配的內存空間實例化對象。

按照優化完的順序,當併發訪問的時候,可能會出現這樣的情況

  1. A線程進入方法進行第1次instance == null判斷。
  2. 此時A線程發現instance 為null 所以對Singleton.class加鎖。
  3. 然後A線程進入方法進行第2次instance == null判斷。
  4. 然後A線程發現instance 為null,開始進行對象實例化。
  5. 為對象分配一個內存空間。
    6.把Instance 引用地址指向內存空間(而就在這個指令完成后,線程B進入了方法)。
  6. B線程首先進入方法進行第1次instance == null判斷。
  7. B線程此時發現instance 不為null ,所以它會直接返回instance (而此時返回的instance 是A線程還沒有初始化完成的對象)

最終線程B拿到的instance 是一個沒有實例化對象的空內存地址,所以導致instance使用的過程中造成程序錯誤。解決辦法很簡單,可以給sInstance對象加上一個關鍵字,volatile,這樣編譯器就不會亂優化,有關volatile的具體內容後續再細說。

主要解決辦法

通過上面的介紹,其實可以歸納無論是CPU緩存,線程切換還是編譯器優化亂序,出現問題的核心都是因為多個線程要併發讀寫某個變量或併發執行某段代碼。那麼我們可以控制,一次只讓一個線程執行變量讀寫就可以了,這就是互斥。

而在某些時候,互斥還不夠,還需要一定的條件。比如一個生產者一個消費者併發,生產者向隊列存東西,消費者向隊列拿東西。那麼生產者寫的時候要保證存的時候隊列不是滿的,消費者要保證拿的時候隊列非空。這種線程與線程間需要通信協作的情況,稱為同步,同步可以說是更複雜的互斥。

既然知道了併發編程的根源以及同步和互斥,那我們來看看有哪些解決的思路。其實一共也就三種:

  • 避免共享
  • Immutability(不變性)
  • 管程及其他工具

下面我們分別說說這三種方案的優缺點

避免共享

我們先來說說避免共享,其實避免共享說是線程本地存儲技術,在java中指的一般就是Threadlocal。ThreadLocal會為每個線程提供一個本地副本,每個線程都只會修改自己的ThreadLocal變量。這樣一來就不會出現共享變量,也就不會出現衝突了。

其實現原理是在ThreadLocal內部維護一個ThreadLocalMap,每次有線程要獲取對應變量的時候,先獲取當前線程,然後根據不同線程取不同的值,典型的以空間換時間。

所以ThreadLocal還是比較適用於需要共享資源,且資源佔用空間不大的情況。比如一些連接的session啊等等。但是這種模式應用場景也較為有限,比如需要同步情況就難以勝任。

Immutability(不變性)

Immutability在函數式中用得比較多,函數式編程的一個主要目的是要寫出無副作用的代碼,有關什麼是無副作用可以參考我以前的文章Scala函數式編程指南(一) 函數式思想介紹。而無副作用的一個主要特點就是變量都是Immutability即不可變的,即創建對象后不會再修改對象,比如scala默認的變量和數據結構都是不可變的。而在java中,不變性變量即通過final修飾的變量,如String,Long,Double等類型都是Immutability的,它們的內部實現都是基於final關鍵字的。

那這又和併發編程有什麼關係呢?其實啊,併發問題很大部分原因就是因為線程切換破壞了原子性,這又導致線程隨意對變量的讀寫破壞了數據的一致性。而不變性就不必擔心這個問題,因為變量都是不變,不可寫只能讀的。在這種編程模式下,你要修改一個變量,那麼只能新生成一個。這樣做的好處很明顯,但壞處也是顯而易見,那就是引入了額外的編程複雜度,喪失了代碼的可讀性和易用性。

因為如此,不變性的併發解決方案其實相對而已沒那麼廣泛,其中比較有代表性的算是Actor併發編程模型,我以前也有討論過,有興趣可以看看Actor模型淺析 一致性和隔離性,這種編程模型和常規併發解決方案有很顯著的差異。按我的了解,Acctor模式多用在分佈式系統的一些協調功能,比如維持集群中多個機器的心跳通信等等。如果在單機併發環境下,還是下面要介紹的管程類工具才是利器。

管程及其他工具

其實最早的操作系統中,解決併發問題用的是信號量,信號量通過兩個原子操作wait(S),和signal(S)(俗稱P,V操作)來實現訪問資源互斥和同步。比如下面這個小例子:

//整型信號量定義
int S;

//P操作
wait(S){
  while(S<=0);
  S--;
}

//V操作
signal(S){
  S++;
}

雖然信號量方便有效,但信號量要對每個共享資源都實現對應的P和V操作,這使得併發編程中可能要出現大量的P,V操作,並且這部分內容難以抽象出來。

為了更好地實現同步互斥,於是就產生了管程(即Monitor,也有翻譯為監視器),值得一提的是,管程也有幾種模型,分別是:Hasen模型,Hoare模型和MESA模型。其中MESA模型應用最廣泛,java也是參考自MESA模型。這裏簡單介紹下管程的理論知識,這部分內容參考自進程同步機制—–為進程併發執行保駕護航,希望了解更多管程理論知識的童鞋可以看看。

我們來通過一個經典的生產-消費隊列來解釋,如下圖

我們先解釋下圖中右半部分的內容,右上角有一個等待調用的線程隊列,管程中每次只能有一個線程在執行任務,所以多個任務需要等待。然後是各個名詞的意思,生產-消費需要往隊列寫入和取出東西,這裏的隊列就是共享變量,對共享資源進行操作稱之為過程(入隊和出隊兩個過程)。而向隊列寫入和取出是有條件的,寫入的時候隊列必須是非滿的,取出的時候隊列必須是非空的,這兩個條件被稱為條件變量。

然後再來看看左半部分的內容,假設線程T1讀取共享變量(即隊列),此時發現隊列為空(條件變量之一),那麼T1此時需要等待,去哪裡等呢?去條件變量隊列不能為空對應的隊列中去等待。此時另一個線程T2向共享變量隊列寫數據,通過了條件變量隊列不能滿,那麼寫完后就會通知線程T1。但因為管程的限制,管程中只能有一個線程在執行,所以T1線程不能立即執行,它會回到右上角的線程等待隊列等待(不同的管程模型在這裡是有分歧的,比如Hasen模型是立即中斷T2線程讓隊列中下一個線程執行)。

解釋完這個圖,管程的概念也就呼之欲出了,

hansen對管程的定義如下:一個管程定義了一個數據結構和能力為併發進程所執行(在該數據結構上)的一組操作,這組操作能同步進程和改變管程中的數據。

本質上,管程是對共享資源以及對共享資源的操作抽象成變量和方法,要操作共享變量僅能通過管程提供的方法(比如上面的入隊和出隊)間接訪問。所以你會發現管程其實和面向對象的理念是十分相近的,在java中,主要提供了低層次了synchronized關鍵字和wait(),notify()等方法。同時還提供了高層次的ReenTrantLock和Condition來實現管程模型。

以上~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

.Net Core微服務入門全紀錄(四)——Ocelot-API網關(上)

前言

上一篇【.Net Core微服務入門全紀錄(三)——Consul-服務註冊與發現(下)】已經使用Consul完成了服務的註冊與發現,實際中光有服務註冊與發現往往是不夠的,我們需要一個統一的入口來連接客戶端與服務。

Ocelot

官網:https://ocelot.readthedocs.io/
Ocelot正是為.Net微服務體系提供一個統一的入口點,稱為:Gateway(網關)。

  • 上手Ocelot:

首先創建一個空的asp.net core web項目。

注意ocelot.json是我們添加的Ocelot的配置文件,記得設置生成時複製到輸出目錄。ocelot.json的文件名不是固定的,可以自己定義。

NuGet安裝一下Ocelot:

只需簡單的修改幾處默認代碼:
Program.cs:

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config.AddJsonFile("ocelot.json");
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

Startup.cs:

    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            //添加ocelot服務
            services.AddOcelot();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //設置Ocelot中間件
            app.UseOcelot().Wait();
        }
    }

ocelot.json:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 9050
        },
        {
          "Host": "localhost",
          "Port": 9051
        },
        {
          "Host": "localhost",
          "Port": 9052
        }
      ],
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //負載均衡,輪詢機制 LeastConnection/RoundRobin/NoLoadBalancer/CookieStickySessions
      }
    },
    {
      "DownstreamPathTemplate": "/orders",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 9060
        },
        {
          "Host": "localhost",
          "Port": 9061
        },
        {
          "Host": "localhost",
          "Port": 9062
        }
      ],
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [
        "Get"
      ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin" //負載均衡,輪詢機制 LeastConnection/RoundRobin/NoLoadBalancer/CookieStickySessions
      }
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:9070"
  }
}

我們先暫時忽略Consul,將服務實例的地址都寫在配置文件中。要知道Consul、Ocelot等組件都是可以獨立存在的。
配置文件中的Routes節點用來配置路由,Downstream代表下游,也就是服務實例,Upstream代表上游,也就是客戶端。我們的路徑比較簡單,只有/products、/orders,路徑中如果有不固定參數則使用{}匹配。我們這個配置的意思呢就是客戶端訪問網關的/orders、/products,網關會轉發給服務實例的/orders、/products,注意這個上游的路徑不一定要和下游一致,比如上游路徑可以配置成/api/orders,/xxx都可以。
LoadBalancerOptions節點用來配置負載均衡,Ocelot內置了 LeastConnection、RoundRobin、NoLoadBalancer、CookieStickySessions 4種負載均衡策略。
BaseUrl節點就是配置我們ocelot網關將要運行的地址。

  • 運行gateway:

目前不考慮網關集群,就不放在docker里了。直接控制台執行:`dotnet Ocelot.APIGateway.dll –urls=”http://*:9070″

用瀏覽器測試一下:

測試正常,我們通過網關可以正常的訪問到服務實例。

  • 接下來繼續改造客戶端代碼:

因為改動太多就直接新建一個GatewayServiceHelper來做。
GatewayServiceHelper:

    /// <summary>
    /// 通過gateway調用服務
    /// </summary>
    public class GatewayServiceHelper : IServiceHelper
    {
        public async Task<string> GetOrder()
        {
            var Client = new RestClient("http://localhost:9070");
            var request = new RestRequest("/orders", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public async Task<string> GetProduct()
        {
            var Client = new RestClient("http://localhost:9070");
            var request = new RestRequest("/products", Method.GET);

            var response = await Client.ExecuteAsync(request);
            return response.Content;
        }

        public void GetServices()
        {
            throw new NotImplementedException();
        }
    }

然後在Startup中修改一下注入的類型,別的就不用改了,這就是依賴注入的好處之一。。。
Startup.ConfigureServices():

//注入IServiceHelper
//services.AddSingleton<IServiceHelper, ServiceHelper>();
            
//注入IServiceHelper
services.AddSingleton<IServiceHelper, GatewayServiceHelper>();

Startup.Configure():

//程序啟動時 獲取服務列表
//serviceHelper.GetServices();

運行客戶端測試:

好了,現在客戶端對服務的調用都通過網關進行中轉,客戶端再也不用去關心那一堆服務實例的地址,只需要知道網關地址就可以了。另外,服務端也避免了服務地址直接暴露給客戶端。這樣做對客戶端,服務都非常友好。

至於我們的api網關呢,又要說到服務發現的問題了。目前我們的服務地址是寫在ocelot.json配置文件里的,當然這種做法在服務實例不經常變化的情況下是沒有問題的,一旦服務變化,需要人為的修改配置文件,這又顯得不太合理了。

當然,強大的Ocelot為我們提供了服務發現的方案。

代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

未完待續…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

深入理解 EF Core:EF Core 讀取數據時發生了什麼?

閱讀本文大概需要 11 分鐘。

原文:https://bit.ly/2UMiDLb
作者:Jon P Smith
翻譯:王亮
聲明:我翻譯技術文章不是逐句翻譯的,而是根據我自己的理解來表述的。其中可能會去除一些本人實在不知道如何組織但又不影響理解的句子。

本文將為你詳細描繪 EF Core 從數據庫中讀取數據的“幕後”視圖。我將揭開兩種數據庫讀取方式的面紗:一個是普通的查詢,另一個是使用 AsNoTracking 方法的非跟蹤查詢。我還將通過一個實驗來演示我是如何解決我的一個客戶遇到的性能問題。

我假設你對 EF Core 已經有了一定的認識,但在深入學習之前,我們先來了解一下如何使用 EF Core,以確保我們已經掌握了一些基本知識。這是一個“深入研究”的課題,所以我準備大量的技術細節,希望我的描述方式你能理解。

本文是“深入理解 EF Core”系列中的第一篇。以下是本系列文章列表:

  • 當 EF Core 從數據庫讀取數據時發生了什麼?(本文)
  • 當 EF Core 寫入數據到數據庫時發生了什麼?(敬請期待)

概要

  • EF Core 有兩種方法從數據庫中讀取數據(也稱為查詢):普通 LINQ 查詢和包含 AsNoTracking 方法的非跟蹤 LINQ 查詢。
  • 這兩種方法查詢的返回類(被稱為實體類),它連接的其它的實體類(即所謂的導航屬性)也被同時加載,但這兩種法如何連接及連接的內容是不一樣的。
  • 普通查詢接受的是 DbContext 執行讀取時所有數據的副本——此時的實體類稱為被跟蹤。這允許加載的實體類參与數據庫的更新操作。
  • 普通查詢還會有一些其它的複雜底層實現,稱為關係修補(fixup),用於描述讀入的實體類和其他被跟蹤實體之間的連接關係。
  • AsNoTracked 非跟蹤查詢沒有副本,所以它沒有被跟蹤——這意味着它比普通查詢更快。這也意味着它不會用於數據庫的寫操作。
  • 最後,我將展示 EF Core 普通查詢中一個鮮為人知的特性,以此作為示例,說明通過導航屬性連接實體類的關係是多麼智能。

EF Core 如何讀取數據庫數據

提示:如果你已經對 EF Core 有一定的認識,那麼你可以跳過這一節,這部分只是一個如何讀取數據庫的例子。

為了能讓你更好地理解,我先描述一個數據庫結構,然後再給出一個簡單的數據庫讀取示例。下面是一些基本表的結構和它們之間的關係。

這些表被映射到具有類似名稱的類,例如 Book、BookAuthor、Author,這些類的屬性名稱與表的字段名稱相同。由於篇幅有限,我不打算展開來講這些類,但您可以在我的 GitHub 倉庫[1]中查看這些類。

EF Core 讀取數據庫需要下面五部分:

  1. 數據庫服務器,如 SQL server, Sqlite, PostgreSQL 等。
  2. 具有數據的數據庫。
  3. 映射到數據表的類(稱為實體類)。
  4. 一個繼承 DbContext 的類,該類包含 EF Core 的配置。
  5. 最後,從數據庫讀取數據的命令。

下面的單元測試代碼來自我的 GitHub 創庫[2],展示了一個簡單的示例,它從現有數據庫中讀取 4 個 Book 實體及其關聯的 BookAuthor 和 Authors 實體。

倉庫地址:https://bit.ly/2Yza7QQ

[Fact]
public void TestBookCountAuthorsOk()
{
    //SETUP
    var options = SqliteInMemory.CreateOptions<EfCoreContext>();
    //code to set up the database with four books, two with the same Author
    using (var context = new EfCoreContext(options))
    {
        //ATTEMPT
        var books = context.Books
            .Include(r => r.AuthorsLink)
            .ThenInclude(r => r.Author)
            .ToList();

        //VERIFY
        books.Count.ShouldEqual(4);
        books.SelectMany(x => x.AuthorsLink.Select(y => y.Author))
            .Distinct().Count().ShouldEqual(3);
    }
}

現在,如果我們將單元測試代碼對應到上面的 5 部分,結果是這樣的:

  1. 數據庫服務器——第 5 行:我選擇了一個 Sqlite 數據庫服務器,在本例中是 SqliteInMemory.CreateOptions 方法,它使用我的一個 NuGet 包 EfCore.TestSupport 創建了一個內存數據庫(內存中的數據庫對於單元測試非常有用,因為你可以為這個測試建立一個新的空數據庫)。
  2. 具有數據的數據庫——第 6 行:我將在下一篇文章介紹數據是如何寫入數據庫的,現在假設有一個數據庫包含 4 本書信息,其中兩本書的作者是同一個人。
  3. 實體類——代碼里這裏沒有展示,但是你可以在這裏查看這些類[1]。其中有一個 Books 實體類,通過一個名為 BookAuhor 的實體類多對多關聯 Authors 實體類。
  4. 一個繼承 DbContext 的類——第 7 行:EfCoreContext 類繼承了 DbContext 類並配置了從類到數據庫的映射關係(你可以在我的 GitHub 倉庫[3] 中查看該類)。
  5. 從數據庫讀取數據的命令——第 10 到 13 行,這是一個查詢:
    • 第 10 行 — context 為 EfCoreContext 的實例,通過它訪問你的數據庫,.Books 表示您希望訪問 Books 表。
    • 第 11 行 — Include 被稱為貪婪加載,它告訴 EF Core 當它加載 Books 時,也應該加載關聯到的所有 BookAuthor 實體類。
    • 第 12 行 — ThenInclude 是繼續貪婪加載,它告訴 EF Core 當它加載一個 BookAuthor 時,它也應該加載關聯到該 BookAuthor 的 Author 實體類。

所有這一切查詢出來是一個結果集,其中有普通屬性,像 Books 的 Title 屬性;有關聯實體類的導航屬性,像 Books 的 AuthorsLink 屬性。

這個示例稱為查詢或讀取,也是四種數據庫訪問類型之一,即 CRUD(新增、讀取、更新和刪除)。我將在下一篇文章中介紹新增和更新。

EF Core 如何表示讀取的數據

當你查詢數據庫時,EF Core 會將數據庫返回的數據轉換為實體類並填充導航屬性的值。在本節中,我們將研究兩種類型的查詢步驟——普通查詢(即沒有 AsNoTracking 方法,也稱為讀寫查詢)和添加了 AsNoTracking 方法的非跟蹤查詢(稱為只讀查詢)。

我們先來看一下最初 LINQ 語句是如何轉換成數據庫相應的查詢命令然後返回數據的。對於我們將要看到的兩種類型的查詢來說,這是很常見的操作。關於查詢的第一部分,請參見下圖。

有一些非常複雜的代碼將你的 LINQ 轉換為數據庫查詢命令,但這些內部細節我們不必關心。如果你的 LINQ 不能被翻譯,你會從 EF Core 得到一個異常消息,其中包含類似“不能被翻譯”的描述詞語。此外,當數據返回時,像 Value Converters[4] 這樣的特性可能會調整數據。

本節展示了查詢的第一部分,其中 LINQ 被轉換為數據庫命令並返回所有正確的值。現在我們來看查詢的第二部分,在這裏 EF Core 獲取返回值並將它們轉換為實體類的實例,並填充導航屬性。我們將分別看看兩種類型的查詢。

1. 普通查詢(讀寫查詢)

普通查詢讀取數據的方式可以修改數據並更新到數據庫,這就是我將其稱為讀寫查詢的原因。它不會自動更新數據(請參閱下一篇文章,了解如何寫入數據庫)。如果你要更新數據,你的查詢必須是讀寫查詢。

我在介紹中給出的示例執行的是一個普通讀寫查詢,讀取帶有 AuthorsLink 實例的示例。下面是該示例的查詢部分的代碼:

var books = context.Books
    .Include(r => r.AuthorsLink)
    .ThenInclude(r => r.Author)
    .ToList();

然後 EF Core 通過三個步驟將這些值轉換並填充含有導航屬性的實體類。下圖显示了這三個步驟以及生成的實體類及其導航屬性的實體類。

讓我們來分析一下這三個步驟:

  1. 創建類並填充數據。它接受數據庫返回的值,並填充非導航(稱為標量)屬性、字段等。在 Book 實體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍色矩形。
  2. 修補關聯關係。首先是填入主鍵和外鍵的信息,它們定義如何相互關聯數據。然後,EF Core 使用這些鍵設置實體類之間的導航屬性(如圖中藍色粗線所示)。這個關係的修補所需的信息不僅是查詢讀入的實體類,它還會查看 DbContext 中跟蹤的每個實體,並填充導航屬性。這是一個強大的功能,但你的被跟蹤實體越多,所需消耗時間也越多——這就是為什麼需要 AsNoTracking 來實現更快的查詢。
  3. 創建跟蹤快照。跟蹤快照是返回給用戶的實體類的一個副本,加上它所隱藏的與每個實體類的關聯關係——若一個實體處於被跟蹤狀態,這意味着它將會發生修改並會寫入到數據庫中。

2. 非跟蹤查詢(只讀查詢)

非跟蹤查詢,即使用 AsNoTracking 方法的查詢,是一個只讀查詢。這意味着,當 SaveChanges 方法被調用時,你讀取的任何內容都不會被寫入數據庫。非跟蹤查詢的查詢效率更高,在下一節中,我將介紹非跟蹤查詢以及與普通查詢的其他區別。

在前文的示例之後,我修改了查詢代碼,添加了下面的 AsNoTracking 方法(請看第 2 行):

var books = context.Books
    .AsNoTracking()
    .Include(r => r.AuthorsLink)
    .ThenInclude(r => r.Author)
    .ToList();

這裏的 LINQ 查詢只有上面的普通查詢的前兩個步驟(沒有第三個步驟)。下圖显示了 AsNoTracking 查詢的步驟。

步驟如下:

  1. 創建類並填充數據。它接受數據庫返回的值,並填充非導航(稱為標量)屬性、字段等。在 Book 實體類中,是 BookId(主鍵)、Title 等屬性——參見上圖左下角淺藍色矩形。
  2. 修補關聯關係。首先是填入主鍵和外鍵的信息,它們定義如何相互關聯數據。然後,EF Core 使用這些鍵設置實體類之間的導航屬性(如圖中藍色粗線所示)。這個關係的修補所需的信息不僅是查詢讀入的實體類,它還會查看 DbContext 中跟蹤的每個實體,並填充導航屬性。這是一個強大的功能,但你的被跟蹤實體越多,所需消耗時間也越多——這就是為什麼需要 AsNoTracking 來實現更快的查詢。

普通查詢和非跟蹤查詢的區別

現在讓我們比較這兩種查詢比較明顯的區別。

  1. 非跟蹤查詢查詢的性能更好。使用非跟蹤查詢查詢的主要原因是性能。非跟蹤查詢查詢表現為:

    • 稍微快一點,使用的內存稍微少一點,因為它不需要創建跟蹤快照。
    • 避免沒有必要的跟蹤快照可以提高 SaveChanges 的性能,因為它不必檢查跟蹤快照以查找更改。
    • 稍微快一點,因為修補關聯關係時沒有所謂的身份解析。這就是為什麼你會得到兩個具有相同數據的 Author 實例。
  2. 非跟蹤查詢修補關聯關係時只鏈接查詢中的實體。在普通查詢中,我已經說過修補關聯關係時連接的是查詢中的實體和當前跟蹤的實體,但是非跟蹤查詢只修補查詢中的實體關係。

  3. 非跟蹤查詢並不總是代表數據庫關係。這兩種類型查詢之間的關係修補的另一個區別是,非跟蹤查詢關係修補更快,它不需要標識的解析。這可以為數據庫中的同一行生成多個實例——見上圖右下角藍色的 Author 實體和註釋。如果只是向用戶显示數據,那麼這種差異並不重要,但是如果具有業務邏輯,那麼多個實例不能正確反映數據的結構,就可能會有問題。

對層級數據有用的關係修補特性

關聯關係修補的步驟是非常智能的,特別是在普通查詢中。下面我想向你展示我是如何利用關係修補的特性來解決一個客戶項目中的性能問題的。

我曾在一家公司工作,那裡的許多數據處理都是層次化結構的,即數據具有一系列深度不確定的關聯關係。問題是我必須先解析整個層次結構,然後才能呈現這些數據。我最初是通過貪婪的方式加載前兩個層級,然後顯式地加載更深的層級來實現這一點的。它可以工作,但是性能非常慢,並且數據庫因大量單數據庫訪問而超載。

這不得不讓我思考解決辦法,如果普通查詢的關係修補那麼智能的話,它能幫助我提高查詢的性能嗎?它可以!讓我給你舉一個公司員工的例子。下圖显示了我們想要加載的公司的層次結構。

你可以接龍式地使用 .Include(x => x.WorksForMe).ThenInclude(x => x.WorksForMe)… 等等來加載所需的層級信息,但結果是一個 .Include(x => x.WorksForMe) 就夠了。因為 EF Core 的關係修補為你做了剩下的事情,這一點很驚奇,但也很有用。

例如,如果我想查詢角色為 Development 的所有員工(每個員工都有一個名為 WhatTheyDo 的屬性和名為 Role 的屬性,該 Role 包含他們工作的部門),我可以這樣編寫代碼:

var devDept = context.Employees
    .Include(x => x.WorksFromMe)
    .Where(x => x.WhatTheyDo.HasFlag(Roles.Development))
    .ToList();

這將創建一個查詢,用於加載角色為 Development 的所有員工,並且在員工實體類上修補與 WorksFoMe 導航屬性(集合)和 Manager 導航屬性(單個)的關係。通過只執行一個查詢,既提高了查詢花費的時間,又減少了數據庫服務器上的負載。

總結

你已經看到了兩種類型的查詢,我稱之為 a)普通的讀寫查詢,和 b) 非跟蹤的只讀查詢。對於每一種查詢類型,我都向你展示了 EF Core “幕後”是如何讀取數據並展示的。他們工作方式的不同也表現出他們的優勢和劣勢。

非跟蹤查詢是只讀查詢的解決方案,因為它比普通讀寫查詢更快。但是您應該記住關係修補的機制,它可以在數據庫只有一個關係的情況下創建類的多個實例。

普通的讀寫查詢是查詢跟蹤實體的解決方案,這意味着你可以在創建、更新和刪除數據時使用它們。普通的讀寫查詢確實會佔用更多的時間和內存資源,但是有一些有用的特性,比如自動鏈接到其他被跟蹤的實體類實例。

我希望這篇文章對您有用。祝你編程快樂!

[1]. https://bit.ly/2MXK3ZY
[2]. https://bit.ly/2Yza7QQ
[3]. https://bit.ly/2Y0UORO
[4]. https://bit.ly/2YEyg8j

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

Spring Cloud Alibaba系列(四)使用gateway作為服務網關

什麼是網關

在微服務架構里,服務的粒度被進一步細分,各個業務服務可以被獨立的設計、開發、測試、部署和管理。這時,各個獨立部署單元可以用不同的開發測試團隊維護,可以使用不同的編程語言和技術平台進行設計,這就要求必須使用一種語言和平台無關的服務協議作為各個單元間的通訊方式。

換句話說就是網關為所有的請求提供了統一的入口,方便我們對服務請求和響應做統一管理。

為什麼要用網關

API 網關是一個處於應用程序或服務(提供 REST API 接口服務)之前的系統,用來管理授權、訪問控制和流量限制等,這樣 REST API 接口服務就被 API 網關保護起來,對所有的調用者透明。

什麼是gateway

Spring Cloud Gateway是Spring官方基於Spring 5.0,Spring Boot 2.0和Project Reactor等技術開發的網關,Spring Cloud Gateway旨在為微服務架構提供一種簡單而有效的統一的API路由管理方式。Spring Cloud Gateway作為Spring Cloud生態系中的網關,目標是替代ZUUL,其不僅提供統一的路由方式,並且基於Filter鏈的方式提供了網關基本的功能,例如:安全,監控/埋點,和限流等。

gateway工作原理

客戶端向Spring Cloud網關發出請求。如果網關處理程序映射確定請求與路由匹配,則將其發送到網關Web處理程序。該處理程序運行通過特定於請求的過濾器鏈發送請求。過濾器由虛線分隔的原因是,過濾器可以在發送代理請求之前或之後執行邏輯。執行所有“前置”過濾器邏輯,然後發出代理請求。發出代理請求后,將執行“后”過濾器邏輯。

路由規則

路由和過濾器是gateway中非常重要的兩個概念,gateway本身提供了非常豐富的路由規則和多種過濾器來適配我們的需求。gateway提供了11種路由規則,分別是:

  • 後置路由謂詞工廠

    該謂詞匹配在當前日期時間之後發生的請求。參數名為 After

  • 前置路由謂詞工廠

    該謂詞匹配當前日期時間之前發生的請求。參數名為 Before

  • 時間段路由謂詞工廠

    該謂詞匹配在datetime1之後和datetime2之前發生的請求。參數名為 Between

  • cookie路由謂詞工廠

    該謂詞匹配具有給定名稱的cookie,並且值匹配正則表達式。參數名為 Cookie

  • 標頭路由謂詞工廠

    該謂詞與具有給定名稱的標頭匹配,並且值與正則表達式匹配。參數名為 Header

  • 主機路由謂詞工廠

    該謂詞是指由路由進行匹配,匹配多個路由時用,隔開。參數名為 Host

  • 方法路由謂詞工廠

    該參數是一個或多個要匹配的HTTP方法。參數名為 Method

  • 路徑路由謂詞工廠

    該謂詞是指在請求路徑上加一個前綴,以此來匹配。參數名為 Path

  • 查詢路由謂詞工廠

  • RemoteAddr路由謂詞工廠

  • 重量路線謂詞工廠

其中,我們比較常用的就是路徑路由謂詞工廠,配合StripPrefix GatewayFilter工廠,實現我們的路由匹配轉發。

路徑路由謂詞工廠配置如下:

spring:
  cloud:
    gateway:
	  discovery:
          locator:
          	enabled: true # 開啟從註冊中心動態創建路由的功能,利用微服務名稱進行路由
      routes:
          # 路由id,建議配合服務名
        - id: demo_route 
          #匹配路由名
          uri: lb://demo-provider 
          predicates:
	  # 斷言,路徑相匹配的進行路由
          - Path=/demo/** 

配置的含義就是,如果請求路徑中是/demo/**,則轉發到demo-provider服務。

網關過濾器

在spring cloud gateway 2.2.2.RELEASE版本中,已經默認實現了30種過濾器。

序號 過濾器工廠 作用 參數
1 AddRequestHeader 為原始請求添加Header Header的名稱及值
2 AddRequestParameter 為原始請求添加請求參數 參數名稱及值
3 AddResponseHeader 為原始響應添加Header Header的名稱及值
4 DedupeResponseHeader 剔除響應頭中重複的值 需要去重的Header名稱及去重策略
5 Hystrix 為路由引入Hystrix的斷路器保護 HystrixCommand的名稱
6 CircuitBreaker 為路由引入Resilience4J斷路器保護 CircuitBreaker的名稱
7 FallbackHeaders 為fallbackUri的請求頭中添加具體的異常信息 Header的名稱
MapRequestHeader 更新原始請求中的Header Header的值
9 PrefixPath 為原始請求頭添加前綴 前綴路徑
10 PreserveHostHeader 為請求添加preserverHostHeader=true的屬性,路由過濾器會檢查該屬性以決定是否要發送原始的host 無
11 RequestRateLimiter 用於對請求限流,限流算法為令牌桶 keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus
12 RedirectTo 將原始請求重定向到指定的url http狀態碼及重定向的url
13 RemoveHopByHopHeadersFilter 為原始請求刪除IETF組織規定的一系列Header 默認就會啟用,可以通過配置指定僅刪除哪些Header
14 RemoveRequestHeader 為原始請求刪除某個Header Header名稱
15 RemoveResponseHeader 為原始響應刪除某個Header Header名稱
16 RewritePath 重寫原始的請求路徑 原始路徑正則表達式以及重寫後路徑的正則表達式
RewriteLocationResponseHeader 重寫響應頭的Location 的值
18 RewriteResponseHeader 重寫原始響應中的某個Header Header名稱,值的正則表達式,重寫后的值
19 SaveSession 在轉發請求之前,強制執行WebSession::save操作 無
20 SecureHeaders 為原始響應添加一系列起安全作用的響應頭 無,支持修改這些安全響應頭的值
21 SetPath 修改原始的請求路徑 修改后的值
22 SetRequestHeader 修改原始請求中的某個Header的值 Header名稱,修改后的值
23 SetResponseHeader 修改原始響應中某個Header的值 Header名稱,修改后的值
24 SetStatus 修改原始響應的狀態碼 HTTP 狀態碼,可以是数字,也可以是字符串
25 StripPrefix 用於截斷原始請求的路徑 使用數字錶示要截斷的路徑的數量
26 Retry 針對不同的響應進行重試 retries、statuses、methods、series
27 RequestSize 設置允許接收最大請求包的大小。如果請求包大小超過設置的值,則返回 413 Payload Too Large設置允許接收最大請求包的大小。如果請求包大小超過設置的值,則返回 413 Payload Too Large 請求包大小,單位為字節,默認值為5M
28 ModifyRequestBody 在轉發請求之前修改原始請求體內容 修改后的請求體內容
29 ModifyResponseBody 修改原始響應體的內容 修改后的響應體內容
30 Default 為所有路由添加過濾器 過濾器工廠名稱及值

這裏比較常用的如第25種,配置如下:

spring:
  cloud:
    gateway:
	  discovery:
          locator:
          	enabled: true # 開啟從註冊中心動態創建路由的功能,利用微服務名稱進行路由
      routes:
		  # 路由id,建議配合服務名
        - id: demo_route 
          #匹配路由名
          uri: lb://demo-provider 
          predicates:
		  # 斷言,路徑相匹配的進行路由
          - Path=/demo/** 
          filters:
          - StripPrefix=1

一般情況下我們配合path路由使用,這裏的意思是假如,我們的demo-provider服務種有一個/test的接口,實際上我們的請求路徑經過網關時應該時/demo/test,這樣就能把這個路由分發到demo-provider服務中,但是分發過去的路由是/demo/test,和我們實際的/test接口不一樣。這時候我們用StripPrefix=1,來截取掉一級路由,這樣轉發過去的路由就是/test了。

自定義網關過濾器

除了上面提供的30種過濾器外,我們還可以實現自定義的過濾器。

1. 實現GatewayFilter接口和Ordered接口

gatewayFilter接口是為了實現請求過濾,ordered接口是為了給過濾器設定優先級,值越大級別越低。

想要實現一個自定義的過濾器,無非就是兩個步驟:1.實現過濾器,2.將過濾器添加到具體路由上。

public class TokenGatewayFilter implements GatewayFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        System.out.println("這裏處理自身邏輯");

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

@Configuration
class RouteConfiguration{

    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder){

        return builder.routes().route( r->
                r.path("/demo/**")
                .uri("lb://demo-provider ")
                .filter(new TokenGatewayFilter())
                .id("demo_route "))
                .build();
    }
}

2.繼承AbstractGatewayFilterFactory類

@Component
public class TokenCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<TokenCheckGatewayFilterFactory.Config> {
    public TokenCheckGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("enabled");
    }
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
        	system.out.println("這裏處理自身邏輯")
            return chain.filter(exchange);
        };

    }

    public static class Config {
        // 控制是否開啟認證
        private boolean enabled = true;

        public Config() {}

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }
}

這裏我們可以直接在application.yml中為需要過濾的路由添加這個過濾器。

spring:
  cloud:
    gateway:
      routes:
        - id: demo_route # 路由id,建議配合服務名
          uri: lb://demo-provider #匹配路由名
          predicates:
          - Path=/demo/** # 斷言,路徑相匹配的進行路由
          filters:
          - TokenCheck=true

需要注意的是,這個地方自定義的過濾器名稱必須是XXGatewayFilterFactory,並且配置文件中配置過濾器時名字必須時這個XX。

當然,我們也可以為每個路由都添加這個過濾器,可以直接這樣寫配置,而不用在每個路由上都去寫。

spring:
  cloud:
    gateway:
      default-filters:
        - TokenCheck=true

3.實現GlobalFilter和ordered

這個GlobalFilter從名字中就可以看出,是一個全局過濾器,也就是說實現這個接口后,所有的請求都會被過濾,我們就不需要在去找往某個路由中加過濾器了。

@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("這裏處理自身邏輯");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

以上就是實現自定義網關過濾器的三種方式了。實際開發中根據需求來實現合適的過濾器就可以了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

HashSet擴容機制在時間和空間上的浪費,遠大於你的想象

一:背景

1. 講故事

自從這個純內存項目進了大客戶之後,搞得我現在對內存和CPU特別敏感,跑一點數據內存幾個G的上下,特別沒有安全感,總想用windbg抓幾個dump看看到底是哪一塊導致的,是我的代碼還是同事的代碼? 很多看過我博客的老朋友總是留言讓我出一套windbg的系列或者視頻,我也不會呀,沒辦法,人在江湖飄,遲早得挨上幾刀,逼着也得會幾個花架子,廢話不多說,這一篇就來看看 HashSet 是如何擴容的。

二:HashSet的擴容機制

1. 如何查看

了解如何擴容,最好的辦法就是翻看HashSet底層源碼,最粗暴的入口點就是 HashSet.Add 方法。

從圖中可以看到最後的初始化是用 Initialize 的,而且裏面有這麼一句神奇的代碼: int prime = HashHelpers.GetPrime(capacity);,從字面意思看是獲取一個質數,哈哈,有點意思,什麼叫質數? 簡單說就是只能被 1 和 自身 整除的數就叫做質數,那好奇心就來了,一起看看質數是怎麼算的吧! 再次截圖。

從圖中看,HashSet底層為了加速默認定義好了 72 個質數,最大的一個質數是 719w,換句話就是說當元素個數大於 719w 的時候,就只能使用 IsPrime 方法動態計算質數,如下代碼:


public static bool IsPrime(int candidate)
{
	if ((candidate & 1) != 0)
	{
		int num = (int)Math.Sqrt(candidate);
		for (int i = 3; i <= num; i += 2)
		{
			if (candidate % i == 0)
			{
				return false;
			}
		}
		return true;
	}
	return candidate == 2;
}

看完了整個流程,我想你應該明白了,當你第一次Add的時候,默認的空間佔用是 72 個預定義中最小的一個質數 3,看過我之前文章的朋友知道List的默認大小是4,後面就是簡單粗暴的 * 2 處理,如下代碼。


private void EnsureCapacity(int min)
{
	if (_items.Length < min)
	{
		int num = (_items.Length == 0) ? 4 : (_items.Length * 2);
	}
}

2. HashSet 二次擴容探究

當HashSet的個數達到3之後,很顯然要進行二次擴容,這一點不像List用一個 EnsureCapacity 方法搞定就可以了,然後細看一下怎麼擴容。


public static int ExpandPrime(int oldSize)
{
	int num = 2 * oldSize;
	if ((uint)num > 2146435069u && 2146435069 > oldSize)
	{
		return 2146435069;
	}
	return GetPrime(num);
}

從圖中可以看到,最後的擴容是在 ExpandPrime 方法中完成的,流程就是先 * 2, 再取最接近上限的一個質數,也就是 7 ,然後將 7 作為 HashSet 新的Size,如果你非要看演示,我就寫一小段代碼證明一下吧,如下圖:

2. 您嗅出風險了嗎?

<1> 時間上的風險

為了方便演示,我把 72 個預定義的最後幾個質數显示出來。


public static readonly int[] primes = new int[72]
{
	2009191,
	2411033,
	2893249,
	3471899,
	4166287,
	4999559,
	5999471,
	7199369
};

也就是說,當HashSet的元素個數為 2893249 的時候觸發擴容變成了 2893249 * 2 => 5786498 最接近的一個質數為:5999471,也就是 289w 暴增到了 599w,一下子就是 599w -289w = 310w 的空間虛占,這可是增加了兩倍多哦,嚇人不? 下面寫個代碼驗證下。


        static void Main(string[] args)
        {
            var hashSet = new HashSet<int>(Enumerable.Range(0, 2893249));

            hashSet.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l

000000B8F4DBE500 00007ffaf00132ae ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 16]
    LOCALS:
        0x000000B8F4DBE538 = 0x0000020e0b8fcc08
0:000> !DumpObj /d 0000020e0b8fcc08
Name:        System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
Size:        64(0x40) bytes
File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Collections.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffaf0096d10  4000017        8       System.Int32[]  0 instance 0000020e2025e9f8 _buckets
00007ffaf00f7ad0  4000018       10 ...ivate.CoreLib]][]  0 instance 0000020e2bea1020 _slots
00007ffaeffdf828  4000019       28         System.Int32  1 instance          2893250 _count
0:000> !DumpObj /d 0000020e2025e9f8
Name:        System.Int32[]
Size:        23997908(0x16e2dd4) bytes
Array:       Rank 1, Number of elements 5999471, Type Int32 (Print Array)
Fields:
None


而且最重要的是,這裡是一次性擴容的,而非像redis中實現的那樣漸進式擴容,時間開銷也是大家值得注意的。

<2> 空間上的風險

這個有什麼風險呢?可以看一下:289w 和 599w 兩個HashSet的佔用空間大小,這也是我最敏感的。


        static void Main(string[] args)
        {
            var hashSet1 = new HashSet<int>(Enumerable.Range(0, 2893249));

            var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
            hashSet2.Add(int.MaxValue);

            Console.Read();
        }

0:000> !clrstack -l
OS Thread Id: 0x4a44 (0)
000000B1B4FEE460 00007ffaf00032ea ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 18]
    LOCALS:
        0x000000B1B4FEE4B8 = 0x000001d13363cc08
        0x000000B1B4FEE4B0 = 0x000001d13363d648

0:000> !objsize 0x000001d13363cc08
sizeof(000001D13363CC08) = 46292104 (0x2c25c88) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])
0:000> !objsize 0x000001d13363d648
sizeof(000001D13363D648) = 95991656 (0x5b8b768) bytes (System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]])

可以看到, hashSet1的佔用: 46292104 / 1024 / 1024 = 44.1M, hashSet2 的佔用 : 95991656 / 1024 / 1024 = 91.5M,一下子就浪費了: 91.5 - 44.1 = 47.4M。

如果你真以為僅僅浪費了 47.4M 的話,那你就大錯特錯了,不要忘了底層在擴容的時候,使用新的 size 覆蓋了老的 size,而這個 老的 size 集合在GC還沒有回收的時候會一直佔用堆上空間的,這個能聽得懂嗎? 如下圖:

要驗證的話可以用 windbg 去託管堆上抓一下 Slot[] m_slots 和 int[] m_buckets 兩個數組,我把代碼修改如下:


    static void Main(string[] args)
    {
        var hashSet2 = new HashSet<int>(Enumerable.Range(0, 2893249));
        hashSet2.Add(int.MaxValue);
        Console.Read();
    }


0:011> !dumpheap -stat
00007ffaf84f7ad0        3    123455868 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

這裏就拿 Slot[] 說事,從上面代碼可以看到,託管堆上有三個 Slot[] 數組,這就有意思了,怎麼有三個哈,是不是有點懵逼,沒關係,我們將三個 Slot[] 的地址找出來,一個一個看。


0:011> !DumpHeap /d -mt 00007ffaf84f7ad0
         Address               MT     Size
0000016c91308048 00007ffaf84f7ad0 16743180     
0000016c928524b0 00007ffaf84f7ad0 34719012     
0000016ce9e61020 00007ffaf84f7ad0 71993676  

0:011> !gcroot 0000016c91308048
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016c928524b0
Found 0 unique roots (run '!gcroot -all' to see all roots).
0:011> !gcroot 0000016ce9e61020
Thread 2b0c:
    0000006AFAB7E5F0 00007FFAF84132AE ConsoleApplication3.Program.Main(System.String[]) [C:\4\ConsoleApp1\ConsoleApp1\Program.cs @ 15]
        rbp-18: 0000006afab7e618
            ->  0000016C8000CC08 System.Collections.Generic.HashSet`1[[System.Int32, System.Private.CoreLib]]
            ->  0000016CE9E61020 System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]

從上面可以看到,我通過 gcroot 去找這三個地址的引用根,有兩個是沒有的,最後一個有的自然就是新的 599w 的size,對不對,接下來用 !do 打出這三個地址的值。


0:011> !do 0000016c91308048
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        16743180(0xff7b0c) bytes
Array:       Rank 1, Number of elements 1395263, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016c928524b0
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        34719012(0x211c524) bytes
Array:       Rank 1, Number of elements 2893249, Type VALUETYPE (Print Array)
Fields:
None

0:011> !do 0000016ce9e61020
Name:        System.Collections.Generic.HashSet`1+Slot[[System.Int32, System.Private.CoreLib]][]
Size:        71993676(0x44a894c) bytes
Array:       Rank 1, Number of elements 5999471, Type VALUETYPE (Print Array)
Fields:
None

從上面的 Rank 1, Number of elements 信息中可以看到,原來託管堆不僅有擴容前的Size :2893249,還有更前一次的擴容Size: 1395263,所以按這種情況算: 託管堆上的總大小近似為: 23.7M + 47.4M + 91.5M = 162.6M,我去,不簡單把。。。 也就是說:託管堆上有 162.6 - 91.5 =71.1M 的未回收垃圾 剛才的 47.4M 的空間虛佔用,總浪費為:118.5M,但願我沒有算錯。。。

3. 有解決方案嗎?

在List中大家可以通過 Capacity 去控制List的Size,但是很遺憾,在 HashSet 中並沒有類似的解決方案,只有一個很笨拙的裁剪方法: TrimExcess,用於將當前Size擴展到最接近的 質數 值, 如下代碼所示:


public void TrimExcess()
{
	int prime = HashHelpers.GetPrime(m_count);
	Slot[] array = new Slot[prime];
	int[] array2 = new int[prime];
	int num = 0;
	for (int i = 0; i < m_lastIndex; i++)
	{
		if (m_slots[i].hashCode >= 0)
		{
			array[num] = m_slots[i];
			int num2 = array[num].hashCode % prime;
			array[num].next = array2[num2] - 1;
			array2[num2] = num + 1;
			num++;
		}
	}
}

應用到本案例就是將 289w 限制到 347w,仍然有 58w的空間佔用。 如下圖:

三: 總結

HashSet的時間和空間上虛占遠比你想象的大很多,而且實占也不小,因為底層用到了雙數組 m_slots 和 m_buckets,每個Slot還有三個元素: struct Slot { int hashCode;internal int next;internal T value; },所以了解完原理之後謹慎着用吧。

如您有更多問題與我互動,掃描下方進來吧~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了標籤模塊和友情鏈接模塊的所有功能,本篇來繼續完成博客最後的模塊,文章的管理。

文章列表 & 刪除

先將分頁查詢的列表給整出來,這塊和首頁的分頁列表是類似的,就是多了個Id字段。

先添加兩條路由規則。

@page "/admin/posts"
@page "/admin/posts/{page:int}"

新建返回數據默認QueryPostForAdminDto.cs。

//QueryPostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class QueryPostForAdminDto
    {
        /// <summary>
        /// 年份
        /// </summary>
        public int Year { get; set; }

        /// <summary>
        /// Posts
        /// </summary>
        public IEnumerable<PostBriefForAdminDto> Posts { get; set; }
    }
}

//PostBriefForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class PostBriefForAdminDto : PostBriefDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}

然後添加所需的參數:當前頁碼、限制條數、總頁碼、文章列表返回數據模型。

/// <summary>
/// 當前頁碼
/// </summary>
[Parameter]
public int? page { get; set; }

/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;

/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;

/// <summary>
/// 文章列表數據
/// </summary>
private ServiceResult<PagedList<QueryPostForAdminDto>> posts;

然後在初始化函數OnInitializedAsync()中調用API獲取文章數據.

/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    // 設置默認值
    page = page.HasValue ? page : 1;

    await RenderPage(page);
}

/// <summary>
/// 點擊頁碼重新渲染數據
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
    // 獲取數據
    posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostForAdminDto>>>($"/blog/admin/posts?page={page}&limit={Limit}");

    // 計算總頁碼
    TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}

在初始化中判斷page參數,如果沒有值給他設置一個默認值1。RenderPage(int? page)方法是調用API返回數據,並計算出總頁碼值。

最後在頁面上進行數據綁定。

<AdminLayout>
    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap archive">
            <NavLink style="float:right" href="/admin/post"><h3>~~~ 新增文章 ~~~</h3></NavLink>
            @if (posts.Success && posts.Result.Item.Any())
            {
                @foreach (var item in posts.Result.Item)
                {
                    <h3>@item.Year</h3>
                    @foreach (var post in item.Posts)
                    {
                        <article class="archive-item">
                            <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(post.Id))"></NavLink>
                            <NavLink title="編輯" @onclick="@(async () => await Common.NavigateTo($"/admin/post/{post.Id}"))"></NavLink>
                            <NavLink target="_blank" class="archive-item-link" href="@("/post" + post.Url)">@post.Title</NavLink>
                            <span class="archive-item-date">@post.CreationTime</span>
                        </article>
                    }
                }
                <nav class="pagination">
                    @for (int i = 1; i <= TotalPage; i++)
                    {
                        var _page = i;

                        if (page == _page)
                        {
                            <span class="page-number current">@_page</span>
                        }
                        else
                        {
                            <a class="page-number" @onclick="@(() => RenderPage(_page))" href="/admin/posts/@_page">@_page</a>
                        }
                    }
                </nav>
            }
            else
            {
                <ErrorTip />
            }
        </div>
    }
</AdminLayout>

HTML內容放在組件AdminLayout中,當 posts 沒加載完數據的時候显示加載組件<Loading />。

在頁面上循環遍歷文章數據和翻頁頁碼,每篇文章標題前面添加兩個按鈕刪除和編輯,同時單獨加了一個新增文章的按鈕。

刪除文章調用DeleteAsync(int id)方法,需要傳遞參數,當前文章的id。

新增和編輯按鈕都跳轉到”/admin/post”頁面,當編輯的時候將id也傳過去即可,路由規則為:”/admin/post/{id}”。

刪除文章“方法如下:

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task DeleteAsync(int id)
{
    // 彈窗確認
    bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這篇該死的文章嗎");

    if (confirmed)
    {
        var response = await Http.DeleteAsync($"/blog/post?id={id}");

        var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

        if (result.Success)
        {
            await RenderPage(page);
        }
    }
}

刪除之前進行二次確認,避免誤刪,當確認刪除之後調用刪除文章API,最後重新渲染數據即可。

新增 & 更新文章

完成了後台文章列表的查詢和刪除,現在整個博客模塊功能就差新增和更新文章了,勝利就在前方,沖啊。

這塊的開發工作耗費了我太多時間,因為想使用 markdown 來寫文章,找了一圈下來沒有一個合適的組件,所以退而求次只能選擇現有的markdown編輯器來實現了。

我這裏選擇了開源的編輯器Editor.md,有需要的可以去 Github 自己下載,https://github.com/pandao/editor.md 。

將下載的資源包解壓放在 wwwroot 文件夾下,默認是比較大的,而且還有很多示例文件,我已經將其精簡了一番,可以去我 Github 下載使用。

先來看下最終的成品效果吧。

是不是感覺還可以,廢話不多說,接下里告訴大家如何實現。

在 Admin 文件夾下添加post.razor組件,設置路由,並且引用一個樣式文件,在頁面中引用樣式文件好像不太符合標準,不過無所謂了,這個後台就自己用,而且還就這一個頁面用得到。

@page "/admin/post"
@page "/admin/post/{id:int}"

<link href="./editor.md/css/editormd.css" rel="stylesheet" />

<AdminLayout>
    ...
</AdminLayout>

把具體HTML內容放在組件AdminLayout中。

因為新增和編輯放在同一個頁面上,所以當id參數不為空的時候需要添加一個id參數,同時默認一進來就讓頁面显示加載中的組件,當頁面和數據加載完成后在显示具體的內容,所以在指定一個布爾類型的是否加載參數isLoading。

我們的編輯器主要依賴JavaScript實現的,所以這裏不可避免要使用到JavaScript了。

在app.js中添加幾個全局函數。

switchEditorTheme: function () {
    editor.setTheme(localStorage.editorTheme || 'default');
    editor.setEditorTheme(localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default');
    editor.setPreviewTheme(localStorage.editorTheme || 'default');
},
renderEditor: async function () {
	await this._loadScript('./editor.md/lib/zepto.min.js').then(function () {
	    func._loadScript('./editor.md/editormd.js').then(function () {
	        editor = editormd("editor", {
	            width: "100%",
	            height: 700,
	            path: './editor.md/lib/',
	            codeFold: true,
	            saveHTMLToTextarea: true,
	            emoji: true,
	            atLink: false,
	            emailLink: false,
	            theme: localStorage.editorTheme || 'default',
	            editorTheme: localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default',
	            previewTheme: localStorage.editorTheme || 'default',
	            toolbarIcons: function () {
	                return ["bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "h1", "h2", "h3", "h4", "h5", "h6", "list-ul", "list-ol", "hr", "link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "html-entities", "emoji", "watch", "preview", "fullscreen", "clear", "||", "save"]
	            },
	            toolbarIconsClass: {
	                save: "fa-check"
	            },
	            toolbarHandlers: {
	                save: function () {
	                    func._shoowBox();
	                }
	            },
	            onload: function () {
	                this.addKeyMap({
	                    "Ctrl-S": function () {
	                        func._shoowBox();
	                    }
	                });
	            }
	        });
	    });
	});
},
_shoowBox: function () {
    DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');
},
_loadScript: async function (url) {
    let response = await fetch(url);
    var js = await response.text();
    eval(js);
}

renderEditor主要實現了動態加載JavaScript代碼,將markdown編輯器渲染出來。這裏不多說,都是Editor.md示例裏面的代碼。

為了兼容暗黑色主題,這裏還加了一個切換編輯器主題的JavaScript方法,switchEditorTheme。

_shoowBox就厲害了,這個方法是調用的.NET組件中的方法,前面我們用過了在Blazor中調用JavaScript,這裏演示了JavaScript中調用Blazor中的組件方法。

現在將所需的幾個參數都添加到代碼中。

/// <summary>
/// 定義一個委託方法,用於組件實例方法調用
/// </summary>
private static Func<Task> action;

/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;

/// <summary>
/// 修改時的文章Id
/// </summary>
[Parameter]
public int? Id { get; set; }

/// <summary>
/// 格式化的標籤
/// </summary>
private string tags { get; set; }

/// <summary>
/// 默認显示加載中
/// </summary>
private bool isLoading = true;

/// <summary>
/// 文章新增或者修改輸入參數
/// </summary>
private PostForAdminDto input;

/// <summary>
/// API返回的分類列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;

大家看看註釋就知道參數是做什麼的了。

現在我們在初始化函數中將所需的數據通過API獲取到。

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    action = ChangeOpenStatus;

    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    if (Id.HasValue)
    {
        var post = await Http.GetFromJsonAsync<ServiceResult<PostForAdminDto>>($"/blog/admin/post?id={Id}");

        if (post.Success)
        {
            var _post = post.Result;
            input = new PostForAdminDto
            {
                Title = _post.Title,
                Author = _post.Author,
                Url = _post.Url,
                Html = _post.Html,
                Markdown = _post.Markdown,
                CategoryId = _post.CategoryId,
                Tags = _post.Tags,
                CreationTime = _post.CreationTime
            };

            tags = string.Join(",", input.Tags);
        }
    }
    else
    {
        input = new PostForAdminDto()
        {
            Author = "阿星Plus",
            CreationTime = DateTime.Now
        };
    }

    categories = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");

    // 渲染編輯器
    await Common.InvokeAsync("window.func.renderEditor");

    // 關閉加載
    isLoading = !isLoading;
}

action是一個異步的委託,在初始化中執行了ChangeOpenStatus方法,這個方法等會說,然後獲取localStorage中token的值。

通過參數Id是否有值來判斷當前是新增文章還是更新文章,如果有值就是更新文章,這時候需要根據id去將文章的數據拿到賦值給PostForAdminDto對象展示在頁面上,如果沒有可以添加幾個默認值給PostForAdminDto對象。

因為文章需要分類和標籤的數據,同時這裏將分類的數據也查出來,標籤默認是List列表,將其轉換成字符串類型。

但完成上面操作后,調用JavaScript方法renderEditor渲染渲染編輯器,最後關閉加載,显示頁面。

現在來看看頁面。

<AdminLayout>
    @if (isLoading)
    {
        <Loading />
    }
    else
    {
        <div class="post-box">
            <div class="post-box-item">
                <input type="text" placeholder="標題" autocomplete="off" @bind="@input.Title" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="作者" autocomplete="off" @bind="@input.Author" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div class="post-box-item">
                <input type="text" placeholder="URL" autocomplete="off" @bind="@input.Url" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="時間" autocomplete="off" @bind="@input.CreationTime" @bind:format="yyyy-MM-dd HH:mm:sss" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div id="editor">
                <textarea style="display:none;">@input.Markdown</textarea>
            </div>

            <Box OnClickCallback="@SubmitAsync" Open="@Open" ButtonText="發布">
                <div class="box-item">
                    <b>分類:</b>
                    @if (categories.Success && categories.Result.Any())
                    {
                        @foreach (var item in categories.Result)
                        {
                            <label><input type="radio" name="category" value="@item.Id" @onchange="@(() => { input.CategoryId = item.Id; })" checked="@(item.Id == input.CategoryId)" />@item.CategoryName</label>
                        }
                    }
                </div>
                <div class="box-item"></div>
                <div class="box-item">
                    <b>標籤:</b>
                    <input type="text" @bind="@tags" @bind:event="oninput" />
                </div>
            </Box>
        </div>
    }
</AdminLayout>

添加了四個input框,分別用來綁定標題、作者、URL、時間,<div id="editor"></div>中為編輯器所需。

然後我這裏還是把之前的彈窗組件搞出來了,執行邏輯不介紹了,在彈窗組件中自定義显示分類和標籤的內容,將獲取到的分類和標籤綁定到具體位置。

每個分類都是一個radio標籤,並且對應一個點擊事件,點哪個就把當前分類的Id賦值給PostForAdminDto對象。

所有的input框都使用@bind和@bind:event綁定數據和獲取數據。

Box彈窗組件這裏自定義了按鈕文字,ButtonText="發布"。

/// <summary>
/// 改變Open狀態,通知組件渲染
/// </summary>
private async Task ChangeOpenStatus()
{
    Open = true;

    var markdown = await Common.InvokeAsync<string>("editor.getMarkdown");
    var html = await Common.InvokeAsync<string>("editor.getHTML");

    if (string.IsNullOrEmpty(input.Title) || string.IsNullOrEmpty(input.Url) ||
        string.IsNullOrEmpty(input.Author) || string.IsNullOrEmpty(markdown) ||
        string.IsNullOrEmpty(html))
    {
        await Alert();
    }

    input.Html = html;
    input.Markdown = markdown;

    StateHasChanged();
}

/// <summary>
/// 暴漏給JS執行,彈窗確認框
/// </summary>
[JSInvokable("showbox")]
public static void ShowBox()
{
    action.Invoke();
}
/// <summary>
/// alert提示
/// </summary>
/// <returns></returns>
private async Task Alert()
{
    Open = false;

    await Common.InvokeAsync("alert", "\n好像漏了點什麼吧");
    return;
}

現在可以來看看ChangeOpenStatus方法了,這個是改變當前彈窗狀態的一個方法。為什麼需要這個方法呢?

因為在Blazor中JavaScript想要調用組件內的方法,方法必須是靜態的,那麼只能通過這種方式去實現了,在靜態方法是不能夠直接改變彈窗的狀態值的。

其實也可以不用這麼麻煩,因為我在編輯器上自定義了一個按鈕,為了好看一些所以只能曲折一點,嫌麻煩的可以直接在頁面上搞個按鈕執行保存數據邏輯也是一樣的。

使用JSInvokableAttribute需要在_Imports.razor中添加命名空間@using Microsoft.JSInterop。

ChangeOpenStatus中獲取到文章內容:HTML和markdown,賦值給PostForAdminDto對象,要先進行判斷頁面上的幾個參數是否有值,沒值的話給出提示執行Alert()方法,最後使用StateHasChanged()通知組件其狀態已更改。

Alert方法就是調用原生的JavaScriptalert方法,給出一個提示。

ShowBox就是暴漏給JavaScript的方法,使用DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');進行調用。

那麼現在一切都正常進行的情況下,點擊編輯器上自定義的保存按鈕,頁面上值不為空的情況下就會彈出我們的彈窗組件Box。

最後在彈窗組件的回調方法中執行新增文章還是更新文章。

/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
    if (string.IsNullOrEmpty(tags) || input.CategoryId == 0)
    {
        await Alert();
    }

    input.Tags = tags.Split(",");

    var responseMessage = new HttpResponseMessage();

    if (Id.HasValue)
        responseMessage = await Http.PutAsJsonAsync($"/blog/post?id={Id}", input);
    else
        responseMessage = await Http.PostAsJsonAsync("/blog/post", input);

    var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
    if (result.Success)
    {
        await Common.NavigateTo("/admin/posts");
    }
}

打開彈窗后執行回調事件之前還是要判斷值是否為空,為空的情況下還是給出alert提示,此時將tags標籤還是轉換成List列表,根據Id是否有值去執行新增數據或者更新數據,最終成功后跳轉到文章列表頁。

本片到這裏就結束了,主要攻克了在Blazor中使用Markdown編輯器實現新增和更新文章,這個系列差不多就快結束了,預計還有2篇的樣子,感謝各位的支持。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

重學 Java 設計模式:實戰代理模式「模擬mybatis-spring中定義DAO接口,使用代理類方式操作數據庫原理實現場景」

作者:小傅哥

博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

難以跨越的瓶頸期,把你拿捏滴死死的!

編程開發學習過程中遇到的瓶頸期,往往是由於看不到前進的方向。這個時候你特別希望能有人告訴你,你還欠缺些什麼朝着哪個方向努力。而導致這一問題的主要原因是由於日常的業務開發太過於複製過去,日復一日的重複。沒有太多的挑戰,也沒參与過較大體量的業務場景,除了這些開發場景因素外,還有缺少組內的技術氛圍和技術分享,沒有人做傳播和佈道者,也缺少自己對各項技術學習的熱情,從而導致一直遊盪在瓶頸之下,難以提升。

小公司與大公司,選擇哪個?

刨除掉薪資以外你會選擇什麼,是不有人建議小公司,因為可以接觸到各個環境,也有人建議大公司,因為正規體量大可以學習到更多。有些時候你的技術成長緩慢也是因為你的不同選擇而導致的,小公司確實要接觸各個環境,但往往如果你所做的業務體量不高,那麼你會用到的技術棧就會相對較少,同時也會技術棧研究的深度也會較淺。大公司中確實有時候你不需要去關心一個集群的部署和維護、一个中間件的開發、全套服務監控等等,但如果你願意了解這些技術在內部都是公開的,你會擁有無限的技術營養可以補充。而這最主要的是提升視野和事業。

除了業務中的CRUD開發,有些技術你真的很難接觸到!

可能很多小夥伴認為技術開發就是承接下產品需求,寫寫CRUD,不會的百度一下,就完事了,總覺得別人問的東西像再造火箭一樣。但在高體量、高併發的業務場景下,每一次的壓測優化,性能提升,都像在研究一道數學題一樣,反覆的錘鍊,壓榨性能。不斷的深究,找到最合適的設計。除了這些優化提升外,還有那麼廣闊的技術體系棧,都可能因為你只是注重CRUD而被忽略;字節碼編程、領域驅動設計架構、代理模式中間件開發、JVM虛擬機實現原理等等。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. Spring 4.3.24.RELEASE
  4. 涉及工程三個,可以通過關注公眾號:bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-12-00 模擬MyBatis開發中間件代理類部分

三、代理模式介紹

代理模式有點像老大和小弟,也有點像分銷商。主要解決的是問題是為某些資源的訪問、對象的類的易用操作上提供方便使用的代理服務。而這種設計思想的模式經常會出現在我們的系統中,或者你用到過的組件中,它們都提供給你一種非常簡單易用的方式控制原本你需要編寫很多代碼的進行使用的服務類。

類似這樣的場景可以想到;

  1. 你的數據庫訪問層面經常會提供一個較為基礎的應用,以此來減少應用服務擴容時不至於數據庫連接數暴增。
  2. 使用過的一些中間件例如;RPC框架,在拿到jar包對接口的描述后,中間件會在服務啟動的時候生成對應的代理類,當調用接口的時候,實際是通過代理類發出的socket信息進行通過。
  3. 另外像我們常用的MyBatis,基本是定義接口但是不需要寫實現類,就可以對xml或者自定義註解里的sql語句進行增刪改查操作。

四、案例場景模擬

在本案例中我們模擬實現mybatis-spring中代理類生成部分

對於Mybatis的使用中只需要定義接口不需要寫實現類就可以完成增刪改查操作,有疑問的小夥伴,在本章節中就可以學習到這部分知識。解析下來我們會通過實現一個這樣的代理類交給spring管理的核心過程,來講述代理類模式。

這樣的案例場景在實際的業務開發中其實不多,因為這是將這種思想運用在中間件開發上,而很多小夥伴經常是做業務開發,所以對Spring的bean定義以及註冊和對代理以及反射調用的知識了解的相對較少。但可以通過本章節作為一個入門學習,逐步了解。

五、代理類模式實現過程

接下來會使用代理類模式來模擬實現一個Mybatis中對類的代理過程,也就是只需要定義接口,就可以關聯到方法註解中的sql語句完成對數據庫的操作。

這裏需要注意一些知識點;

  1. BeanDefinitionRegistryPostProcessor,spring的接口類用於處理對bean的定義註冊。
  2. GenericBeanDefinition,定義bean的信息,在mybatis-spring中使用到的是;ScannedGenericBeanDefinition 略有不同。
  3. FactoryBean,用於處理bean工廠的類,這個類非常見。

1. 工程結構

itstack-demo-design-12-00
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── agent
    │   │       │	├── MapperFactoryBean.java
    │   │       │	├── RegisterBeanFactory.java
    │   │       │	└── Select.java
    │   │       └── IUserDao.java
    │   └── resources	
    │       └── spring-config.xml
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

代理模式中間件模型結構

  • 此模型中涉及的類並不多,但都是抽離出來的核心處理類。主要的事情就是對類的代理和註冊到spring中。
  • 上圖中最上面是關於中間件的實現部分,下面對應的是功能的使用。

2. 代碼實現

2.1 自定義註解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {

    String value() default "";  // sql語句

}
  • 這裏我們定義了一個模擬mybatis-spring中的自定義註解,用於使用在方法層面。

2.2 Dao層接口

public interface IUserDao {

    @Select("select userName from user where id = #{uId}")
    String queryUserInfo(String uId);

}
  • 這裏定義一個Dao層接口,並把自定義註解添加上。這與你使用的mybatis組件是一樣的。
  • 2.1和2.2是我們的準備工作,後面開始實現中間件功能部分。

2.3 代理類定義

public class MapperFactoryBean<T> implements FactoryBean<T> {

    private Logger logger = LoggerFactory.getLogger(MapperFactoryBean.class);

    private Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        InvocationHandler handler = (proxy, method, args) -> {
            Select select = method.getAnnotation(Select.class);
            logger.info("SQL:{}", select.value().replace("#{uId}", args[0].toString()));
            return args[0] + ",小傅哥,bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!";
        };
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperInterface}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}
  • 如果你有閱讀過mybatis源碼,是可以看到這樣的一個類;MapperFactoryBean,這裏我們也模擬一個這樣的類,在裏面實現我們對代理類的定義。
  • 通過繼承FactoryBean,提供bean對象,也就是方法;T getObject()。
  • 在方法getObject()中提供類的代理以及模擬對sql語句的處理,這裏包含了用戶調用dao層方法時候的處理邏輯。
  • 還有最上面我們提供構造函數來透傳需要被代理類,Class<T> mapperInterface,在mybatis中也是使用這樣的方式進行透傳。
  • 另外getObjectType()提供對象類型反饋,以及isSingleton()返回類是單例的。

2.4 將Bean定義註冊到Spring容器

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {
    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(MapperFactoryBean.class);
        beanDefinition.setScope("singleton");
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(IUserDao.class);

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        // left intentionally blank
    }

}
  • 這裏我們將代理的bean交給spring容器管理,也就可以非常方便讓我們可以獲取到代理的bean。這部分是spring中關於一個bean註冊過程的源碼。
  • GenericBeanDefinition,用於定義一個bean的基本信息setBeanClass(MapperFactoryBean.class);,也包括可以透傳給構造函數信息addGenericArgumentValue(IUserDao.class);
  • 最後使用 BeanDefinitionReaderUtils.registerBeanDefinition,進行bean的註冊,也就是註冊到DefaultListableBeanFactory中。

2.5 配置文件spring-config

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-autowire="byName">

    <bean id="userDao" class="org.itstack.demo.design.agent.RegisterBeanFactory"/>

</beans>
  • 接下來在配置文件中添加我們的bean配置,在mybatis的使用中一般會配置掃描的dao層包,這樣就可以減少這部分的配置。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_IUserDao() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo("100001");
    logger.info("測試結果:{}", res);
}
  • 測試的過程比較簡單,通過加載Bean工廠獲取我們的代理類的實例對象,之後調用方法返回結果。
  • 那麼這個過程你可以看到我們是沒有對接口先一個實現類的,而是使用代理的方式給接口生成一個實現類,並交給spring管理。

3.2 測試結果

23:21:57.551 [main] DEBUG o.s.core.env.StandardEnvironment - Adding PropertySource 'systemProperties' with lowest search precedence
...
23:21:57.858 [main] DEBUG o.s.c.s.ClassPathXmlApplicationContext - Unable to locate LifecycleProcessor with name 'lifecycleProcessor': using default [org.springframework.context.support.DefaultLifecycleProcessor@7bc1a03d]
23:21:57.859 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'lifecycleProcessor'
23:21:57.860 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
23:21:57.861 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userDao'
23:21:57.915 [main] INFO  o.i.d.design.agent.MapperFactoryBean - SQL:select userName from user where id = 100001
23:21:57.915 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:100001,小傅哥,bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!

Process finished with exit code 0
  • 從測試結果可以看到,我們打印了SQL語句,這部分語句是從自定義註解中獲取的;select userName from user where id = 100001,我們做了簡單的適配。在mybatis框架中會交給SqlSession的實現類進行邏輯處理返回操作數據庫數據
  • 而這裏我們的測試結果是一個固定的,如果你願意更加深入的研究可以嘗試與數據庫操作層進行關聯,讓這個框架可以更加完善。

六、總結

  • 關於這部分代理模式的講解我們採用了開發一個關於mybatis-spring中間件中部分核心功能來體現代理模式的強大之處,所以涉及到了一些關於代理類的創建以及spring中bean的註冊這些知識點,可能在平常的業務開發中都是很少用到的,但是在中間件開發中確實非常常見的操作。
  • 代理模式除了開發中間件外還可以是對服務的包裝,物聯網組件等等,讓複雜的各項服務變為輕量級調用、緩存使用。你可以理解為你家裡的電燈開關,我們不能操作220v電線的人肉連接,但是可以使用開關,避免觸電。
  • 代理模式的設計方式可以讓代碼更加整潔、乾淨易於維護,雖然在這部分開發中額外增加了很多類也包括了自己處理bean的註冊等,但是這樣的中間件復用性極高也更加智能,可以非常方便的擴展到各個服務應用中。

七、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式(多種類型商品發獎場景)
  • 2. 重學 Java 設計模式:實戰建造者模式(裝修物料組合套餐選配場景)
  • 3. 重學 Java 設計模式:實戰原型模式(多套試每人題目和答案亂序場景)
  • 4. 重學 Java 設計模式:實戰橋接模式(多支付渠道「微信支付寶」與多支付模式「刷臉、指紋」場景)
  • 5. 重學 Java 設計模式:實戰組合模式(營銷差異化人群發券決策樹引擎搭建場景)
  • 6. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

Spring系列.AOP原理簡析

Spring AOP使用簡介

Spring的兩大核心功能是IOC和AOP。當我們使用Spring的AOP功能時是很方便的。只需要進行下面的配置即可。

@Component
@Aspect
public class MyAspect {

//PointCut匹配的方法必須是Spring中bean的方法
//Pointcut可以有下列方式來定義或者通過&& || 和!的方式進行組合.
//下面定義的這些切入點就可以通過&& ||組合

private static Logger logger = LoggerFactory.getLogger(MyAspect.class);

//*:代表方法的返回值可以是任何類型
//整個表達式匹配controller包下面任何的的echo方法,方法入參樂意是任意
@Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(..))")
public void pointCut1(){}

@Before("pointCut1()")
public void befor(){
    logger.info("前置通知vvvv...");
    logger.info("我要做些事情...");
}
}

然後再開啟註解

//自動選擇合適的AOP代理
//傳統xml這樣配置:<aop:aspectj-autoproxy/>

//exposeProxy = true屬性設置成true,意思是將動態生成的代理類expose到AopContext的ThreadLocal線程
//可以通過AopContext.currentProxy();獲取到生成的動態代理類。

//proxyTargetClass屬性設置動態代理使用JDK動態代理還是使用CGlib代理,設置成true是使用CGlib代理,false的話是使用JDK動態代理

//注意:如果使用Spring Boot的話,下面的配置可以不需要。AopAutoConfiguration這個自動配置類中已經自動開啟了AOP
//默認使用CGLIB動態代理,Spring Boot配置的優先級高於下面的配置

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = false)
public class AopConfig {

}

通上面的配置,當我們調用controller包下面的任何類的echo方法時就會觸發前置通知。其實這個說法不是很準確。因為我們調用的類已經不是我們自己寫的類了。而是Spring框架通過動態代理生成的類。

稍微了解一點Spring AOP的同學都會知道Spring的AOP是通過動態代理實現的。那Spring是怎麼生成動態代理類,並將Advice織入代理類的呢?整個流程是怎樣的呢?下面就分析下Spring生成動態代理類的過程。

需要說明下的是,本博客旨在梳理整個AOP動態代理的過程,細節方面需要大家自己去看。

@EnableAspectJAutoProxy幹了些啥

如果讓你從頭開始研究下AOP的原理,你是不是一頭霧水,根本不知道從何入手。但其實看Spring的代碼有個小技巧:如果你要研究一個功能,可以從開啟這個功能的Enable註解開始看。Spring的很多功能都是通過Enable註解開啟的,所以這些註解肯定和這些功能相關。

那麼這邊我們可以從@EnableAspectJAutoProxy這個註解開始着手,看下這個註解做了些什麼操作。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    //設置為true的話就一直使用cglib動態代理
    //設置為false的話,對於接口使用jdk動態代理,對於類使用cglib代理
	boolean proxyTargetClass() default false;
	boolean exposeProxy() default false;
}

看到上面的@Impoer註解,我們很自然就會想到去看AspectJAutoProxyRegistrar這個類。

//AspectJAutoProxyRegistrar源代碼
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * 主要作用也就是註冊AnnotationAwareAspectJAutoProxyCreator
     */
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //註冊AnnotationAwareAspectJAutoProxyCreator的BeanDefinition
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            //給上面註冊的BeanDefinition中添加兩個屬相proxyTargetClass和exposeProxy
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }
    }

}

我們可以看到上面的類中也沒幹什麼特別的事情,就註冊了一個BeanDefinition。如果我們點進去看下AnnotationAwareAspectJAutoProxyCreator這個類的源代碼會發現這個類竟然實現了InstantiationAwareBeanPostProcessor這個接口。熟悉Spring尿性的朋友會敏銳的感覺到Spring可能是在postProcessBeforeInstantiation或者postProcessAfterInstantiation這些方法中對Bean進行動態代理的。

“大膽假設,小心求證”,讓我們帶着這個猜想去看看AnnotationAwareAspectJAutoProxyCreator到底幹了些什麼?

AnnotationAwareAspectJAutoProxyCreator生成動態代理類

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
    Object cacheKey = getCacheKey(beanClass, beanName);

    if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
        if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
        }
        if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
        }
    }
    // 一般不指定CustomTargetSource,所以不會進入這段代碼,所以關鍵代碼在
    // postProcessAfterInitialization中
    // Create proxy here if we have a custom TargetSource.
    // Suppresses unnecessary default instantiation of the target bean:
    // The TargetSource will handle target instances in a custom fashion.
    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
    if (targetSource != null) {
        if (StringUtils.hasLength(beanName)) {
            this.targetSourcedBeans.add(beanName);
        }
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
        Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    return null;
}

下面是創建動態代理類的關鍵代碼。

@Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                //這邊是創建代碼類的關鍵代碼
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }

        // Create proxy if we have advice.
        //獲取當前Bean配置的advice,這步是關鍵
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            //創建代理
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }

        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

層層dedug進去我們能看到下面這段代碼,我們口中常說的JDK動態代理和Cglib動態代理就是在這邊生成的。

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                //生成JDK動態代理
                return new JdkDynamicAopProxy(config);
            }
            //生成Cglib動態代理
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }
    /**
     * Determine whether the supplied {@link AdvisedSupport} has only the
     * {@link org.springframework.aop.SpringProxy} interface specified
     * (or no proxy interfaces specified at all).
     */
    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] ifcs = config.getProxiedInterfaces();
        return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
    }

到此,我們已經簡單分析了Spring動態代理類的生成流程。

PS:關於InstantiationAwareBeanPostProcessor接口和BeanPostProcessor接口大家可以自行了解下,這兩個接口是Spring中非常重要的接口。看懂了這兩個接口,Spring很多“神秘”的功能你就能理解了。

簡單總結

通過上面分析,其實我們發現如果不去看AOP動態代理類生成的細節的話,整個Spring AOP的流程還是挺簡單的:

  • @EnableAspectJAutoProxy註解通過AopConfigUtils這個工具類註冊AnnotationAwareAspectJAutoProxyCreator這個類,這個類實現了InstantiationAwareBeanPostProcessor接口,所以會在Bean實例化前後對Bean做一系列額外的操作;
  • AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization中會找出所有和當前Bean相關的Advice,如果找到就創建相應的動態代理類,如果找不到就不生成,返回原始類。

所以整個大流程就這麼簡單。

一些重要類:

  • @EnableAspectJAutoProxy;
  • AspectJAutoProxyRegistrar:註冊AnnotationAwareAspectJAutoProxyCreator
  • AnnotationAwareAspectJAutoProxyCreator:AOP動態代理自動生成的處理類,其他類似的類有AspectJAwareAdvisorAutoProxyCreator和InfrastructureAdvisorAutoProxyCreator等;
  • AopConfigUtils:AOP配置工具類
  • ProxyFactory:代理工廠
  • AopProxy接口:常見實現類ObjenesisCglibAopProxy、JdkDynamicAopProxy

參考

  • https://blog.csdn.net/zhoushimiao1990/article/details/89853368

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Asp.Net Core入門之自定義中間件

什麼是中間件?

這裏引用官方解釋:

       中間件是用於組成應用程序管道來處理請求和響應的組件。管道內的每一個組件都可以選擇是否將請求交給下一個組件、並在管道中調用下一個組件之前和之後執行某些操作。請求委託被用來建立請求管道,請求委託處理每一個 HTTP 請求。

       請求委託通過使用 IApplicationBuilder 類型的 Run、Map 以及 Use 擴展方法來配置,並在 Startup 類中傳給 Configure 方法 。每個單獨的請求委託都可以被指定為一個內嵌匿名方法,或其定義在一個可重用的類中。這些可重用的類被稱作 中間件 或 中間件組件。每個位於請求管道內的中間件組件負責調用管道中下一個組件,或適時短路調用鏈。

       ASP.NET 請求管道由一系列的請求委託所構成,它們一個接着一個被調用,如圖所示(該執行線程按黑色箭頭的順序執行):

 

ASP.Net Core內置很多中間件,如:靜態文件,身份驗證,MVC等。

這裏需要注意的是,中間件添加的順序非常重要,這會影響他們處理請求的順序。如果順序添加不對,很可能其中某个中間件會造成管道短路,不能觸發下一個請求委託,造成系統異常。

自定義中間件

1.自定義中間件處理邏輯。

 

 1     public class MyMiddleWare
 2     {
 3         private readonly ILogger logger;
 4         private readonly RequestDelegate next;
 5         /// <summary>
 6         /// 通過構造函數 注入Logger日誌和Http請求委託
 7         /// </summary>
 8         /// <param name="_logger"></param>
 9         /// <param name="_next"></param>
10         public MyMiddleWare(ILogger<MyMiddleWare> _logger, RequestDelegate _next)
11         {
12             logger = _logger;
13             next = _next;
14         }
15 
16         /// <summary>
17         /// 在調用下一個組件之前和之後執行某些操作
18         /// </summary>
19         /// <param name="context"></param>
20         /// <returns></returns>
21         public async Task Invoke(HttpContext context)
22         {
23             logger.LogInformation("自定義中間件,開始" + DateTime.Now);
24             await next.Invoke(context);
25             logger.LogInformation("自定義中間件,結束" + DateTime.Now);
26         }
27     }

 

2.使用IApplicationBuilder嚮應用程序的請求管道添加中間件

1     public static class ServiceExtensions
2     {
3         public static IApplicationBuilder UserMyMiddleWare(this IApplicationBuilder app)
4         {
5             return app.UseMiddleware<MyMiddleWare>();
6         }
7     }

3.通過Configure方法添加自定義中間件。

Configure 方法用於指定 ASP.NET 應用程序將如何響應每一個 HTTP 請求。中間件需要最終在這裏添加。

 1         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
 2         {
 3             if (env.IsDevelopment())
 4             {
 5                 app.UseDeveloperExceptionPage();
 6             }
 7             //wwroot目錄下靜態文件
 8             app.UseStaticFiles();
 9             //添加自定義中間件
10             app.UserMyMiddleWare();
11         }

 

當然大家也可以在Configure 方法中直接使用 app.UseMiddleware<MyMiddleWare>()進行添加,我這裡是單獨做了一個派生類,方便管理吧。

 

好了,然後我們啟動程序,大家會發現我們剛才添加的中間件處理請求信息:

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※FB行銷專家,教你從零開始的技巧

代碼之間為什麼要加空格?

據我觀察,大多數人寫代碼喜歡在符號兩側加空格,而且還會嘲諷不加空格的人。這些支持者認為,加空格可以讓代碼美觀,增強代碼的可讀性。但是我不喜歡加空格,因為這麼寫對代碼含義沒有影響,浪費空間還很麻煩。我不認為多幾個空格就能讓原本凌亂的代碼清晰起來,反而會讓代碼看起來很鬆散,影響閱讀。

以上,是我在知乎上看到的一個題目,感覺蠻清新的,說得似乎有理有據。就好像在說,據我觀察,大多數人喜歡早晚刷牙,而且還會嘲笑不刷牙的人。這些支持者認為,刷牙有益於牙齒健康,空腔清新。但我不喜歡刷牙,因為我牙齒沒壞,吃嘛嘛香。我不認為刷牙能保護牙齒健康,反而很浪費時間。

對吧?我敢保證,這世界上有不少人不刷牙。說出來不怕大家笑話,我上初中之前真沒刷過牙,就算是到現在,很多農村的孩子仍然沒有刷牙的習慣。

在我寫代碼的前一兩年裡,寫代碼也不習慣加空格,覺得很麻煩,並且加不加空格,真的不影響代碼的運行效果。

// 加空格的寫法
int a = 5;
int b = 3;
a = a + b;

// 不加空格的寫法
int a=5;
int b=3;
a=a+b;

想知道為什麼後來我開始刷牙了呢?

記得小學五年級的時候,牙齒疼過一次,真的是喪心病狂地疼,什麼東西也吃不了。那時候,哪有看牙科的,完全沒有,至少在我那時候的成長環境下。

沒辦法,疼了大概有一周的時間,終於打聽到鄰村有一個老先生,他那有秘方,父母就帶我去了。還真的見效,某一種秘制的液體滴在牙齒上,超級冰涼的那種,感覺整個牙齒完全失去了知覺。

有了這段痛苦的記憶,上初中后,看到很多同學都刷牙,於是我也就買了牙刷。初中的條件還是很差勁的,冬天,沒有熱水,洗臉的水和刷牙的水,都是前一天下午飯缸里接的水,放在課桌里。第二天早上到了教室,同學們就互幫互助,端着對方的飯缸倒水洗臉,臉沒凍壞,牙沒凍壞就是萬幸啊。

我在代碼里加空格,和開始刷牙有相似的經歷。

Thisisavarofthings=yourfuckingtype+otherdamnedresult*me;
oh=mygodblessedsweetheart-thesaintangel/cursedcorpsehere;

哦,老天,上面這段代碼里就完全沒有空格,看起來密密麻麻,可讀性差到你感覺自己的雙眼受到了侮辱。

當符號兩側的單詞很簡單的時候,簡單到只有 a 和 b 的時候,的確,不加空格好像看起來更緊湊一些。但考慮到實際的編碼中,命名要有意義,長度都不會太短,這時候,如果沒有空格的輔助,代碼就真的不堪入目了。

Thisisavarofthings = yourfuckingtype + otherdamnedresult * me;
oh = mygodblessedsweetheart - thesaintangel / cursedcorpsehere;

OK,現在加了空格,是不是觀感上就完全不一樣了。單詞與單詞之間,很清楚,很協調,不拖泥帶水。

一般的 IDE 都有自動格式化代碼的快捷鍵,寫完代碼來那麼一下,立馬就會把該有的空格給加上。我現在已經完全習慣手敲空格了,感覺自己很優雅,甚至有點大師範的感覺,畢竟程序員還是要有點追求的,對吧?

說到這,不得不替古人感慨一下,由於紙張稀缺,所以大師們寫出來的文章都是沒有標點符號的,要是沒點文化水平,密密麻麻一團的字擠在一起,真不一定能讀的懂,況且漢語是這麼博大精深。

我們算是幸運的,讀到的文言文都經過了加工,編輯們幫我們加了標點符號,所以看起來不至於那麼吃力。但我就特別討厭一些題目,讓加標點符號斷句的那些,很無聊。

所以,別任性,該加空格就加空格,你寫的代碼可不只是給你一個人看的,負責任點。

換句話說,我也可以寫文章不換行,對吧?寫一行兩行文字真沒必要換行。但段落多了,還不換行,我保證你看不下去。

不過,需要注意一點的是,我看有些視頻課里,有些講課人習慣什麼地方都加空格,我也是有點受不了的,比如說方法名和小括號之間——public void print (),這就有點不合時宜了。

按照規約來說,方法名和小括號之間是不需要空格的,因為沒有空格看起來更自然,更像一個方法,而不是割裂的關鍵字和小括號。

寫代碼不是鑽牛角尖,就像有些人喜歡在 if 語句里使用 == false 一樣——if (isFlase == false) {},他覺得這樣的代碼更具有可讀性,這不是出來搞笑的嗎?

最次,你應該寫成下面這樣子:

if (isTrue) {

} else {

}

這都比在 if 條件中使用 == false 來得讓人舒服。

做程序員嘛,總得有個程序員的樣子,想優雅點的話,把非“!”用上——if (!isFlase),搞那些無病呻吟的代碼,真的很掉檔次。

無規矩不成方圓,你如果不想遵守代碼規約的話,不想團隊協作的話,請隨意處置你的代碼,誰也拿你沒辦法。逃逃逃)

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

※網頁設計最專業,超強功能平台可客製化