SpringBoot统一接口返回和全局异常
写在最前:后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建,这是我们应该深入思考的问题,公司不同,规范不同,要求不同。不管公司有没有要求,我们对自己必须要有要求。今天的这篇文章我们主要介绍,怎么统一处理下接口的返回格式问题。在写后端接口,思考下药做些什么:请求,逻辑处理,返回。这似乎已经可以满足
目录
2.2、第二种:Validator + BindResult进行校验
2.5、第三种高阶用法:Validator + 自动包装处理异常
1、写在最前
前端和前端进行交互,前端按约定的请求URL路径,并合并相关参数,进入服务器接收请求,进行业务处理,返回数据给前端。
后端接口大致分为四个部分组成:接口地址(url)、接口请求方式(get、post等)、请求数据(request)、响应数据(response)。如何构建,这是我们应该深入思考的问题,公司不同,规范不同,要求不同。不管公司有没有要求,我们对自己必须要有要求。
针对URL路径的restful风格,以及引用参数的公共请求头的要求(如:app_version,api_version,device等),老顾这里就不介绍了,小伙伴们可以自行去了解,也比较简单。
今天的这篇文章我们主要介绍,怎么统一处理下接口的返回格式问题。
简单更新接口逻辑,其中包括:
① 请求参数、响应结果日志打印
② 基础参数的校验
③更新用户业务主逻辑
④ 全局异常的捕获
⑤ 对Result结果的封装。这其中貌似一个步骤也不能省略,但却导致我们写代码效率低下,其实最重要的一步仅仅是③
那么我们改如何改进呢?
① 请求参数、响应结果日志打印 -> 使用在controller方法外对做切面统一打印日志(非GET方法都会打印)
② 基础参数的校验 -> 使用hibernate validate做操作校验
④ 全局异常的捕获 -> 使用@ControllerAdvice写全局异常处理类控制异常展现方式
⑤ 对Result结果的封装 -> 实现ResponseBodyAdvice接口,对接口响应体统一处理
请求:请求方式,请求数据格式,参数检验
逻辑处理:日志记录。。。。。。
返回:统一返回对象,包装,异常处理
2、参数校验几种方式
一个接口一般对参数(请求数据)都会进行安全校验,参数校验的重要性自然不必多说,那么如何对参数进行校验就有讲究了。
2.1、第一种:业务层校验
来看一下最常见的做法,业务层进行参数校验:
if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
return "对象或者对象字段不能为空";
}
if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
return "不能输入空字符串";
}
if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return "密码长度必须是6-16个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}
2.2、第二种:Validator + BindResult进行校验
为什么要用validator?
javax.validation的一系列注解可以帮我们完成参数校验,免去繁琐的串行校验
什么是javax.validation
JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,
2.2.1、注解说明
-
@NotNull:不能为null,但可以为empty(""," "," ")
-
@NotEmpty:不能为null,而且长度必须大于0 (" "," ")
-
@NotBlank:只能作用在String上,不能为null,而且调用trim()后,长度必须大于0("test") 即:必须有实际字符
2.2.2、实战演练
使用Spring Validator,Validator依赖包在SpringBoot中已经包含在starter-web中,可直接使用,2.3以后需要自己引入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.10.RELEASE</version>
</dependency>
1)对参数的字段进行注解标注
Validator可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Data
public class User {
@NotNull(message = "用户id不能为空")
private Long id;
/** 用户名*/
@NotBlank(message = "用户名不能为空")
@Length(max = 20, message = "用户名不能超过{max}个字符")
@Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
private String username;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
/** 手机号*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
private String mobile;
}
2)@Validated 声明要检查的参数
校验规则和错误提示信息配置完毕后,接下来只需要在接口需要校验的参数上加上@Valid注解,并添加BindResult参数用于存放验证结果,即可方便完成验证:
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
return userService.addUser(user);
}
}
当请求数据传递到接口的时候Validator就自动完成校验了,校验的结果就会封装到BindingResult中去,如果有错误信息我们就直接返回给前端,业务逻辑代码也根本没有执行下去。此时,业务层里的校验代码就已经不需要了:
现在可以看一下参数校验效果。我们故意给这个接口传递一个不符合校验规则的参数,先传递一个错误数据给接口,故意将password这个字段不满足校验条件:
{
"uaername": "testusername",
"email": "111111@qq.com",
"id": 0,
"password": ,
"mobile":"13372677829"
}
再来看一下接口的响应数据:
不难看出使用Validator校验有如下几个好处:
- 代码简化:之前业务层那么一大段校验代码都被省略掉了。
- 使用方便:那么多校验规则可以轻而易举的实现,比如邮箱格式验证,手机号码校验等,用Validator直接一个注解搞定。
- 减少耦合度:使用Validator能够让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来。
使用Validator+ BindingResult已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个Result参数,然后再提取错误信息返回给前端。
这样有点麻烦,并且重复代码很多(尽管可以将这个重复代码封装成方法)。我们能否去掉BindingResult这一步呢?当然是可以的!
2.5、第三种高阶用法:Validator + 自动包装处理异常
我们完全可以将BindingResult这一步给去掉:
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}
去掉之后会发生什么事情呢?直接来试验一下,还是按照之前一样故意传递一个不符合校验规则的参数给接口。此时我们观察控制台可以发现接口已经引发MethodArgumentNotValidException异常
去掉BindingResult后会自动引发异常,异常发生了自然而然就不会执行业务逻辑。也就是说,我们完全没必要添加相关BindingResult相关操作嘛。
具体实现见第五节
3、异常处理
3.1全局异常处理
参数校验失败或业务操作抛出的异常,当然不可能再去手动捕捉异常进行处理,不然还不如用之前BindingResult方式呢。又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用SpringBoot全局异常处理来达到一劳永逸的效果!
为了优雅一点,我们将参数异常,业务异常,统一做了一个全局异常,将控制层的异常包装到我们自定义的异常中
为了优雅一点,我们还做了一个统一的结构体,将请求的code,和msg,data一起统一封装到结构体中,增加了代码的复用性
3.1.1、基本使用
首先,我们需要新建一个类,在这个类上加上@ControllerAdvice或@RestControllerAdvice注解,这个类就配置成全局处理类了。(这个根据你的Controller层用的是@Controller还是@RestController来决定)
@RestControllerAdvice = @ControllerAdvice + @ResponseBody + ..
然后在类中新建方法,在方法上加上@ExceptionHandler注解并指定你想处理的异常类型,接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!
我们现在就来演示一下对参数校验失败抛出的MethodArgumentNotValidException全局处理:
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return objectError.getDefaultMessage();
}
}
我们再来看下这次校验失败后的响应数据:
返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!以后我们再想写接口参数校验,就只需要在入参的成员变量上加上Validator校验规则注解,然后在参数上加上@Valid注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!
3.2、自定义异常
全局处理当然不会只能处理一种异常,用途也不仅仅是对一个参数校验方式进行优化。在实际开发中,如何对异常处理其实是一个很麻烦的事情。传统处理异常一般有以下烦恼:
- 是捕获异常(try…catch)还是抛出异常(throws)
- 是在controller层做处理还是在service层处理又或是在dao层做处理
- 处理异常的方式是啥也不做,还是返回特定数据,如果返回又返回什么数据
- 不是所有异常我们都能预先进行捕捉,如果发生了没有捕捉到的异常该怎么办?
以上这些问题都可以用全局异常处理来解决,全局异常处理也叫统一异常处理,全局和统一处理代表什么?代表规范!规范有了,很多问题就会迎刃而解!
全局异常处理的基本使用方式大家都已经知道了,我们接下来更进一步的规范项目中的异常处理方式:自定义异常
在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,我这时候就可以手动抛出异常从而触发事务回滚。那手动抛出异常最简单的方式就是throw new RuntimeException("异常信息")
了,不过使用自定义会更好一些:
- 自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
- 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
- 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
我们现在就来开始写一个自定义异常:
@Getter //只要getter方法,无需setter
public class APIException extends RuntimeException {
private int code;
private String msg;
public APIException() {
this(1001, "接口错误");
}
public APIException(String msg) {
this(1001, msg);
}
public APIException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
}
在刚才的全局异常处理类中记得添加对我们自定义异常的处理:
@ExceptionHandler(APIException.class)
public String APIExceptionHandler(APIException e) {
return e.getMsg();
}
这样就对异常的处理就比较规范了,当然还可以添加对Exception的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端,不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。
现在全局异常处理和自定义异常已经弄好了,不知道大家有没有发现一个问题,就是当我们抛出自定义异常的时候全局异常处理只响应了异常中的错误信息msg给前端,并没有将错误代码code返回。这就要引申出我们接下来要讲的东西了:数据统一响应
4、数据统一响应
现在我们规范好了参数校验方式和异常处理方式,然而还没有规范响应数据!比如我要获取一个分页信息数据,获取成功了呢自然就返回的数据列表,获取失败了后台就会响应异常信息,即一个字符串,就是说前端开发者压根就不知道后端响应过来的数据会是啥样的!所以,统一响应数据是前后端规范中必须要做的!
4.1、SpringBoot默认返回格式
4.1.1、字符串
@GetMapping("/getUserName")
public String getUserName(){
return "HuaGe";
}
调用接口返回结果:
HuaGe
4.1.2、实体类
@GetMapping("/getUserName")
public User getUserName(){
return new User("HuaGe",18,"男");
}
调用接口返回结果:
{
"name": "HuaGe",
"age": "18",
"性别": "男",
}
4.2、参数说明
4.2.1、返回格式
最初返回给前端我们一般用JSON体方式,定义如下:
{
#返回状态码:后台可以维护一套统一的状态码;后台可以维护一套统一的状态码;
code:integer,
#返回信息描述,接口调用成功/失败的提示信息
message:string,
#返回值
data:object
}
4.2.2、CODE状态码
code返回状态码,开发的时候需要什么,就添加什么。
可以参考HTTP请求返回的状态码
常见的HTTP状态码:
200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误
可以参考这样的设计,这样的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成4个数字
#1000~1999 区间表示参数错误
#2000~2999 区间表示用户错误
#3000~3999 区间表示接口异常
4.2.3、状态码
这个相对相对理解比较简单,就是发生错误时,如何友好的进行提示。一般的设计是和code状态码一起设计,如
/**
* 全局异常处理枚举类
*/
public enum ResultEnum {
BODY_NOT_MATCH(400,"请求的数据格式不符!"),
SIGNATURE_NOT_MATCH(401,"请求的数字签名不匹配!"),
NOT_FOUND(404, "未找到该资源!"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误!"),
SERVER_BUSY(503,"服务器正忙,请稍后再试!"),
/** 成功 */
SUCCESS(1000, "成功"),
/** 无法找到资源错误 */
NOT_FOUNT_RESOURCE(1001,"没有找到相关资源!"),
/** 请求参数有误 */
PARAMETER_ERROR(1002,"请求参数有误!"),
/** 确少必要请求参数异常 */
PARAMETER_MISSING_ERROR(1003,"确少必要请求参数!"),
/** 确少必要请求参数异常 */
REQUEST_MISSING_BODY_ERROR(1004,"缺少请求体!"),
/** 未知错误 */
SYSTEM_ERROR(9998,"未知的错误!"),
/** 系统错误 */
UNKNOWN_ERROR(9999,"未知的错误!");
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
状态码和信息就会一一对应,比较好维护。
4.2.4、数据
返回数据体,JSON格式,根据不同的业务又不同的JSON体。
@Data
public class Result<T> {
private int code;
private String message;
private T data;
public Result() {}
public Result(int code, String message) {
this.code = code;
this.message = message;
}
/**
* 成功
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<T>();
result.setCode(ResultEnum.SUCCESS.getCode());
result.setMessage(ResultEnum.SUCCESS.getMessage());
result.setData(data);
return result;
}
/**
* 失败
*/
public static <T> Result<T> error(int code, String message) {
return new Result(code, message);
}
}
上面两步定义了数据返回格式和状态码,接下来就要看下在接口中如何使用了。
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ResultaddUser(@Validated @RequestBody User user) {
user.setId(10000L);
user.setCreateTime(new Date());
return Result.success(user);
}
}
返回
{
"code": 0,
"message": "成功",
"data": "huage"
}
Result.success()这段逻辑显然很多余,每个方法都要这样写一遍
5、高阶用法
将所有的异常进行集中处理,并统一返回一个响应体。这可以使代码更清晰,减少代码重复,同时也可以提高应用程序的健壮性和可维护性。通过返回统一的响应体,客户端可以更容易地理解和处理错误。
5.1、类介绍
- @ResponseBodyAdvice: 该接口是SpringMVC 4.1提供的,它允许在执行@ResponseBody后自定义返回数据,用来封装统一数据格式返回;
- @RestControllerAdvice: 该注解是对Controller进行增强的,可以全局捕获抛出的异常。
5.2、切面
- 新建ResponseAdvice类;
- 实现ResponseBodyAdvice接口,实现supports、beforeBodyWrite方法;
- 该类用于统一封装controller中接口的返回结果。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 全局统一返回
*/
//加上需要扫描的包
@ControllerAdvice(basePackages = "com.ykx.code.controller")
public class GlobResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
/**
* 是否开启功能 true:开启
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
// 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false
return !returnType.getGenericParameterType().equals(ResultVO.class);
}
/**
* 处理返回结果
*/
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
//处理字符串类型数据
if(o instanceof String){
try {
return objectMapper.writeValueAsString(Result.success(o));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
//返回类型是否已经封装
if(o instanceof Result){
return o;
}
return Result.success(o);
}
}
我们可以通过getUserName接口测试一下,会发现和直接使用Result返回的结果是一致的。
不过,在ResponseAdvice我们全部使用了Result.success(o)来处理结果,对于error类型的结果未做处理。我们来看下,发生异常情况时,返回结果是什么样呢?继续使用上面HashMap空指针异常的代码,测试结果如下:
{
"code": 0,
"message": "成功",
"data": {
"timestamp": "2021-08-09T09:33:26.805+00:00",
"status": 405,
"error": "Method Not Allowed",
"path": "/sysUser/getUserName"
}
}
虽然格式上没有毛病,但是在code、data字段的具体数据上是不友好或不正确的。
5.3、响应体
统一数据响应第一步肯定要做的就是我们自己自定义一个响应体类,无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!那么如何定义响应体呢?
可以参考我们自定义异常类,也来一个响应信息代码code和响应信息说明msg:
import lombok.Data;
import lombok.experimental.Accessors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 自定义统一响应体返回消息类
*
* @param <T>
*/
@Data
@Accessors(chain = true)
public class ResponseMessage<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(ResponseMessage.class);
/**
* 返回状态码
*/
private int code;
/**
* 状态消息
*/
private String message;
/**
* 返回数据
*/
private T data;
public ResponseMessage() {
}
public ResponseMessage(int code, String message) {
this.code = code;
this.message = message;
LOGGER.info(toString());
}
public ResponseMessage(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
LOGGER.info(toString());
}
public static ResponseMessage success() {
return new ResponseMessage(ResponseMessageErrorCodeEnum.SUCCESS.getCode(), "", true);
}
public static <T> ResponseMessage<T> success(int code, T t) {
return new ResponseMessage(code, "", t);
}
public static <T> ResponseMessage<T> success(int code, String message, T t) {
return new ResponseMessage(code, message, t);
}
public static <T> ResponseMessage<T> success(T t) {
return new ResponseMessage(ResponseMessageErrorCodeEnum.SUCCESS.getCode(), "", t);
}
public static ResponseMessage error() {
return error("");
}
public static ResponseMessage error(String message) {
return error(ResponseMessageErrorCodeEnum.SYSTEM_INNER_ERROR.getCode(), message);
}
public static ResponseMessage error(int code, String message) {
return error(code, message, null);
}
public static <T> ResponseMessage<T> error(int code, String message, T t) {
return new ResponseMessage(code, message, t);
}
}
然后我们修改一下全局异常处理那的返回值:
/** -------- 指定异常处理方法 -------- **/
@ExceptionHandler(NullPointerException.class)
public ResponseMessage error(NullPointerException e) {
e.printStackTrace();
return new ResponseMessage(ResultEnum.PARAMETER_MISSING_ERROR.getCode(), ResultEnum.PARAMETER_ERROR.getMessage());
}
/** -------- 参数校验异常 -------- **/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseMessage MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return new ResponseMessage(ResultEnum.PARAMETER_MISSING_ERROR.getCode(), ResultEnum.PARAMETER_ERROR.getMessage());
}
我们再来看一下此时如果发生异常了会响应什么数据给前端:
完整的Controller
@ResponseBody
@RestController
public class HealthControl {
@Autowired
private HealthCodeService healthService;
@RequestMapping(value = "/query_one", method = {RequestMethod.POST})
public Object queryOne(@Valid RequstDto requstDto) {
int i = 1 / 0;
return "";
}
@RequestMapping("/query_list")
public Object queryList(RequstDto requstDto) {
return healthService.queryList();
}
/**
* 健康检测
*
* @return
*/
@RequestMapping(value = "healthCheck")
public String healthCheck() {
return "200";
}
}
看一下如果响应正确返回的是什么效果:
这样无论是正确响应还是发生异常,响应数据的格式都是统一的,十分规范!
数据格式是规范了,不过响应码code和响应信息msg还没有规范呀!大家发现没有,无论是正确响应,还是异常响应,响应码和响应信息是想怎么设置就怎么设置,要是10个开发人员对同一个类型的响应写10个不同的响应码,那这个统一响应体的格式规范就毫无意义!所以,必须要将响应码和响应信息给规范起来。
5.4、响应码枚举
要规范响应体中的响应码和响应信息用枚举简直再恰当不过了,我们现在就来创建一个响应码枚举类:
全局状态码
当你发现你的系统中错误码随意定义,没有任何规范的时候,你应该考虑下使用一个枚举全局管理下你的状态码,这对线上环境定位错误问题和后续接口文档的维护都是很有帮助的。
下面我们在给出一个完整版代码示例,用于参考:
import java.util.ArrayList;
import java.util.List;
/**
* API 统一返回状态码
* Created by zhumaer on 17/5/24.
*/
public enum ResultCode {
/* 成功状态码 */
SUCCESS(1, "成功"),
/* 参数错误:10001-19999 */
PARAM_IS_INVALID(10001, "参数无效"),
PARAM_IS_BLANK(10002, "参数为空"),
PARAM_TYPE_BIND_ERROR(10003, "参数类型错误"),
PARAM_NOT_COMPLETE(10004, "参数缺失"),
/* 用户错误:20001-29999*/
USER_NOT_LOGGED_IN(20001, "用户未登录"),
USER_LOGIN_ERROR(20002, "账号不存在或密码错误"),
USER_ACCOUNT_FORBIDDEN(20003, "账号已被禁用"),
USER_NOT_EXIST(20004, "用户不存在"),
USER_HAS_EXISTED(20005, "用户已存在"),
/* 业务错误:30001-39999 */
SPECIFIED_QUESTIONED_USER_NOT_EXIST(30001, "某业务出现问题"),
/* 系统错误:40001-49999 */
SYSTEM_INNER_ERROR(40001, "系统繁忙,请稍后重试"),
/* 数据错误:50001-599999 */
RESULE_DATA_NONE(50001, "数据未找到"),
DATA_IS_WRONG(50002, "数据有误"),
DATA_ALREADY_EXISTED(50003, "数据已存在"),
/* 接口错误:60001-69999 */
INTERFACE_INNER_INVOKE_ERROR(60001, "内部系统接口调用异常"),
INTERFACE_OUTTER_INVOKE_ERROR(60002, "外部系统接口调用异常"),
INTERFACE_FORBID_VISIT(60003, "该接口禁止访问"),
INTERFACE_ADDRESS_INVALID(60004, "接口地址无效"),
INTERFACE_REQUEST_TIMEOUT(60005, "接口请求超时"),
INTERFACE_EXCEED_LOAD(60006, "接口负载过高"),
/* 权限错误:70001-79999 */
PERMISSION_NO_ACCESS(70001, "无访问权限");
private Integer code;
private String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer code() {
return this.code;
}
public String message() {
return this.message;
}
public static String getMessage(String name) {
for (ResultCode item : ResultCode.values()) {
if (item.name().equals(name)) {
return item.message;
}
}
return name;
}
public static Integer getCode(String name) {
for (ResultCode item : ResultCode.values()) {
if (item.name().equals(name)) {
return item.code;
}
}
return null;
}
@Override
public String toString() {
return this.name();
}
}
然后修改响应体的构造方法,让其只准接受响应码枚举来设置响应码和响应信息:
import java.io.Serializable;
/**
* API统一返回值类
* Created by ws on 19/4/23.
*/
@Data
public class ResultMessage implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String msg;
private Object data;
public ResultMessage() {}
public ResultMessage(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public static ResultMessage success() {
ResultMessage result = new ResultMessage();
result.setResultCode(ResultCode.SUCCESS);
return result;
}
public static ResultMessage success(Object data) {
ResultMessage result = new ResultMessage();
result.setResultCode(ResultCode.SUCCESS);
result.setData(data);
return result;
}
public static ResultMessage failure(ResultCode resultCode) {
ResultMessage result = new ResultMessage();
result.setResultCode(resultCode);
return result;
}
public static ResultMessage failure(ResultCode resultCode, Object data) {
ResultMessage result = new ResultMessage();
result.setResultCode(resultCode);
result.setData(data);
return result;
}
public void setResultCode(ResultCode code) {
this.code = code.code();
this.msg = code.message();
}
}
然后同时修改全局异常处理的响应码设置方式:
@ExceptionHandler(APIException.class)
public ResultVO<String> APIExceptionHandler(APIException e) {
// 注意哦,这里传递的响应码枚举
return new ResultVO<>(ResultCode.FAILED, e.getMsg());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 注意哦,这里传递的响应码枚举
return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
}
这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!
5.5、全局异常处理器
以前我们遇到异常时,第一时间想到的应该是try..catch..finnal吧,不过这种方式会导致大量代码重复,维护困难,逻辑臃肿等问题,这不是我们想要的结果。
今天我们要用的全局异常处理方式,用起来是比较简单的。首先新增一个类,增加@RestControllerAdvice注解。
@RestControllerAdvice
public class CustomerExceptionHandler {
}
如果我们有想要拦截的异常类型,就新增一个方法,使用@ExceptionHandler注解修饰,注解参数为目标异常类型。
例如:controller中接口发生Exception异常时,就会进入到Execption方法中进行捕获,将杂乱的异常信息,转换成指定格式后交给ResponseAdvice方法进行统一格式封装并返回给前端小伙伴。
@RestControllerAdvice
@Slf4j
public class CustomerExceptionHandler {
@ExceptionHandler(AuthException.class)
public String ErrorHandler(AuthorizationException e) {
log.error("没有通过权限验证!", e);
return "没有通过权限验证!";
}
@ExceptionHandler(Exception.class)
public Result Execption(Exception e) {
log.error("未知异常!", e);
return Result.error(ResultMsgEnum.SERVER_BUSY.getCode(),ResultMsgEnum.SERVER_BUSY.getMessage());
}
}
再次调用接口getUserName查看返回结果,会发现还是有一些问题,因为我们在CustomerExceptionHandler中已经将接口返回结果封装成Result类型,而代码执行到统一结果返回类ResponseAdvice时,又会结果再次封装,就出现了如下问题。
{
"code": 0,
"message": "成功",
"data": {
"code": 503,
"message": "服务器正忙,请稍后再试!",
"data": null
}
}
5.6、统一返回结果处理类最终版
解决上述问题非常简单,只要在beforeBodyWrite中增加一条判断即可。
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
/**
* 是否开启功能 true:开启
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
/**
* 处理返回结果
*/
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
//处理字符串类型数据
if(o instanceof String){
try {
return objectMapper.writeValueAsString(Result.success(o));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
//返回类型是否已经封装
if(o instanceof Result){
return o;
}
return Result.success(o);
}
}
至此,本章的任务就全部讲完,上述代码可以直接引用,不需要其他的配置项,非常推荐引用到自己的项目中。
注意:beforeBodyWrite方法里包装数据无法对String类型的数据直接进行强转,所以要进行特殊处理,这里不讲过多的细节,有兴趣可以自行深入了解。
6、总结
自此整个后端接口基本体系就构建完毕了
- 通过Validator + 自动抛出异常来完成了方便的参数校验
- 通过全局异常处理 + 自定义异常完成了异常操作的规范
- 通过数据统一响应完成了响应数据的规范
- 多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口
- 无效数据清理:对于json响应接口,我们需要遵守对所有值为null的字段不做返回,对前端不关心的数据不做返回(合理的定义VO是很有必要的)。
对于spring boot 我们可以用下配置,实现字段值为null时不做返回。
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=Asia/Shanghai
spring.jackson.default-property-inclusion= non_null
- 必要数据返回
对于添加(POST)、修改(PUT | PATCH)这类方法我们需要立即返回添加或更新后的数据以备前端使用(这是一个约定需要遵守)。
再次强调:项目体系该怎么构建、后端接口该怎么写都没有一个绝对统一的标准,不是说一定要按照本文的来才是最好的,你怎样都可以,本文每一个环节你都可以按照自己的想法来进行编码,本文提供了一个思路!
鸣谢
更多推荐
所有评论(0)