循序漸進學.Net Core Web Api開發系列【14】:異常處理


系列目錄

循序漸進學.Net Core Web Api開發系列目錄

 本系列涉及到的源碼下載地址:https://github.com/seabluescn/Blog_WebApi

 

一、概述

本篇介紹異常處理的知識。由於異常處理的技術應用並不復雜,本篇更多討論異常處理的一些理論知識,包括一些原則、約定和建議。

 

二、異常處理的基本原則

在Win32API編程中是沒有異常處理機制的,函數一般都是通過返回一個BOOL型的狀態碼來表達處理是否成功,比如需要通過ID取得一個實體信息,需要這樣定義:

BOOL GetArticleByID(string ID,out Article article);

當調用失敗時(函數返回false),其實調用者是不知道失敗的原因的,如果需要知道原因,那就要返回一個int類型來表達狀態,-1表示成功,其他都是錯誤碼,這種函數對調用者而言簡直是噩夢。

.NET Framework中采用異常處理機制后,情況就好多了,上面的方法定義如下:

Article GetArticleByID(string ID);

看到這樣的定義,基本上不要看文檔也能明白這個方法的含義,另外所有可能失敗的情況都通過異常來進行報告。

所以,對於調用者而言,所有與期望不符的結果都可以認為是“異常”。

 對於異常的處理,有幾個基本原則:

1、只處理(catch)預計可能會發生的異常 

      在代碼中,我們只處理我們預計可能會發生的異常,比如要把一個字符串轉換為數字,我們預計可能會發生FormatException異常,那么我們就Catch該異常,並提供處理辦法。

      這里的異常應該是我們有能力處理的,其實每一行代碼我們都預計可能發生OutMemoryException的異常 ,但這個異常發生時,應用是沒有能力處理的,請不要catch它。

2、絕對不要catch根異常Exception

      這個原則和上面的原則其實是很類似的,catch了根異常表示你有能力處理所有未知異常,而且以同一種方式來處理,顯然是不合適的。

     由於考慮不周,我沒有考慮到某個異常,又不允許我catch根異常,實際運行時應該果然報了一個之前沒有預料的異常怎么辦?很簡單,把這個異常加上就可以了。發生這種事情是因為編程者的經驗不足造成的,不能因為這個原因破壞異常處理的原則。

3、如果方法還有調用者,應該對異常進行封裝

      如果我們是寫類庫相關的代碼,主要是提供服務給消費者調用的,最好對捕捉到的異常進行封裝,給出和調用者重新約定的異常類型。比如我們在DAO層把所有捕捉到的異常處理完成后重新拋出一個DBOperateException,並提供相關信息。Control層在調用DAO時相對就簡單了,只需處理DBOperateException並把信息(或處理過的信息)報告給View就可以了。

下面我們會以一些實例描述我們是如何遵守和打破這些原則的。

 

三、在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。另外,我們可能預料到會發生無法連接數據庫的異常,在此也處理了,由於數據庫連接異常可能在每個方法調用時都可能發生。建議提供為統一異常處理。

 

四、全局未處理異常

設計一個全局異常處理的中間件:

 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內的未處理異常。

 

五、兩點思考

1、為什么我們處理了根異常Exception

前面提到不要處理根異常,但這里卻處理了,這是什么情況?我們說不要處理根異常,是因為不希望某個方法掩蓋了問題,向上級報告一個虛假的狀態,但對於所有處理流程的最上級,可以適當違反該原則。

就應用程序而言,當發生未處理異常時,操作系統會接管該異常的處理,這是微軟推薦的做法,但我們還是常常會進行全局未處理異常的處理,彈出一個用戶看得懂的提示框,並登記一個異常報告。

對於WebApi而言,接口並不直接面對用戶,但由於異常機制無法通過Http協議進行傳輸,接口的調用者就是WebApi的最終用戶了,所有可以對根異常進行捕獲。

這里有兩種選擇:

1)不捕獲根異常,出現未處理異常時,向調用者報500;

2)捕獲根異常,出現未處理異常時,向調用者報200,同時報告異常內容。

具體如何選擇,就不是一個技術問題了,主要看團隊的管理規定與約定。某些公司規定接口是不允許報500的,否則是要扣績效的,那只能捕獲根異常了,畢竟績效最重要對吧。

 

2、異常發生時,應該報告給客戶端什么樣的狀態碼?

 我們和前端約定使用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  因為臨時文件超載導致服務器不能處理當前請求。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM