前言:今天我们来聊一聊在基于SpringBoot前后端分离开发模式下,如何友好的返回统一的标准格式以及如何优雅的处理全局异常。
一般系统的大致整体架构图如下:
因为本篇博客主要介绍的是API接口,其他的模块比如网关、Redis缓存、MQ消息中间件小伙伴们自行去补充。
一、前后端RESTful接口交互
前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。
1.1、RESTful风格的URL路径设计
REST,即Representational State Transfer的缩写。翻译过来是表现层状态转换。实际上 REST 的全称是 Resource Representational State Transfer ,直白地翻译过来就是 “资源”在网络传输中以某种“表现形式”进行“状态转移” 。如果一个架构符合REST原则,就称它为RESTful架构。
REST由Roy Fielding在他的论文中提出。REST用来描述客户端通过某种形式获取服务器的数据,这些数据资源的格式通常是JSON或XML。同时,这些资源的表现或资源的集合是可以修改的,伴随着行为和关系可以通过多媒体来发现。在我看来,一种简单的理解就是:在设计API时,使用路径定位资源,方法定义操作,通过Content-Type和Accept来协商资源的类型。
资源(Resource) :我们可以把真实的对象数据称为资源。一个资源既可以是一个集合,也可以是单个个体。比如我们的班级 classes 是代表一个集合形式的资源,而特定的 class 代表单个个体资源。每一种资源都有特定的 URI(统一资源标识符)与之对应,如果我们需要获取这个资源,访问这个 URI 就可以了,比如获取特定的班级:
/class/12
。另外,资源也可以包含子资源,比如/classes/classId/teachers
:列出某个指定班级的所有老师的信息表现形式(Representational):"资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式比如
json
,xml
,image
,txt
等等叫做它的"表现层/表现形式"。状态转移(State Transfer) :大家第一眼看到这个词语一定会很懵逼?内心 BB:这尼玛是啥啊? 大白话来说 REST 中的状态转移更多地描述的服务器端资源的状态,比如你通过增删改查(通过 HTTP 动词实现)引起资源状态的改变。ps:互联网通信协议 HTTP 协议,是一个无状态协议,所有的资源状态都保存在服务器端。
1.2、RESTful规范
方法动作
GET:查询操作,举个例子:
GET /classes
(获取所有班级)POST:修改/添加操作,举个例子:
POST /classes
(创建班级)PUT:更新操作,举个例子:
PUT /classes/12
(更新编号为 12 的班级)DELETE:删除操作,举个例子:
DELETE /classes/12
(删除编号为 12 的班级)PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。
接口路径命名规范
表示 API 的具体网址。实际开发中常见的规范如下:
接口网址尽量使用名词,避免使用动词,API 中的名词也应该使用复数。 因为 RESTful API 操作(HTTP Method)的是资源(名词)而不是动作(动词),REST 中的资源往往和数据库中的表对应,而数据库中的表都是同种记录的"集合"(collection)。如果 API 调用并不涉及资源(如计算,翻译等操作)的话,可以用动词。比如:
GET /calculate?param1=11¶m2=33
。不用大写字母,建议用中杠 - 不用下杠 _ 。比如邀请码写成
invitation-code
而不是 invitation_code 。善用版本化 API。当我们的 API 发生了重大改变而不兼容前期版本的时候,我们可以通过 URL 来实现版本化,比如
http://api.example.com/v1
、http://apiv1.example.com
。版本不必非要是数字,只是数字用的最多,日期、季节都可以作为版本标识符,项目团队达成共识就可。
RESTful为什么加版本号
所有的API必须保持向后兼容,必须在引入新版本API的同时确保旧版本API仍然可用。所以应该为其提供版本支持。必须在URL中嵌入版本编号,格式要求如下:http://URL/api/v1/*
一般来说,API接口是提供给其他系统或是其他公司使用,不能随意频繁的变更。然而,需求和业务不断变化,接口和参数也会发生相应的变化。如果直接对原来的接口进行修改,势必会影响线其他系统的正常运行。这就必须对api 接口进行有效的版本控制。
例如,添加用户的接口,由于业务需求变化,接口的字段属性也发生了变化而且可能和之前的功能不兼容。为了保证原有的接口调用方不受影响,只能重新定义一个新的接口。
http://localhost:8080/api/v1/user
http://localhost:8080/api/v2/user
Api 版本控制的方式:
请求url 路径区分,在同一个域名下使用不同的url路径,test.com/api/v1/,test.com/api/v2
域名区分管理,即不同的版本使用不同的域名,v1.api.test.com,v2.api.test.com
请求参数区分,在同一url路径下,增加version=v1或v2 等,然后根据不同的版本,选择执行不同的方法。
实际项目中,一般选择第一种:请求url路径区分。因为第一种既能保证水平扩展,又不影响以前的老版本
1.3、RESTful URL设计
RESTful风格的url能实现自我功能的描述,清晰明了。
user/{uid}/friends //用户/用户id/用户的朋友,表示用户的好友列表,语义清晰明了,符合RESTful风格
user/friends/uid //不符合RESTful风格,url语义不顺畅
item/{id}/delete //这里delete可以视为名词,商品/商品id/删除,语义清晰明了,符合RESTful风格
item/delete/{id} //商品/删除/商品id,url语义不明了,不清晰
针对RESTful风格URL路径的传入参数的公共请求头的要求(如:app_version,api_version,device等),这里就不介绍了。
1.4、RestFul接口举例
Talk is cheap!来举个实际的例子来说明一下吧!现在有这样一个 API 提供班级(class)的信息,还包括班级中的学生和教师的信息,则它的路径应该设计成下面这样。
GET /classes:列出所有班级
POST /classes:新建一个班级
GET /classes/{classId}:获取某个指定班级的信息
PUT /classes/{classId}:更新某个指定班级的信息(一般倾向整体更新)
PATCH /classes/{classId}:更新某个指定班级的信息(一般倾向部分更新)
DELETE /classes/{classId}:删除某个班级
GET /classes/{classId}/teachers:列出某个指定班级的所有老师的信息
GET /classes/{classId}/students:列出某个指定班级的所有学生的信息
DELETE /classes/{classId}/teachers/{ID}:删除某个指定班级下的指定的老师的信息
反例:
/getAllclasses
/createNewclass
/deleteAllActiveclasses
理清资源的层次结构,比如业务针对的范围是学校,那么学校会是一级资源:/schools
,老师: /schools/teachers
,学生: /schools/students
就是二级资源。
1.5、@GetMapping、@PostMapping、@PutMapping和@DeleteMapping
从Spring4.3开始引进了{@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping},提供了对Restful风格的支持,来帮助简化常用的HTTP方法的映射,并更好地表达被注解方法的语义。
@GetMapping,处理get请求
@PostMapping,处理post请求
@PutMapping,处理put请求
@DeleteMapping,处理delete请求
以@GetMapping和@PostMapping为例,Spring官方文档说:
@GetMapping是一个组合注解,是@RequestMapping(method = RequestMethod.GET)的缩写。该注解将HTTP Get 映射到 特定的处理方法上。
@PostMapping 是一个组合注解,是@RequestMapping(method = RequestMethod.POST)的缩写。该注解将HTTP Post 映射到 特定的处理方法上。
二、统一的API接口返回格式
着重介绍一下后端服务器如何实现把数据返回给前端,后端返回给前端我们一般用JSON体方式,定义如下:
{
#返回状态码
code:integer,
#返回信息描述
message:string,
#返回值
data:object
}
2.1、CODE状态码
code返回状态码,一般小伙伴们是在开发的时候需要什么,就添加什么。
如接口要返回用户权限异常,我们加一个状态码为101吧,下一次又要加一个数据参数异常,就加一个102的状态码。这样虽然能够照常满足业务,但状态码太凌乱了
我们应该可以参考HTTP请求返回的状态码
下面是常见的HTTP状态码:
200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误
我们可以参考这样的设计,这样的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成4位数。
#1000~1999 区间表示参数错误
#2000~2999 区间表示用户错误
#3000~3999 区间表示接口异常
这样前端开发人员在得到返回值后,根据状态码就可以知道,大概什么错误,再根据message相关的信息描述,可以快速定位。
2.2、Message
这个字段相对理解比较简单,就是发生错误时,如何友好的进行提示。一般的设计是和code状态码一起设计,如:
再在枚举中定义,状态码:
状态码和信息就会一一对应,比较好维护。
2.3、Data
返回数据体,JSON格式,根据不同的业务有不同的JSON体。
2.4、API接口统一返回使用举例
(1) 我们要设计一个返回体类Result
(2)控制层Controller
我们会在controller层处理业务请求,并返回给前端,以order订单为例
我们看到在获得order对象之后,我们是用的Result构造方法进行包装赋值,然后进行返回。小伙伴们有没有发现,构造方法这样的包装是不是很麻烦,我们可以优化一下。
(3)美观美化
我们可以在Result类中,加入静态方法,一看就懂
那我们来改造一下Controller:
上面我们看到在Result类中增加了静态方法,使得业务处理代码更加简洁。
三、设计一个返回体类ResultBean类
package com.hs.demo.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 统一返回 ResultBean:将所有的接口的响应数据的格式进行统一。
*
* @Data 注解的主要作用是提高代码的简洁,使用这个注解可以省去代码中大量的get()、 set()、 toString()等方法;要使用 @Data 注解要先引入lombok,lombok 是什么,它是一个工具类库,可以用简单的注解形式来简化代码,提高开发效率。
*/
@Data
@ApiModel("固定返回格式")
public class ResultBean implement Serializable{
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
@ApiModelProperty("错误码")
private Integer code;
/**
* 提示信息
*/
@ApiModelProperty("提示信息")
private String message;
/**
* 具体的内容
*/
@ApiModelProperty("响应数据")
private Object data;
}
四、API全局返回码
描述API接口的共性返回码,API自定义的接口返回码请参阅对应API接口文档描述。
API全局返回码,就是每次调用接口时,可能获得正确或错误的返回码,开发者可以根据返回码信息调试接口,排查错误。
百度Open API错误码定义:
错误码 | 错误描述 | Error Description |
0 | 成功 | Success |
1 | 未知错误 | Unknown error |
2 | 服务暂不可用 | Service temporarily unavailable |
3 | 未知的方法 | Unsupported openapi method |
4 | 接口调用次数已达到设定的上限 | Open api request limit reached |
5 | 请求来自未经授权的IP地址 | Unauthorized client IP address |
6 | 无权限访问该用户数据 | No permission to access user data |
7 | 来自该refer的请求无访问权限 | No permission to access data for this referer |
100 | 请求参数无效 | Invalid parameter |
101 | api key无效 | Invalid API key |
104 | 无效签名 | Incorrect signature |
105 | 请求参数过多 | Too many parameters |
106 | 未知的签名方法 | Unsupported signature method |
107 | timestamp参数无效 | Invalid/Used timestamp parameter |
109 | 无效的用户资料字段名 | Invalid user info field |
110 | 无效的access token | Access token invalid or no longer valid |
111 | access token过期 | Access token expired |
210 | 用户不可见 | User not visible |
211 | 获取未授权的字段 | Unsupported permission |
212 | 没有权限获取用户的email | No permission to access user email |
800 | 未知的存储操作错误 | Unknown data store API error |
801 | 无效的操作方法 | Invalid operation |
802 | 数据存储空间已超过设定的上限 | Data store allowable quota was exceeded |
803 | 指定的对象不存在 | Specified object cannot be found |
804 | 指定的对象已存在 | Specified object already exists |
805 | 数据库操作出错,请重试 | A database error occurred. Please try again |
900 | 访问的应用不存在 | No such application exists |
五、授权/令牌请求接口返回码
描述应用发起授权请求或令牌请求时,开放平台的返回码。
错误码 | 错误描述 | Error Description |
10000 | 非法的请求参数 | Invalid request |
10001 | 用户认证失败 | Invalid client |
10002 | 非法的授权信息 | Invalid grant |
10003 | 应用没有被授权,无法使用所指定的grant_type | Unauthorized client |
10004 | grant_type字段超过定义范围 | Unsupported grant_type |
10005 | scope信息无效或超出范围 | Invalid scope |
10006 | 提供的更新令牌已过期 | Expired token |
10007 | redirect_uri字段与注册应用时所填写的不匹配 | Redirect_uri mismatch |
10008 | response_type参数值超过定义范围 | Unsupported response type |
10009 | 用户或授权服务器拒绝授予数据访问权限 | Access denied |
参考文档:
SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!