從壹開始先後端分離 40 || 完美基於AOP的接口性能分析

旁白音:本文是不定時更新的.net core,當前主線任務的Nuxt+VueAdmin教程的 nuxt.js 之 tibug項目已上線,你們能夠玩一玩:http://123.206.33.109:7090,具體的部署教程會在下週發表。css

緣起

哈嘍你們週五好呀,今天是一個不定時更新的文章,是很簡單的一篇文章,你們應該都能看懂,雖然很簡單,可是我感受很實用,主要包含了兩個內容,一個是對AOP編程的進一步的理解(其中還有和過濾器比較),第二個就是一個簡單的小插件——記錄接口的調用時間調用狀況,也就是很簡單的性能記錄,這個時候你確定不要和 Metricss+influxdb+grafana 做比較了,它們功能雖然很大,可是用起來笨重,我們這種入門級別的小項目暫時先不用這個了,說到這裏,昨天有小夥伴留言,說要不要在項目中增長消息隊列 ReditMQ ,我正在考慮中,看看如何使用,好啦廢話很少說,先來幾個小問題,熱熱身:html

一、在平時開發的時候,你們是如何記錄當前 <接口/方法> 的調用時間的?// 手動寫起止時間相減 git

二、如何對異常進行處理的,這裏有 <api層的異常>,以及 <service 層的異常> 的?// 加 try catch  github

三、如何快速的找到當前接口的錯誤信息?// 查看日誌記錄redis

你們先帶着這幾個問題本身想想,是否是和個人綠色解決方案一致,要是有更好的辦法也幫忙提給我,不勝感激!sql

不過!今天確定不會用這些解決方案的,今天玩兒一個新花樣,應該也會有人玩兒過,彆着急,我們往下看。數據庫

先來個實現圖,預熱下,之後調試接口,性能+錯誤信息一目瞭然:編程

 

1、複習篇——咱們把AOP用在了哪裏?

時間是很快,我也已經從第一個專題,寫到了第三個專題,還記得當時第一次寫AOP的時候《框架之十 || AOP面向切面編程淺解析:簡單日誌記錄 + 服務切面緩存》,不少不少的小夥伴不是很明白,也不知道應用場景到底在哪裏,徹底不瞭解落地幾何,如今也能在羣裏,時不時看到有小夥伴用到AOP編程,感受很開心,至少幫到了一些人了,這就是最大的欣慰!那我們在Blog.core 項目中,到底如何運用了 AOP 呢?json

 

一、切面緩存

這一塊相信已經有小夥伴知道,而且用到了,我也是在不少地方使用到了,好比在基於策略的權限認證文章中《JWT完美實現權限與接口的動態分配》,經過了對角色模塊的切面緩存,很好的實現了快速受權的目的,不只代碼整潔,並且功能也實現了:api

 // 將最新的角色和接口列表更新
 var data = await _roleModulePermissionServices.GetRoleModule();
 var list = (from item in data
             where item.IsDeleted == false
             orderby item.Id
             select new PermissionItem
             {
                 Url = item.Module?.LinkUrl,
                 Role = item.Role?.Name,
             }).ToList();

