5

[.net] 关于Exception的几点思考和在项目中的使用(三)

 3 years ago
source link: https://www.cnblogs.com/hubaijia/p/about-exceptions-3.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文链接: 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, 这是我必备的一款扩展。

c0EGLt.png

这款插件会借助注释的形式,协助实现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,以及全局错误处理。

谢谢阅读。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK