一、使用異常篩選器捕獲所有異常
我們知道,一般情況下,WebApi作為服務使用,每次客戶端發送http請求到我們的WebApi服務里面,服務端得到結果輸出response到客戶端。這個過程中,一旦服務端發生異常,會統一向客戶端返回500的錯誤。這種錯誤是服務器自動做出的反映,對於后期維護人員很難定位排錯。
例如:下面代碼
using System.Web.Http;
namespace WebApplication1.Controllers
{
public class HomeController : ApiController
{
[Route("api/home")]
public IHttpActionResult Get()
{
var a = 1;
var b = 0;
return Json(a/b);
}
}
}
該代碼可以很容易看出來,是錯誤的。0不能為除數!我們看下服務器返回的結果
在這個請求報文的返回信息中我們並不能確定服務器到底是哪里出現了問題,而有些時候,我們客戶端需要得到更加精確的錯誤碼來判斷異常類型,怎么辦呢?
雖然有些時候頁面的錯誤返回信息會給我們一些提示,但是作為一個合格程序員,這些錯誤的信息是不能返回給用戶看到。這個是需要我們進行后台處理后給他一個合理的返回值。
學習過mvc的同學們肯定知道過濾器這個東東啦,MVC里面的IExceptionFilter接口,這個接口用於定義異常篩選器所需的方法,在WebApi里面,也有這么一個異常篩選器,下面我們通過一個實例來看看具體如何實現。
首先在App_Start里面新建一個類CustomHandleErrorAttribute.cs,繼承ExceptionFilterAttribute,重寫OnException方法
代碼如下:
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Filters;
namespace WebApplication1.App_Start
{
public class CustomHandleErrorAttribute : ExceptionFilterAttribute
{
//重寫基類的異常處理方法
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
//1.異常日志記錄(正式項目里面一般是用log4net記錄異常日志)
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "——" +
actionExecutedContext.Exception.GetType().ToString() + ":" + actionExecutedContext.Exception.Message + "——堆棧信息:" +
actionExecutedContext.Exception.StackTrace);
//2.返回調用方具體的異常信息
if (actionExecutedContext.Exception is NotImplementedException)
{
actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
}
else if (actionExecutedContext.Exception is TimeoutException)
{
actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.RequestTimeout);
}
//.....這里可以根據項目需要返回到客戶端特定的狀態碼。如果找不到相應的異常,統一返回服務端錯誤500
else
{
actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
base.OnException(actionExecutedContext);
}
}
}
代碼解析:通過判斷異常的具體類型,向客戶端返回不同的http狀態碼,示例里面寫了兩個,可以根據項目的實際情況加一些特定的我們想要捕獲的異常,然后將對應的狀態碼寫入http請求的response里面,對於一些我們無法判斷類型的異常,統一返回服務端錯誤500。關於http的狀態碼,framework里面定義了一些常見的類型,我們大概看看:
#region 程序集 System.dll, v4.0.0.0
// C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.dll
#endregion
using System;
namespace System.Net
{
// 摘要:
// 包含為 HTTP 定義的狀態代碼的值。
public enum HttpStatusCode
{
// 摘要:
// 等效於 HTTP 狀態 100。 System.Net.HttpStatusCode.Continue 指示客戶端可能繼續其請求。
Continue = 100,
//
// 摘要:
// 等效於 HTTP 狀態 101。 System.Net.HttpStatusCode.SwitchingProtocols 指示正在更改協議版本或協議。
SwitchingProtocols = 101,
//
// 摘要:
// 等效於 HTTP 狀態 200。 System.Net.HttpStatusCode.OK 指示請求成功,且請求的信息包含在響應中。 這是最常接收的狀態代碼。
OK = 200,
//
// 摘要:
// 等效於 HTTP 狀態 201。 System.Net.HttpStatusCode.Created 指示請求導致在響應被發送前創建新資源。
Created = 201,
//
// 摘要:
// 等效於 HTTP 狀態 202。 System.Net.HttpStatusCode.Accepted 指示請求已被接受做進一步處理。
Accepted = 202,
//
// 摘要:
// 等效於 HTTP 狀態 203。 System.Net.HttpStatusCode.NonAuthoritativeInformation 指示返回的元信息來自緩存副本而不是原始服務器,因此可能不正確。
NonAuthoritativeInformation = 203,
//
// 摘要:
// 等效於 HTTP 狀態 204。 System.Net.HttpStatusCode.NoContent 指示已成功處理請求並且響應已被設定為無內容。
NoContent = 204,
//
// 摘要:
// 等效於 HTTP 狀態 205。 System.Net.HttpStatusCode.ResetContent 指示客戶端應重置(或重新加載)當前資源。
ResetContent = 205,
//
// 摘要:
// 等效於 HTTP 狀態 206。 System.Net.HttpStatusCode.PartialContent 指示響應是包括字節范圍的 GET
// 請求所請求的部分響應。
PartialContent = 206,
//
// 摘要:
// 等效於 HTTP 狀態 300。 System.Net.HttpStatusCode.MultipleChoices 指示請求的信息有多種表示形式。
// 默認操作是將此狀態視為重定向,並遵循與此響應關聯的 Location 標頭的內容。
MultipleChoices = 300,
//
// 摘要:
// 等效於 HTTP 狀態 300。 System.Net.HttpStatusCode.Ambiguous 指示請求的信息有多種表示形式。 默認操作是將此狀態視為重定向,並遵循與此響應關聯的
// Location 標頭的內容。
Ambiguous = 300,
//
// 摘要:
// 等效於 HTTP 狀態 301。 System.Net.HttpStatusCode.MovedPermanently 指示請求的信息已移到 Location
// 頭中指定的 URI 處。 接收到此狀態時的默認操作為遵循與響應關聯的 Location 頭。
MovedPermanently = 301,
//
// 摘要:
// 等效於 HTTP 狀態 301。 System.Net.HttpStatusCode.Moved 指示請求的信息已移到 Location 頭中指定的
// URI 處。 接收到此狀態時的默認操作為遵循與響應關聯的 Location 頭。 原始請求方法為 POST 時,重定向的請求將使用 GET 方法。
Moved = 301,
//
// 摘要:
// 等效於 HTTP 狀態 302。 System.Net.HttpStatusCode.Found 指示請求的信息位於 Location 頭中指定的
// URI 處。 接收到此狀態時的默認操作為遵循與響應關聯的 Location 頭。 原始請求方法為 POST 時,重定向的請求將使用 GET 方法。
Found = 302,
//
// 摘要:
// 等效於 HTTP 狀態 302。 System.Net.HttpStatusCode.Redirect 指示請求的信息位於 Location 頭中指定的
// URI 處。 接收到此狀態時的默認操作為遵循與響應關聯的 Location 頭。 原始請求方法為 POST 時,重定向的請求將使用 GET 方法。
Redirect = 302,
//
// 摘要:
// 等效於 HTTP 狀態 303。 作為 POST 的結果,System.Net.HttpStatusCode.SeeOther 將客戶端自動重定向到
// Location 頭中指定的 URI。 用 GET 生成對 Location 標頭所指定的資源的請求。
SeeOther = 303,
//
// 摘要:
// 等效於 HTTP 狀態 303。 作為 POST 的結果,System.Net.HttpStatusCode.RedirectMethod 將客戶端自動重定向到
// Location 頭中指定的 URI。 用 GET 生成對 Location 標頭所指定的資源的請求。
RedirectMethod = 303,
//
// 摘要:
// 等效於 HTTP 狀態 304。 System.Net.HttpStatusCode.NotModified 指示客戶端的緩存副本是最新的。 未傳輸此資源的內容。
NotModified = 304,
//
// 摘要:
// 等效於 HTTP 狀態 305。 System.Net.HttpStatusCode.UseProxy 指示請求應使用位於 Location 頭中指定的
// URI 的代理服務器。
UseProxy = 305,
//
// 摘要:
// 等效於 HTTP 狀態 306。 System.Net.HttpStatusCode.Unused 是未完全指定的 HTTP/1.1 規范的建議擴展。
Unused = 306,
//
// 摘要:
// 等效於 HTTP 狀態 307。 System.Net.HttpStatusCode.RedirectKeepVerb 指示請求信息位於 Location
// 頭中指定的 URI 處。 接收到此狀態時的默認操作為遵循與響應關聯的 Location 頭。 原始請求方法為 POST 時,重定向的請求還將使用
// POST 方法。
RedirectKeepVerb = 307,
//
// 摘要:
// 等效於 HTTP 狀態 307。 System.Net.HttpStatusCode.TemporaryRedirect 指示請求信息位於 Location
// 頭中指定的 URI 處。 接收到此狀態時的默認操作為遵循與響應關聯的 Location 頭。 原始請求方法為 POST 時,重定向的請求還將使用
// POST 方法。
TemporaryRedirect = 307,
//
// 摘要:
// 等效於 HTTP 狀態 400。 System.Net.HttpStatusCode.BadRequest 指示服務器未能識別請求。 如果沒有其他適用的錯誤,或者不知道准確的錯誤或錯誤沒有自己的錯誤代碼,則發送
// System.Net.HttpStatusCode.BadRequest。
BadRequest = 400,
//
// 摘要:
// 等效於 HTTP 狀態 401。 System.Net.HttpStatusCode.Unauthorized 指示請求的資源要求身份驗證。 WWW-Authenticate
// 頭包含如何執行身份驗證的詳細信息。
Unauthorized = 401,
//
// 摘要:
// 等效於 HTTP 狀態 402。 保留 System.Net.HttpStatusCode.PaymentRequired 以供將來使用。
PaymentRequired = 402,
//
// 摘要:
// 等效於 HTTP 狀態 403。 System.Net.HttpStatusCode.Forbidden 指示服務器拒絕滿足請求。
Forbidden = 403,
//
// 摘要:
// 等效於 HTTP 狀態 404。 System.Net.HttpStatusCode.NotFound 指示請求的資源不在服務器上。
NotFound = 404,
//
// 摘要:
// 等效於 HTTP 狀態 405。 System.Net.HttpStatusCode.MethodNotAllowed 指示請求的資源上不允許請求方法(POST
// 或 GET)。
MethodNotAllowed = 405,
//
// 摘要:
// 等效於 HTTP 狀態 406。 System.Net.HttpStatusCode.NotAcceptable 指示客戶端已用 Accept 頭指示將不接受資源的任何可用表示形式。
NotAcceptable = 406,
//
// 摘要:
// 等效於 HTTP 狀態 407。 System.Net.HttpStatusCode.ProxyAuthenticationRequired 指示請求的代理要求身份驗證。
// Proxy-authenticate 頭包含如何執行身份驗證的詳細信息。
ProxyAuthenticationRequired = 407,
//
// 摘要:
// 等效於 HTTP 狀態 408。 System.Net.HttpStatusCode.RequestTimeout 指示客戶端沒有在服務器期望請求的時間內發送請求。
RequestTimeout = 408,
//
// 摘要:
// 等效於 HTTP 狀態 409。 System.Net.HttpStatusCode.Conflict 指示由於服務器上的沖突而未能執行請求。
Conflict = 409,
//
// 摘要:
// 等效於 HTTP 狀態 410。 System.Net.HttpStatusCode.Gone 指示請求的資源不再可用。
Gone = 410,
//
// 摘要:
// 等效於 HTTP 狀態 411。 System.Net.HttpStatusCode.LengthRequired 指示缺少必需的 Content-length
// 頭。
LengthRequired = 411,
//
// 摘要:
// 等效於 HTTP 狀態 412。 System.Net.HttpStatusCode.PreconditionFailed 指示為此請求設置的條件失敗,且無法執行此請求。
// 條件是用條件請求標頭(如 If-Match、If-None-Match 或 If-Unmodified-Since)設置的。
PreconditionFailed = 412,
//
// 摘要:
// 等效於 HTTP 狀態 413。 System.Net.HttpStatusCode.RequestEntityTooLarge 指示請求太大,服務器無法處理。
RequestEntityTooLarge = 413,
//
// 摘要:
// 等效於 HTTP 狀態 414。 System.Net.HttpStatusCode.RequestUriTooLong 指示 URI 太長。
RequestUriTooLong = 414,
//
// 摘要:
// 等效於 HTTP 狀態 415。 System.Net.HttpStatusCode.UnsupportedMediaType 指示請求是不支持的類型。
UnsupportedMediaType = 415,
//
// 摘要:
// 等效於 HTTP 狀態 416。 System.Net.HttpStatusCode.RequestedRangeNotSatisfiable 指示無法返回從資源請求的數據范圍,因為范圍的開頭在資源的開頭之前,或因為范圍的結尾在資源的結尾之后。
RequestedRangeNotSatisfiable = 416,
//
// 摘要:
// 等效於 HTTP 狀態 417。 System.Net.HttpStatusCode.ExpectationFailed 指示服務器未能符合 Expect
// 頭中給定的預期值。
ExpectationFailed = 417,
//
UpgradeRequired = 426,
//
// 摘要:
// 等效於 HTTP 狀態 500。 System.Net.HttpStatusCode.InternalServerError 指示服務器上發生了一般錯誤。
InternalServerError = 500,
//
// 摘要:
// 等效於 HTTP 狀態 501。 System.Net.HttpStatusCode.NotImplemented 指示服務器不支持請求的函數。
NotImplemented = 501,
//
// 摘要:
// 等效於 HTTP 狀態 502。 System.Net.HttpStatusCode.BadGateway 指示中間代理服務器從另一代理或原始服務器接收到錯誤響應。
BadGateway = 502,
//
// 摘要:
// 等效於 HTTP 狀態 503。 System.Net.HttpStatusCode.ServiceUnavailable 指示服務器暫時不可用,通常是由於過多加載或維護。
ServiceUnavailable = 503,
//
// 摘要:
// 等效於 HTTP 狀態 504。 System.Net.HttpStatusCode.GatewayTimeout 指示中間代理服務器在等待來自另一個代理或原始服務器的響應時已超時。
GatewayTimeout = 504,
//
// 摘要:
// 等效於 HTTP 狀態 505。 System.Net.HttpStatusCode.HttpVersionNotSupported 指示服務器不支持請求的
// HTTP 版本。
HttpVersionNotSupported = 505,
}
}
Http狀態碼
定義好了異常處理方法,剩下的就是如何使用了。可以根據實際情況,在不同級別使用統一的異常處理機制。
二、調用方式
1. 特性標簽
執行完成之后瀏覽器查看:
如果需要,甚至可以向Status Code里面寫入自定義的描述信息,並且還可以向我們的Response的Content里面寫入我們想要的信息。我們稍微改下OnException方法:
2. 控制器級別
如果想要某一個或者多個控制器里面的所有接口都使用異常過濾,直接在控制器上面標注特性即可。
- 某一個控制器上面啟用異常過濾
- 多個控制器上面同時啟用異常過濾
如果多個控制器都需要啟用異常過濾捕獲,那么就可以在多個控制器上面添加該特性標記。
那么要是控制器很多,豈不是我們每個都添加一遍,好累啊😴。聰明的程序員總是有辦法的!請看第三種方式。
3. 全局捕獲異常
如果需要對整個應用程序都啟用異常過濾,則需要做如下兩步:
- 在Global.asax全局配置里面添加 GlobalConfiguration.Configuration.Filters.Add(new CustomHandleErrorAttribute()); 這一句,如下:
void Application_Start(object sender, EventArgs e)
{
// 在應用程序啟動時運行的代碼
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
RouteConfig.RegisterRoutes(RouteTable.Routes);
GlobalConfiguration.Configuration.Filters.Add(new WebApiExceptionFilterAttribute());
}
- 在WebApiConfig.cs文件的Register方法里面添加 config.Filters.Add(new CustomHandleErrorAttribute()); 這一句,如下:
public static void Register(HttpConfiguration config)
{
// Web API 路由
config.MapHttpAttributeRoutes();
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
).RouteHandler = new SessionControllerRouteHandler();
config.Filters.Add(new CustomHandleErrorAttribute());
}
附帶一個我項目中使用的全局過濾器的源代碼
CustomHandleErrorAttribute
using BD.Common;
using NLog;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http.Filters;
namespace BD.EGMP.Website.Models
{
/// <summary>
/// 全局自定義異常處理
/// </summary>
public class CustomHandleErrorAttribute:ExceptionFilterAttribute
{
private readonly static ILogger logger = LogManager.GetCurrentClassLogger();
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
var code = -1;
var message = "服務器異常,請求失敗!請聯系管理員";
//獲取action的請求參數
var requestParameters= JsonHelper.SerializeObject(actionExecutedContext.ActionContext.ActionArguments.Values);
actionExecutedContext.Response = GetResponseMessage(code, message);
logger.Debug(actionExecutedContext.Exception, "服務器異常!" + actionExecutedContext.Exception.Message+ "——堆棧信息:" + actionExecutedContext.Exception.StackTrace+"——請求參數為:{0}", requestParameters);
}
private HttpResponseMessage GetResponseMessage(int code, string message)
{
var resultModel = new ApiModelsBase() { Code = code, Message = message };
return new HttpResponseMessage()
{
Content = new ObjectContent<ApiModelsBase>(
resultModel,
new JsonMediaTypeFormatter(),
"application/json"
)
};
}
}
internal class ApiModelsBase
{
public int Code { get; set; }
public string Message { get; set; }
}
}
JsonHelper
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
namespace BD.Common
{
/// <summary>
/// Json幫助類
/// </summary>
public class JsonHelper
{
/// <summary>
/// 將對象序列化為JSON格式
/// </summary>
/// <param name="o">對象</param>
/// <returns>json字符串</returns>
public static string SerializeObject(object o)
{
string json = JsonConvert.SerializeObject(o);
return json;
}
/// <summary>
/// 解析JSON字符串生成對象實體
/// </summary>
/// <typeparam name="T">對象類型</typeparam>
/// <param name="json">json字符串(eg.{"ID":"112","Name":"石子兒"})</param>
/// <returns>對象實體</returns>
public static T DeserializeJsonToObject<T>(string json) where T : class
{
JsonSerializer serializer = new JsonSerializer();
StringReader sr = new StringReader(json);
object o = serializer.Deserialize(new JsonTextReader(sr), typeof(T));
T t = o as T;
return t;
}
/// <summary>
/// 解析JSON數組生成對象實體集合
/// </summary>
/// <typeparam name="T">對象類型</typeparam>
/// <param name="json">json數組字符串(eg.[{"ID":"112","Name":"石子兒"}])</param>
/// <returns>對象實體集合</returns>
public static List<T> DeserializeJsonToList<T>(string json) where T : class
{
JsonSerializer serializer = new JsonSerializer();
StringReader sr = new StringReader(json);
object o = serializer.Deserialize(new JsonTextReader(sr), typeof(List<T>));
List<T> list = o as List<T>;
return list;
}
/// <summary>
/// 反序列化JSON到給定的匿名對象.
/// </summary>
/// <typeparam name="T">匿名對象類型</typeparam>
/// <param name="json">json字符串</param>
/// <param name="anonymousTypeObject">匿名對象</param>
/// <returns>匿名對象</returns>
public static T DeserializeAnonymousType<T>(string json, T anonymousTypeObject)
{
T t = JsonConvert.DeserializeAnonymousType(json, anonymousTypeObject);
return t;
}
}
}