zoukankan      html  css  js  c++  java
  • 循序渐进学.Net Core Web Api开发系列【14】:异常处理

    原文:循序渐进学.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;
            }
    
        </span><span style="color: #0000ff;">public</span> ResultState state { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }
        </span><span style="color: #0000ff;">public</span> String ExceptionString { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }
    
        </span><span style="color: #0000ff;">public</span> Object result { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }
    }
    
    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">enum</span><span style="color: #000000;"> ResultState
    {
        Success,
        Exception
    }</span></pre>
    
    复制代码

    具体的Controller设计如下: 

    复制代码
    public ResultObject GetArticleByID(string id)
            {
                try
                {
                    int idn = int.Parse(id);
    
                Article article </span>=<span style="color: #000000;"> _context.Articles
                    .AsNoTracking()
                    .Where(a </span>=&gt; a.ID ==<span style="color: #000000;"> id)
                    .Single();
    
                </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> ResultObject
                {
                    result </span>=<span style="color: #000000;"> article
                };
            }
            </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (System.FormatException ex)
            {
                _logger.LogError(ex.Message </span>+ <span style="color: #800000;">"</span><span style="color: #800000;">
    </span><span style="color: #800000;">"</span> +<span style="color: #000000;"> ex.StackTrace);
    
                </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> ResultObject
                {
                    state </span>=<span style="color: #000000;"> ResultState.Exception,
                    ExceptionString </span>= <span style="color: #800000;">"</span><span style="color: #800000;">id必须为数字</span><span style="color: #800000;">"</span><span style="color: #000000;">
                };
            }
            </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (System.InvalidOperationException ex)
            {
                _logger.LogError(ex.Message </span>+ <span style="color: #800000;">"</span><span style="color: #800000;">
    </span><span style="color: #800000;">"</span> +<span style="color: #000000;"> ex.StackTrace);
    
                </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> ResultObject
                {
                    state </span>=<span style="color: #000000;"> ResultState.Exception,
                    ExceptionString </span>= <span style="color: #800000;">"</span><span style="color: #800000;">未查询到预料的数据</span><span style="color: #800000;">"</span><span style="color: #000000;">
                };
            }
            </span><span style="color: #0000ff;">catch</span><span style="color: #000000;">(MySql.Data.MySqlClient.MySqlException ex)
            {
                _logger.LogError(ex.Message </span>+ <span style="color: #800000;">"</span><span style="color: #800000;">
    </span><span style="color: #800000;">"</span> +<span style="color: #000000;"> ex.StackTrace);
    
                </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> ResultObject
                {
                    state </span>=<span style="color: #000000;"> ResultState.Exception,
                    ExceptionString </span>= <span style="color: #800000;">"</span><span style="color: #800000;">数据库异常</span><span style="color: #800000;">"</span><span style="color: #000000;">
                };
            }
        }</span></pre>
    
    复制代码

    对于上述代码,我们预料到可能用户会输入字符串而不是数字,也能预料到可能查询不到结果,所以就截获了这两个异常。对于ToList这样的操作,没有查询到数据会返回NULL,不会报异常,所以就不应该catch InvalidOperationException。另外,我们可能预料到会发生无法连接数据库的异常,在此也处理了,由于数据库连接异常可能在每个方法调用时都可能发生。建议提供为统一异常处理。

    四、全局未处理异常

    设计一个全局异常处理的中间件:

    复制代码
     public class UnifyExceptionMiddleware
        {
            private readonly RequestDelegate _next;
            private readonly ILogger _logger;
    
        </span><span style="color: #0000ff;">public</span> UnifyExceptionMiddleware(RequestDelegate next, ILogger&lt;UnifyExceptionMiddleware&gt;<span style="color: #000000;"> logger)
        {
            _next </span>=<span style="color: #000000;"> next;
            _logger</span>=<span style="color: #000000;">logger;
        }
    
        </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">async</span><span style="color: #000000;"> Task Invoke(HttpContext context)
        {
            ResultObject result </span>=<span style="color: #0000ff;">null</span><span style="color: #000000;">;
    
            </span><span style="color: #0000ff;">try</span><span style="color: #000000;">
            {
                </span><span style="color: #0000ff;">await</span><span style="color: #000000;"> _next(context);
            }
            </span><span style="color: #0000ff;">catch</span><span style="color: #000000;">(MySql.Data.MySqlClient.MySqlException ex)
            {
                _logger.LogError(ex.Message </span>+ <span style="color: #800000;">"</span><span style="color: #800000;">
    </span><span style="color: #800000;">"</span> +<span style="color: #000000;"> ex.StackTrace);
    
                result </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> ResultObject
                {
                    state </span>=<span style="color: #000000;"> ResultState.Exception,
                    ExceptionString </span>= <span style="color: #800000;">"</span><span style="color: #800000;">数据库异常</span><span style="color: #800000;">"</span><span style="color: #000000;">
                };
            }
            </span><span style="color: #0000ff;">catch</span><span style="color: #000000;">(Exception ex)
            {
                _logger.LogError($</span><span style="color: #800000;">"</span><span style="color: #800000;">系统发生未处理异常:{ex.StackTrace}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
    
                result </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> ResultObject
                {
                    state </span>=<span style="color: #000000;"> ResultState.Exception,
                    ExceptionString </span>= <span style="color: #800000;">"</span><span style="color: #800000;">系统发生未处理异常</span><span style="color: #800000;">"</span><span style="color: #000000;">
                };                
            }
    
            context.Response.StatusCode </span>= <span style="color: #800080;">200</span><span style="color: #000000;">;
            context.Response.ContentType </span>= <span style="color: #800000;">"</span><span style="color: #800000;">application/json; charset=utf-8</span><span style="color: #800000;">"</span><span style="color: #000000;">;
            context.Response.WriteAsync(JsonConvert.SerializeObject(result));
        }
    }
    
    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">class</span><span style="color: #000000;"> VisitLogMiddlewareExtensions
    {
        </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> IApplicationBuilder UseUnifyException(<span style="color: #0000ff;">this</span><span style="color: #000000;"> IApplicationBuilder builder)
        {
            </span><span style="color: #0000ff;">return</span> builder.UseMiddleware&lt;UnifyExceptionMiddleware&gt;<span style="color: #000000;">();
        }
    }</span></pre>
    
    复制代码

    使用该中间件

    复制代码
    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  因为临时文件超载导致服务器不能处理当前请求。

     

  • 相关阅读:
    ReadMe文档编写规范
    记录配置GPU遇到的问题
    文本分类流程详细总结(keras)
    打包 SyntaxError:Cannot use import statement outside a module browser_init.js
    SQLServer 转mysql
    Sql Server 查询某月有多少天
    nodejs http是使用异步方式调用接口,通过此方法可以实现同步调用
    linux 编译安装amqp扩展
    记一次 hadoop does not have enough number of replicas问题处理
    ldap+squid 实现
  • 原文地址:https://www.cnblogs.com/owenzh/p/11207176.html
Copyright © 2011-2022 走看看