Spring Boot教程(25) – 状态码和错误码的一种最佳实践

原文转载自 「闷瓜蛋子的BLOG」 (https://fookwood.com/spring-boot-tutorial-25-rest-codes)

预计阅读时间 0 分钟(共 0 个字, 0 张图片, 0 个链接)

前两天在V2EX上看到一个帖子,有人吐槽公司项目所有的接口请求都返回200状态码,再在body里加入code进行业务区分的做法不合理,帖子引起了程序员们激烈的讨论。左耳朵耗子(陈皓)之前也发微博讨论过这个问题,不同意见的两方人数都挺多的,各有各的道理。

“200一把梭”的人会采用类似如下的JSON响应:

{ 
   "code":123,
   "message":"OK",
   "data":{ ... }
}

即所有的请求状态码都是200,业务根据body中的code字段判断请求的状态,需要时会从data字段中拿出数据。这种方式的使用者,大概理由是,业务的状态码足够多,如果对应成HTTP状态码根本不够用。

而另一堆人对于这种做法十分反对,认为这相当于抛弃了HTTP协议的状态码,抛弃了普遍认可的共识。

我来说说我的看法吧:

首先,HTTP状态码是一定要用的,不能仅仅返回200。比如你们的项目可能有监控的需求,想要及时得知服务器错误信息,如果你使用了5xx的状态码,那么监控起来就会轻松一点,毕竟5xx是服务器错误的共识,监控软件肯定会针对性处理,如果你把错误藏在body里,再解析不是麻烦么。再比如有的时候可能对请求进行缓存,然而你肯定不希望把5xx错误都缓存起来。最后,你们的前端在调试的时候,如果发现页面发出的请求不太对,肯定会打开浏览器的DevTools去看看到底是哪个请求出错了,如果你用的是4xx或者5xx,那么DevTools里会显示红色的错误请求,方便前端开发去定位,如果你全部都是200,那不是不太方便嘛,共识帮你提高效率。

其次,HTTP状态码是不够用的。因为现实中乱七八糟的业务实在太多了,规范里的不够用。比如用户访问一个资源,被禁止了,那么状态码是403,实际项目中你可能需要告诉用户为什么被禁止了,也许是用户所在用户组没有权限,也许是这个用户被拉黑名单了,也许是资源被暂时关闭了,你想把这些信息体现给API的访问者,就需要提供更多的信息。你可能想自己创建个状态码,比如601,602,603什么的,但是我总感觉给HTTP生造状态码有点怪怪的。那么到底该怎么做呢?

我的选择是,所有请求一旦成功,返回200,然后body里面直接包含期望的数据,不使用code、message和data什么的再包一层。然后发生错误的话,按照HTTP协议里的状态码该选择哪个选择哪个,body里加上error_code和error_message字段,以及更详细的错误信息。这种方法也是很多开发者的选择,一些大厂用的也是类似的思路,比如Twitter,自己定义了几十种错误码,Stripe也自定义了code,不过是字符串类型的,不是数字,你也可以这样用。

还有一些大厂不是这么用的,很典型的就是微信的各种API,都是自己在body里包一层,当然这不是大问题,业务能跑能赚钱就行是不是?大家工作中也不要过于纠结,比较善于吵架的话,可以让同事都用你的选择,否则还是那句话:领导用啥咱用啥,同事用啥咱用啥,老代码用啥咱用啥,最后才是,我习惯用啥就用啥。

Spring Boot中如何来实现

既然我们是Spring Boot教程,那就需要搞清楚用它怎么去实现。

不知道大家平常是怎么处理异常流程的,我平常写控制器方法的时候,如果一切正常,一旦执行到最后的return语句,一定是返回2xx的。如果不成功,就抛出异常,然后让@ExceptionHandler方法处理,返回对应的错误响应。有的人可能使用的是ResponseEntity对象,在项目中通过分支控制来返回不同的ResponseEntity对象,此对象可以控制不同的HTTP状态码、Header和Body。下面用代码来简单说明一下。

这是一个控制器方法,用来查询用户信息。可以添加一个参数“userId”来提供用户ID,userId参数是Optional类型的,请求方可能传也可能不传,不传的时候会抛出异常UserIdMissingException,下面会发现通过一个UserService来查询具体的用户信息,它的实现可以是多种多样,比如从数据库查询、从网络查询、从内存中查询等等,它返回空的时候表明没有查到,同时会抛出异常UserNotFoundException,方法的返回值是最终查到的用户信息。如果发生了异常,代码会跳转到下图的@ExceptionHandler中。

针对不同的异常,你可以有不同的处理方法,可以有不同的HTTP状态码和错误码还有错误信息。ErrorResponse是我造的一个基类,对于一些简单的错误,你可以直接返回它的对象,对于复杂的错误,你需要创建个它的子类,加入一些字段以提供更多的信息。

这样做的好处是什么?一方面,程序的主流程比较清晰,你一打开控制器的方法,喵一眼就可以知道程序的目的是什么会返回什么结果,相对于不用异常而是用分支流程和用ResponseEntity对象作为返回结果,你能保留不少脑细胞。另一方面,错误处理都放在统一的文件里,比较好找好改,而且同一异常可能在多个地方被抛出,这样的话容易复用。

我们知道@ControllerAdvice中的@ExceptionHandler只能处理控制器中发生的异常,在控制器外发生的异常他们是捕捉不到的。之前我们介绍错误处理机制的时候说过,没有捕获的异常之后处理流程会把请求转发到/error路径下,BasicErrorController会对其进行处理,如果是网页,会返回我们熟悉的“白标错误页”,其他情况会返回JSON,有五个字段,如图:

这五个字段是由ErrorAttributes对象生成的,你可以查看ErrorMvcAutoConfiguration源码,如果你没有自定义一个ErrorAttributes对象,它会自动生成一个默认的DefaultErrorAttributes。如果你想将其改为我们上面说的那种格式,可以继承DefaultErrorAttributes从它的结果中拿到status和message,构造成我们所需要的结构:

这样一番折腾下来,所有发生错误的请求都会返回error_code和error_message,实现了我们设计的统一的格式。对于一些通用的,不必要很详细的错误,error_code直接照搬HTTP状态码,比如400,对于复杂和细化的错误,error_code在HTTP状态码后面加个后缀,比如40001,40002等等,方便调用者根据错误码做针对性的处理。当然这个error_code的设计还是非常灵活的,你一旦设计好,在文档中写清楚就好。

本文所说的方法,都是我瞎总结出来的,如果有朋友有更好的,设计更合理的实践,方便的话留言告诉我。

more_vert