系列目录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 由于临时文件超载致使服务器不能处理当前请求。