從壹開始前後端分離 40 || 完美基於AOP的介面效能分析
旁白音:本文是不定時更新的.net core,當前主線任務的Nuxt+VueAdmin教程的 nuxt.js 之 tibug專案已上線,大家可以玩一玩: ofollow,noindex">http://123.206.33.109:7090 ,具體的部署教程會在下週發表。
緣起
哈嘍大家週五好呀,今天是一個不定時更新的文章,是很簡單的一篇文章,大家應該都能看懂,主要包含了兩個內容,一個是對AOP程式設計的進一步的理解(其中還有和過濾器比較),第二個就是一個簡單的小外掛——記錄介面的呼叫時間呼叫情況,也就是很簡單的效能記錄,這個時候你肯定不要和 Metricss+influxdb+grafana 作比較了,它們功能雖然很大,但是用起來笨重,咱們這種入門級別的小專案暫時先不用這個了,說到這裡,昨天有小夥伴留言,說要不要在專案中增加訊息佇列 ReditMQ ,我正在考慮中,看看如何使用,好啦廢話不多說,先來幾個小問題,熱熱身:
1、在平時開發的時候,大家是如何記錄當前 <介面/方法> 的呼叫時間的? // 手動寫起止時間相減
2、如何對異常進行處理的,這裡有 <api層的異常>,以及 <service 層的異常> 的? // 加 try catch
3、如何快速的找到當前介面的錯誤資訊? // 檢視日誌記錄
大家先帶著這幾個問題自己想一想,是不是和我的綠色解決方案一致,要是有更好的辦法也幫忙提給我,不勝感激!
不過!今天肯定不會用這些解決方案的,今天玩兒一個新花樣,應該也會有人玩兒過,彆著急,咱們往下看。
先來個實現圖,預熱下:
一、複習篇——我們把AOP用在了哪裡?
時間是很快,我也已經從第一個專題,寫到了第三個專題,還記得當時第一次寫AOP的時候《 框架之十 || AOP面向切面程式設計淺解析:簡單日誌記錄 + 服務切面快取 》,很多很多的小夥伴不是很明白,也不知道應用場景到底在哪裡,完全不瞭解落地幾何,現在也能在群裡,時不時看到有小夥伴用到AOP程式設計,感覺很開心,至少幫到了一些人了,這就是最大的欣慰!那咱們在Blog.core 專案中,到底如何運用了 AOP 呢?
1、切面快取
這一塊相信已經有小夥伴知道,並且用到了,我也是在很多地方使用到了,比如在基於策略的許可權認證文章中《 JWT完美實現許可權與介面的動態分配 》,通過了對角色模組的切面快取,很好的實現了快速授權的目的,不僅程式碼整潔,而且功能也實現了:
// 將最新的角色和介面列表更新 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的快取你要看懂了,先在腦子裡回顧下整體流程。
2、切面日誌
上邊咱們說了快取,我個人感覺還有一個很大的使用者就是切面日誌,這個具體的內容以前已經說過了,這裡就不多說了,想了解原理和詳細說明,請看《 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 的區別了:
1、Filter過濾器是基於當前Http請求的,也就是介面層面的,顆粒度比較大; 2、而AOP是基於服務切面的,是 Service 層的請求,顆粒度比較小;
那既然AOP能記錄呼叫日誌,能捕獲異常麼,上次群裡有一個小夥伴問到了,這裡就點名表揚了,挺棒的,能自己思考,那如何捕獲呢?
3、切面異常
在平時的開發中,我們經常會遇到各種 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() 這裡報異常,直接崩潰出去,你感覺這樣的設計合理麼?我們不能因為一個不重要的動作就不說核心的 我愛你 了吧:joy:,所以我們一般就會使用 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,如何找到你心儀的那一個呢,這樣,文章開頭的第三問就出來了,這裡先不說,先說下第一個問題。請往下看。
二、介面執行時間分析——MiniProfiler
1、平時我們的是如何測介面時間的
A: 我們平時在開發介面的時候,總想看看一個介面到底執行了多久,甚至想看看某一個service 方法執行了多長時間,這個時候,我以前都是通過 斷點除錯的方法,來看執行時間,比如這樣:
(這裡說下我用的 Sqlsugar 的批量新增,100條,0.4s,速度還行,給凱旋兄打給廣告:grinning:)
B: 或者直接就是寫個起止時間方法,來判斷下,比如這樣(圖片來自群管理DX):
但是我們有那麼多的介面,那麼多的方法,總不能都這麼 Debug 除錯吧,也不能都寫個方法吧,
C: 我們可以使用AOP來進行每次方法的時間記錄
就算是用上邊的 AOP 寫了一個,還是得檢視日誌不是,而且也只能是service層的,那api層還得寫過濾器了,這個時候,有一個外掛可以幫我們省去檢視日誌的詬病,就是它——MiniProfiler,這個具體的功能呢,我就不說了,大家自行百度即可,就是一個小外掛,我這裡就說下如何使用:
2、如何使用 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 中使用。
3、在 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了,最後就是如何使用了:
4、在介面中使用 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 日誌記錄又派上用場了!
5、切面 MiniFilter 分析
很簡單,只需要在 AOP 日誌記錄中,配置 MiniProfiler 即可:
try { // 就是這裡!! MiniProfiler.Current.Step($"執行Service方法:{invocation.Method.Name}() -> "); invocation.Proceed(); } catch (Exception e) { //執行的 service 中,捕獲異常 dataIntercept += ($"方法執行中出現異常:{e.Message + e.InnerException}"); }
這樣就在任何的介面中,都能看到這個呼叫資訊了!
是不是現在更加體會到了 AOP 的好處!
就這樣,咱們文章開頭的第一個問題就這樣解決啦!目前是第一、第二兩個問題都解決了,就剩下第三個問題了,如果檢視錯誤日誌呢,目前我們還是在日誌裡檢視的,別慌,雖然 MiniProfiler沒有這個功能,但是我自己奇思妙想了一個,請往下看。
三、結合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)); }
這個時候,我們檢視下效果,還記得上邊咱們說到的那個栗子麼,就是為了一個不重要的方法,而不能說出核心功能 “我愛你” 的那個栗子,咱們現在再看看效果:
是不是很方便!不僅能查看出所有的介面呼叫時間記錄,還能檢視錯誤資訊!這對於我們平時開發還是很有幫助的!
文章開頭的第三個問題也完美解決!
四、結語
好啦,今天的講解就到這裡了,主要說了分析了AOP切面程式設計的大作用,然後講解了 MiniProfiler 外掛的使用,同時對異常的處理,極大的幫助開發者定位錯誤已經資訊處理。
五、Github & Gitee
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core
-- ♥ -- ♥ -- ♥ -- ♥ -- ♥ -- ♥ --