系列目錄html
按部就班學.Net Core Web Api開發系列目錄前端
本系列涉及到的源碼下載地址:https://github.com/seabluescn/Blog_WebApigit
1、概述github
本篇介紹異常處理的知識。因爲異常處理的技術應用並不複雜,本篇更多討論異常處理的一些理論知識,包括一些原則、約定和建議。數據庫
2、異常處理的基本原則編程
在Win32API編程中是沒有異常處理機制的,函數通常都是經過返回一個BOOL型的狀態碼來表達處理是否成功,好比須要經過ID取得一個實體信息,須要這樣定義:json
BOOL GetArticleByID(string ID,out Article article);服務器
當調用失敗時(函數返回false),其實調用者是不知道失敗的緣由的,若是須要知道緣由,那就要返回一個int類型來表達狀態,-1表示成功,其餘都是錯誤碼,這種函數對調用者而言簡直是噩夢。app
.NET Framework中採用異常處理機制後,狀況就好多了,上面的方法定義以下:async
Article GetArticleByID(string ID);
看到這樣的定義,基本上不要看文檔也能明白這個方法的含義,另外全部可能失敗的狀況都經過異常來進行報告。
因此,對於調用者而言,全部與指望不符的結果均可以認爲是「異常」。
對於異常的處理,有幾個基本原則:
一、只處理(catch)預計可能會發生的異常
在代碼中,咱們只處理咱們預計可能會發生的異常,好比要把一個字符串轉換爲數字,咱們預計可能會發生FormatException異常,那麼咱們就Catch該異常,並提供處理辦法。
這裏的異常應該是咱們有能力處理的,其實每一行代碼咱們都預計可能發生OutMemoryException的異常 ,但這個異常發生時,應用是沒有能力處理的,請不要catch它。
二、絕對不要catch根異常Exception
這個原則和上面的原則實際上是很相似的,catch了根異常表示你有能力處理全部未知異常,並且以同一種方式來處理,顯然是不合適的。
因爲考慮不周,我沒有考慮到某個異常,又不容許我catch根異常,實際運行時應該果真報了一個以前沒有預料的異常怎麼辦?很簡單,把這個異常加上就能夠了。發生這種事情是由於編程者的經驗不足形成的,不能由於這個緣由破壞異常處理的原則。
三、若是方法還有調用者,應該對異常進行封裝
若是咱們是寫類庫相關的代碼,主要是提供服務給消費者調用的,最好對捕捉到的異常進行封裝,給出和調用者從新約定的異常類型。好比咱們在DAO層把全部捕捉到的異常處理完成後從新拋出一個DBOperateException,並提供相關信息。Control層在調用DAO時相對就簡單了,只需處理DBOperateException並把信息(或處理過的信息)報告給View就能夠了。
下面咱們會以一些實例描述咱們是如何遵照和打破這些原則的。
3、在WebApi開發中的異常處理
咱們要設計一個Controller,實現經過ID來獲取實例對象的功能,因爲異常沒法經過Http協議進行傳送,因此咱們定義了一個ResultObject的返回類型,用於向客戶端傳送調用結果。
public class ResultObject { public ResultObject() { state = ResultState.Success; ExceptionString = ""; result = null; } public ResultState state { get; set; } public String ExceptionString { get; set; } public Object result { get; set; } } public enum ResultState { Success, Exception }
具體的Controller設計以下:
public ResultObject GetArticleByID(string id) { try { int idn = int.Parse(id); Article article = _context.Articles .AsNoTracking() .Where(a => a.ID == id) .Single(); return new ResultObject { result = article }; } catch (System.FormatException ex) { _logger.LogError(ex.Message + "\n" + ex.StackTrace); return new ResultObject { state = ResultState.Exception, ExceptionString = "id必須爲數字" }; } catch (System.InvalidOperationException ex) { _logger.LogError(ex.Message + "\n" + ex.StackTrace); return new ResultObject { state = ResultState.Exception, ExceptionString = "未查詢到預料的數據" }; } catch(MySql.Data.MySqlClient.MySqlException ex) { _logger.LogError(ex.Message + "\n" + ex.StackTrace); return new ResultObject { state = ResultState.Exception, ExceptionString = "數據庫異常" }; } }
對於上述代碼,咱們預料到可能用戶會輸入字符串而不是數字,也能預料到可能查詢不到結果,因此就截獲了這兩個異常。對於ToList這樣的操做,沒有查詢到數據會返回NULL,不會報異常,因此就不該該catch InvalidOperationException。另外,咱們可能預料到會發生沒法鏈接數據庫的異常,在此也處理了,因爲數據庫鏈接異常可能在每一個方法調用時均可能發生。建議提供爲統一異常處理。
4、全局未處理異常
設計一個全局異常處理的中間件:
public class UnifyExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public UnifyExceptionMiddleware(RequestDelegate next, ILogger<UnifyExceptionMiddleware> logger) { _next = next; _logger=logger; } public async Task Invoke(HttpContext context) { ResultObject result =null; try { await _next(context); } catch(MySql.Data.MySqlClient.MySqlException ex) { _logger.LogError(ex.Message + "\n" + ex.StackTrace); result = new ResultObject { state = ResultState.Exception, ExceptionString = "數據庫異常" }; } catch(Exception ex) { _logger.LogError($"系統發生未處理異常:{ex.StackTrace}"); result = new ResultObject { state = ResultState.Exception, ExceptionString = "系統發生未處理異常" }; } context.Response.StatusCode = 200; context.Response.ContentType = "application/json; charset=utf-8"; context.Response.WriteAsync(JsonConvert.SerializeObject(result)); } } public static class VisitLogMiddlewareExtensions { public static IApplicationBuilder UseUnifyException(this IApplicationBuilder builder) { return builder.UseMiddleware<UnifyExceptionMiddleware>(); } }
使用該中間件
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddNLog(); app.UseUnifyException(); app.UseMvcWithDefaultRoute(); } }
異常處理的中間件要放在MVC中間件以前,這樣就能夠截獲Contriller內的未處理異常。
5、兩點思考
一、爲何咱們處理了根異常Exception
前面提到不要處理根異常,但這裏卻處理了,這是什麼狀況?咱們說不要處理根異常,是由於不但願某個方法掩蓋了問題,向上級報告一個虛假的狀態,但對於全部處理流程的最上級,能夠適當違反該原則。
就應用程序而言,當發生未處理異常時,操做系統會接管該異常的處理,這是微軟推薦的作法,但咱們仍是經常會進行全局未處理異常的處理,彈出一個用戶看得懂的提示框,並登記一個異常報告。
對於WebApi而言,接口並不直接面對用戶,但因爲異常機制沒法經過Http協議進行傳輸,接口的調用者就是WebApi的最終用戶了,全部能夠對根異常進行捕獲。
這裏有兩種選擇:
1)不捕獲根異常,出現未處理異常時,向調用者報500;
2)捕獲根異常,出現未處理異常時,向調用者報200,同時報告異常內容。
具體如何選擇,就不是一個技術問題了,主要看團隊的管理規定與約定。某些公司規定接口是不容許報500的,不然是要扣績效的,那隻能捕獲根異常了,畢竟績效最重要對吧。
二、異常發生時,應該報告給客戶端什麼樣的狀態碼?
咱們和前端約定使用ResultObject來返回調用狀態和結果,對於發生「異常」時應該返回什麼樣的狀態碼比較合適呢,這大體也有兩種選擇:
1)一概返回200,經過ResultObject報告接口,字段不夠能夠增長信息字段;
2)經過狀態碼返回一些特殊的異常,好比:找不到資源返回404,認證失敗返回401等,未知異常報500等等。
對於WebApi而言推薦使用第一種模式。
附:Http Response 返回碼
HTTP協議狀態碼錶示的意思主要分爲五類,大致是:
1×× |
保留 |
2×× |
表示請求成功地接收 |
3×× |
爲完成請求客戶需進一步細化請求 |
4×× |
客戶錯誤 |
5×× |
服務器錯誤 |
列舉一些常見的狀態碼:
200 OK 指示客服端的請求已經成功收到,解析,接受。
401 Unauthorized 若是請求須要用戶驗證。回送應該包含一個WWW-Authenticate頭字段用來指明請求資源的權限。
403 Forbidden 服務器接受請求,可是被拒絕處理。
404 Not Found 服務器已經找到任何匹配Request-URI的資源。
500 Internal Server Error 服務器遭遇異常阻止了當前請求的執行。
502 Bad Gateway 無效網關。
503 Service Unavailable 由於臨時文件超載致使服務器不能處理當前請求。