本文鏈接: https://www.cnblogs.com/hubaijia/p/about-exceptions-3.html
系列文章:
本文目錄
Web Api 的錯誤返回
在使用.net 的 Web Api構建Endpoint對外提供訪問時,往往需要統一的錯誤返回格式。
如果按照前面兩篇文章(一)、(二)所說,采用帶有ErrorCode的異常ErrorCodeException,那么在WebApi中返回錯誤時,只要返回ErrorCode即可。
代碼如下:
[HttpPut]
[Authorize]
public IActionResult Update(UpdatetNickNameRequest request)
{
try
{
long userId = User.GetUserId();
_identityService.SetNickName(userId, request.NickName);
return Ok();
}
catch (IdentityException ex)
{
//做相應處理
//... ...
return new BadRequestObjectResult(ex.ErrorCode);
}
catch (OtherException ex)
{
//做相應處理
//... ...
return new BadRequestObjectResult(ex.ErrorCode);
}
}
正如評論里,大家所說的,可以使用ExceptionFilter,或者中間件,來統一的捕捉各類異常。
也可以使用
UseExceptionHandler
擴展方法,將異常投遞到自定義的路徑上。
舉例來說,如果使用ExceptionFilter來統一處理的話,代碼如下:
public class ExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
ErrorCode errorCode = context.Exception is ErrorCodeException ex ? ex.ErrorCode : ErrorCode.Empty;
//其他操作,比如日志等,或者根據不同的ErrorCode做出不同的處理
context.Result = new BadRequestObjectResult(errorCode);
context.ExceptionHandled = true;
}
}
將ExceptionFilter
注冊到services
中后,可以在services.AddControllers()
方法里,全局添加Filter,避免每一個方法都手工添加。
代碼如下:
services.AddControllers(options =>
{
options.Filters.AddService<UserActivityFilter>();
})
總之,重點是在我們返回了ErrorCode。
ErrorCode類包含了Id, Name, Message屬性,方便客戶端在收到錯誤返回后進行處理。
自定義的客戶端在接受到Api的返回后,檢查HttpStatusCode,如果不成功(不是2xx),那么也直接拋出帶有ErrorCode的異常ErrorCodeException即可。
這樣,前后端的處理就變得一致和簡便。特別是如果你在前端也采用 .net 技術,比如 blazor、xamarin、wpf、winform等等,那前后端可以使用相同的CodeBase。
Exception的捕捉
前面幾篇文章,一直在關注Exception的拋出,現在我們來關注一下Exception的捕捉。
那些基礎的catch/finally這里就不再贅述,說幾個項目中實際出現的現象和問題。
不要吃掉異常
不要吃掉異常有兩個層面,一個是直接忽略,比如catch了所有的Exception,並不妥善處理(假處理,就打印一句log,甚至不處理),不過這種失誤要不是新手所犯,要不就是編碼過程中草草了事的結果。
這里主要注意第二個層面,即catch時不要隱藏異常的類型,即不要catch(Exception ex),這意味着隱藏了異常的類型,吃掉了針對不同的錯誤采取不同的措施的機會。
微軟的code analysis也直接給出了規則 CA1031:不要捕捉一般異常類型.
Checked Exception
CE是確保你所有的異常都得到捕捉,它往往在編譯層面提供對Exception的檢查,確保你的程序不會因為未處理的異常而終止。
在編寫可靠的程序時,程序員需要知道調用的方法拋出什么異常,我是否需要處理,還是包裝,還是直接拋出不理,總之我們需要有途徑獲取這樣的信息。
當然,Checked Exception肯定會帶來更多的代碼量,且在項目初始建立起來,往往需要修改一處,而動整個鏈條上的代碼,一層層的修改。
所以,有些人喜歡CE,認為它帶來了規矩,改善了團隊代碼質量;當然也有些人認為它帶來了繁瑣。
仁者見仁智者見智,這里不加入這些爭辯。爭辯1, 爭辯2, 爭辯3, 爭辯4
.net vs java
不得不拿這兩門語言進行對比,每個世界都要互相借鑒。
熟悉java語法的同學肯定會知道java方法定義上有throws這一關鍵詞:
public int div(int i,int j) throws Exception {...}
throws這一關鍵詞保證了從方法定義上就能知道一個方法拋出什么樣的異常,直接借助編譯器檢查或者IDE的智能提醒,就不會漏掉異常。
而c#中並沒有相同的實現,可以見Stackoverflow上的討論.
那么在 .net 世界中當你調用一個方法時,怎么妥善的知道這個方法拋出什么樣的異常呢?知道后我們才能決定是否處理這個異常,還是繼續拋出。
目前的答案是:注釋!(不要笑,很嚴肅的解決方案)。
/// <exception cref="IdentityService.IdentityService">這樣寫Exception注釋</exception>
public void SetNickName(long userId, string newNickName)
{
//....
Ensure.NickNameNotExisted(newNickName);
//....
}
當你翻看.net的源碼時,會看到所有方法的注釋中都良好的列出了有哪些異常。
有幾個問題:
- 我們團隊沒人寫注釋,怎么辦?
- 調用方法時,並沒有智能提示有哪些異常,所以我們經常忽略
- 想看異常就得F12看定義,太繁瑣。
有很多問題只從技術上沒法解決,但我們盡量可以借助一些Review工具來檢查團隊的代碼,提出要求。
此外你會喜歡上 ctrl + k, ctrl+i
這個快捷鍵的,他能幫助你快速查看注釋文檔,查看有哪些異常。
visual studio 擴展推薦
在這里,我推薦一個visual studio的擴展,是的,它的名字就叫 Checked Exceptions, 這是我必備的一款擴展。
這款插件會借助注釋的形式,協助實現Checked Exception的功能,並且可以快速添加相應注釋。
這個有個小提示:如果你從項目伊始采用這款插件,折磨小一點,如果半路使用,那么當作檢驗團隊代碼強健性的工具也不錯。
此外,這個款擴展可能還有些bug,導致即使注釋了Exception還不斷提示,所以我平時並不一直啟用它,而是在做Code Review時,使用它作為一個檢查工具。
這樣可以比較好的解決上面提到的Checked Exception的缺點,又利用它的優點。
常見模式
不要重復的拋出
這個只是簡單提醒下,見如下代碼。
void BadSmellMethod()
{
try
{
.....
}
catch(Exception ex)
{
//.... some thing
// 錯誤的做法
throw ex;
}
}
void GoodMethod()
{
try
{
.....
}
catch
{
//... some thing
// 正確的做法
throw;
}
}
簡單來說就是重復拋出,丟失了引發異常原始方法和當前方法調用之間的StackTrace。
在code analysis中也有相應規則。CA2200.
在asp.net core 中
在捕捉異常時,往往一個異常被一路拋出,或者包裝再拋出,直到終點。如果到了終點還沒有被捕獲,那么就會引發程序中止,這是誰都不想看到的。
在asp.net core中,這個終點就是Controller控制器,所以我們需要在Controller的方法里調用需要的Service,處理那些需要特殊處理的異常,然后使用全局錯誤處理(ExceptionFilter、中間件等)兜住其他的異常。
在xamarin.forms中
如果你是同道中人,使用xamarin.forms,那么你肯定知道MVVM模式。
異常的終點往往就在MVVM模式中的ViewModel中,比如LoginPageViewModel中,同樣ViewModel調用各項Service,你需要在這里處理當下場景里需要處理的異常,然后自定義全局的錯誤處理兜住其他異常。
結語
本文,簡要介紹了具體項目中異常的捕捉問題,歡迎大家交流指正。
下篇,我們關注一下 異步編程中的Exception,以及全局錯誤處理。
謝謝閱讀。