// 經過AOP緩存獲取角色模塊信息 [Caching(AbsoluteExpiration
= 10)] public async Task<List<RoleModulePermission>> GetRoleModule() { var roleModulePermissions = await dal.Query(a => a.IsDeleted == false); //....... return roleModulePermissions; }

 

另外還有在當前第三系列教程中,獲取Bug信息的時候,也用到了切面緩存(因此,若是你用了文章開頭的 tibug 系統,提交了一個 bug,可是沒有馬上顯示出來,就是這個緣由,10分鐘緩存):

綜上所言,通過屢次使用,我我的表示真的是一個神器,在徹底不露痕跡的狀況下,實現了緩存,是否是很好用?

這個時候你會問,單獨爲了緩存的話,AOP不是很透徹,那還有其餘的用處麼?請往下看。前提是上邊的這種基於AOP的緩存你要看懂了,先在腦子裏回顧下總體流程。

 

二、切面日誌

 上邊我們說了緩存,我我的感受還有一個很大的用戶就是切面日誌,這個具體的內容之前已經說過了,這裏就很少說了,想了解原理和詳細說明,請看《AOP面向切面編程淺解析:簡單日誌記錄 + 服務切面緩存》,這裏只是複習下流程:

        public void Intercept(IInvocation invocation)
        {
            //記錄被攔截方法信息的日誌信息
            var dataIntercept = $"{DateTime.Now} " +
                $"當前執行方法:{ invocation.Method.Name} ";

            //在被攔截的方法執行完畢後 繼續執行當前方法,注意是被攔截的是異步的
            invocation.Proceed();

            dataIntercept += ($"方法執行完畢,返回結果:{invocation.ReturnValue}");

            #region 輸出到日誌文件     
            #endregion
        }

只須要咱們 ServiceConfigure 中開啓服務之後,就能夠在指定文件中,看到每次的切面接口調用狀況,注意這裏是 service 層的,不是 controller 的日誌,這個時候就是 AOP 和過濾器 Filter 的區別了:

一、Filter過濾器是基於當前Http請求的,也就是接口層面的,顆粒度比較大;

二、而AOP是基於服務切面的,是 Service 層的請求,顆粒度比較小;

那既然AOP能記錄調用日誌,能捕獲異常麼,上次羣裏有一個小夥伴問到了,這裏就點名表揚了,挺棒的,能本身思考,那如何捕獲呢? 

 

三、切面異常 

在平時的開發中,咱們常常會遇到各類 Bug ,在 controller 裏的錯誤就不說了,編譯的時候基本都能看出來,可是不少 service 層的錯誤,真是難找,好比我故意寫的這個bug,一個不重要的方法:

  public void NoImportantMethod()
  {
      int a = 1;
      int b = 0;
      int c = a / b;
  }

這個錯誤是如何捕獲的,你們還記得麼,就是咱們用全局變量異常過濾器 Filter 捕獲的《三十五║ 完美實現全局異常日誌記錄》:

 

咱們平時可能會在 api 中使用這樣的service層方法,而後下邊也會有其餘的一些方法,由於這個方法不重要,好比僅僅是閱讀數量+1,那咱們就不能讓當前接口崩了

 public async Task<MessageModel<Response>> Get()
 {
     var data = new MessageModel<Response>();
     
     // 一個不重要的方法
     _advertisementServices.NoImportantMethod();
    
     // 核心功能:說愛你
     Love love = New Love();
     love.SayLoveU();

     return data;
 }

這個咱們咱們會在 NoImportantMethod() 這裏報異常,直接崩潰出去,你感受這樣的設計合理麼?咱們不能由於一個不重要的動做就不說核心的 我愛你 了吧😂,因此咱們通常就會使用 try catch 的方法,把咱們認爲會出錯的地方包括住,好比這樣:

 public async Task<MessageModel<Response>> Get()
 {
    var data = new MessageModel<Response>();
     
    try{
     // 一個不重要的方法
     _advertisementServices.NoImportantMethod();
    }catch{}

     // 核心功能:說愛你
     Love love = New Love();
     love.SayLoveU();

     return data;
 }

 

這樣雖然問題解決了,可是總感受很難受,太醜了,會致使咱們全部的接口都寫滿了這個 try try try,哦no!那咋辦麼,別慌,還記得咱們的AOP記錄日誌麼,既然能記錄日誌,照樣能記錄異常!

沒錯就是這裏:

 try
 {
     invocation.Proceed();
 }
 catch (Exception e)
 {//執行的 service 中,捕獲異常
     dataIntercept += ($"方法執行中出現異常:{e.Message + e.InnerException}");
 }

咱們看看效果:

 

而後很順利的捕獲到了異常,而且主程序也走了下去,咱們就能夠找到咱們的日誌文件,而後看看錯誤:

20190118 11:40:50 當前執行方法:ReturnExp 參數是:  
方法執行中出現異常:Attempted to divide by zero.方法執行完畢,返回結果:

 

到這裏!咱們的文章開頭提問的第二個方法(如何對異常進行處理的)已經順利解決!我如今在公司的項目中就是用的這種切面方法,來進行處理的,把那些不重要的方法給走下去,雖然可能會出現,提交了訂單,可是一直提交不上的問題,可是至少保證不會讓頁面崩了的尷尬狀況。

到了這裏不知道你是否滿意了呢,別慌,剛剛咱們是如何來查看錯誤的?是經過找日誌是吧,而後還須要對應的時間呀,接口方法呀啥的,若是日誌信息不少,那茫茫bug,如何找到你心儀的那一個呢,這樣,文章開頭的第三問就出來了,這裏先不說第三個,先說下第一個問題(如何記錄當前 <接口/方法> 的調用時間)。請往下看。

 

2、接口執行時間分析——MiniProfiler

一、平時咱們的是如何測接口時間的

 A:咱們平時在開發接口的時候,總想看看一個接口到底執行了多久,甚至想看看某一個service 方法執行了多長時間,這個時候,我之前都是經過 斷點調試的方法,來看執行時間,好比這樣:

(這裏說下我用的 Sqlsugar 的批量添加,100條,0.4s,速度還行,由於我中間還有一個if操做,因此時間應該在0.3左右,給凱旋兄打給廣告😀)

 

B:或者直接就是寫個起止時間方法,來判斷下,好比這樣(圖片來自羣管理DX):

可是咱們有那麼多的接口,那麼多的方法,總不能都這麼 Debug 調試吧,也不能都寫個方法吧,

 

C:咱們可使用AOP來進行每次方法的時間記錄

就算是用上邊的 AOP 寫了一個,仍是得查看日誌不是,並且也只能是service層的,那api層還得寫過濾器了,這個時候,有一個插件能夠幫咱們省去查看日誌的詬病,就是它——MiniProfiler,這個具體的功能呢,我就不說了,你們自行百度便可,就是一個小插件,我這裏就說下如何使用:

 

二、如何使用 MiniProfiler

使用方法很簡單,首先咱們須要引入nuget包:

 Install-Package MiniProfiler.AspNetCore.Mvc

 

而後,在startup.cs 中配置服務ConfigureServices:

  services.AddMiniProfiler(options =>
      {
          options.RouteBasePath = "/profiler";//注意這個路徑要和下邊 index.html 腳本配置中的一致,
          (options.Storage as MemoryCacheStorage).CacheDuration = TimeSpan.FromMinutes(10);

      }

最後,調用下中間件便可:

  app.UseMiniProfiler();

當前這個時候還不能使用,咱們還須要在 Swagger 中進行配置,以便它能在 swagger 中使用。

 

三、在 Swagger 中配置 MiniProfiler

 上邊咱們在配置中已經啓動了服務,接下來就須要設置如何在 swagger 中展現了,這個時候咱們就須要自定義咱們的swagger主頁了,之前咱們是用的默認的index.html,如今我們須要自定義一個:

從官網 Github 上,下載最新的 index.html:https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html(官方的應該是這個,和個人有一丟丟不同,應該也能夠用)

而後添加到項目中,我是放到了根路徑了:

這個時候記得要把這個文件設置成嵌入資源的類型:

接下來,在 Index.html 文件中,增長配置腳本(我是在頂部寫的,Head應該也能夠,其餘的沒實驗):

<script async="async" id="mini-profiler" src="/profiler/includes.min.js?v=4.0.138+gcc91adf599"
        data-version="4.0.138+gcc91adf599" data-path="/profiler/"
        data-current-id="4ec7c742-49d4-4eaf-8281-3c1e0efa748a" data-ids="" data-position="Left"
        data-authorized="true" data-max-traces="15" data-toggle-shortcut="Alt+P"
        data-trivial-milliseconds="2.0" data-ignored-duplicate-execute-types="Open,OpenAsync,Close,CloseAsync">
</script>

<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
........
........

具體的參數請自行研究吧,基本都能看懂,好比版本,/profiler的路徑,position的位置顯示(我是左邊),authorized的是否權限,max-traces最多顯示多少條(15)等等。 

 

而後咱們修改下中間件去調用咱們這個 index.html 頁面:

app.UseSwaggerUI(c =>
{
    typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version =>
    {
        c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{ApiName} {version}");
    });
// 將swagger首頁,設置成咱們自定義的頁面,記得這個字符串的寫法:解決方案名.index.html c.IndexStream
= () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("Blog.Core.index.html"); });

 

最後,記得必定要配置了使用靜態資源文件的中間件:

 app.UseStaticFiles();

好啦,如今已經配置好了 MiniProfiler 和 Swagger了,最後就是如何使用了:

 

四、在接口中使用 MiniProfiler

這裏我在BlogController 的Get方法中,使用:

       public async Task<object> Get(int id, int page = 1, string bcategory = "技術博文")
        {
......
// 你能夠用這種包括的形式 using (MiniProfiler.Current.Step("開始加載數據:")) { if (redisCacheManager.Get<object>("Redis.Blog") != null) { // 也能夠直接這麼寫 MiniProfiler.Current.Step("從Redis服務器中加載數據:"); blogArticleList = redisCacheManager.Get<List<BlogArticle>>("Redis.Blog"); } else { MiniProfiler.Current.Step("從MSSQL服務器中加載數據:"); blogArticleList = await blogArticleServices.Query(a => a.bcategory == bcategory); redisCacheManager.Set("Redis.Blog", blogArticleList, TimeSpan.FromHours(2)); } } ...... }

最後的效果就是這樣的(這裏說明下:我有時候在F5啓動項目,而後第一次點擊某一個接口的時候,當前接口分析報告會沒有,再點擊這個接口,報告就行了,狀況之後也不會出現了,若是你也遇到了不要慌,屬於正常問題):

 

是否是感受挺好的!這樣的時間都有了,而後還記得上邊配置的 /profiler 麼,咱們點擊 share 就能看到了,這裏不細說了,你們本身玩一玩。如今有一個問題就是,我總不能每個 api 接口都這麼寫吧,多麻煩呀!機智如你,這個時候 AOP 日誌記錄又派上用場了!

五、切面 MiniFilter 分析

很簡單,只須要在 AOP 日誌記錄中,配置 MiniProfiler 便可:

 try
 {
     // 就是這裏!!
     MiniProfiler.Current.Step($"執行Service方法:{invocation.Method.Name}() -> ");
 
     invocation.Proceed();
 }
 catch (Exception e)
 {
     //執行的 service 中,捕獲異常
     dataIntercept += ($"方法執行中出現異常:{e.Message + e.InnerException}");
 }

這樣就在任何的接口中,都能看到這個調用信息了!

 

是否是如今更加體會到了 AOP  的好處!

就這樣,我們文章開頭的第一個問題就這樣解決啦!目前是第1、第二兩個問題都解決了,就剩下第三個問題了,若是查看錯誤日誌呢,目前咱們仍是在日誌裏查看的,別慌,雖然 MiniProfiler沒有這個功能,可是我本身奇思妙想了一個,請往下看。

 

六、在Swagger中展現 Sql 執行日誌 

操做很簡單,功能頗有效,動圖以下,具體的看個人在線地址 http://123.206.33.109:8081

 

若是想要開啓這個 SQL AOP的功能,請參考個人源代碼,主要是經過 SqlSugar 的 相關功能來實現:

Blog.Core.Repository -> DbContext.cs ->  _db.Aop.OnLogExecuting  

 _db.Aop.OnLogExecuting = (sql, pars) => //SQL執行中事件
 {
     Parallel.For(0, 1, e =>
     {
         MiniProfiler.Current.CustomTiming("SQL:", GetParas(pars) + "【SQL語句】:" + sql);
         LogLock.OutSql2Log("SqlLog", new string[] { GetParas(pars), "【SQL語句】:" + sql });

     });
 };

 

 

3、結合AOP+MiniProfiler實現異常可視化

剛剛上邊我們也說到了,可使用 AOP 進行異常的捕獲記錄,只不過須要在 log 日誌文件中,查看。而 miniprofiler 的功能居然正好是時間的分析展現,那咱們可不能夠融合下二者呢!

很簡單!咱們只須要在 AOP 的切面異常catch中,還有 全局Filter 異常中,將錯誤信息融入到 MiniProfiler便可:

 

// 這個是AOP中
 try
 {
     MiniProfiler.Current.Step($"執行Service方法:{invocation.Method.Name}() -> ");
     invocation.Proceed();
 }
 catch (Exception e)
 {
     //執行的 service 中,收錄異常
     MiniProfiler.Current.CustomTiming("Errors:", e.Message);
     //執行的 service 中,捕獲異常
     dataIntercept += ($"方法執行中出現異常:{e.Message + e.InnerException}");
 }

 //這個是全局異常處理中
 public void OnException(ExceptionContext context)
 {
     //...................不重要內容...............

     MiniProfiler.Current.CustomTiming("Errors:", json.Message);

     //採用log4net 進行錯誤日誌記錄
     _loggerHelper.Error(json.Message, WriteLog(json.Message, context.Exception));

 }

 

這個時候,咱們查看下效果,還記得上邊我們說到的那個栗子麼,就是爲了一個不重要的方法,而不能說出核心功能 「我愛你」 的那個栗子,我們如今再看看效果:

 

是否是很方便!不只能查看出全部的接口調用時間記錄,還能查看錯誤信息!這對於咱們平時開發仍是頗有幫助的!

文章開頭的第三個問題(如何快速的找到當前接口的錯誤信息)也完美解決!

 

4、結語

 好啦,今天的講解就到這裏了,主要說了分析了AOP切面編程的大做用,而後講解了 MiniProfiler 插件的使用,同時對異常的處理,極大的幫助開發者定位錯誤已經信息處理。

最後給你們說下,關於Miniprofilter 還有其餘的情景:

一、能夠針對 EF 對數據庫sql執行的性能進行監控(由於本項目是Sqlsugar,因此就不針對性展現了,想實驗的能夠自行測試)。

  • 首先修改Client solution, 添加 Dbcontext、Models、Repository
  • Nuget安裝packageMiniProfiler.EntityFrameworkCore
  • Startup類的方法ConfigureServices添加代碼:
services.AddMiniProfiler().AddEntityFramework(); 
  • 而後在 ControllerIndex 方法中添加對 repository 方法的調用

二、不須要必定在swagger中使用,其餘的也行,好比mvc,只不過我感受swagger更舒服。

 

5、Github & Gitee

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

-- ♥ -- ♥ -- ♥ -- ♥ -- ♥ -- ♥ --

相關文章
相關標籤/搜索