likes
comments
collection
share

Springboot-优雅配置接口响应和统一异常处理

作者站长头像
站长
· 阅读数 12

Springboot-优雅配置接口响应和统一异常处理

一、前言

越来越多的项目开始基于前后端分离的模式进行开发,这对后端接口的报文格式便有了一定的要求。通常,我们会采用JSON格式作为前后端交换数据格式,从而减少沟通成本等。

@PostMapping("/user/add")
public ResponseInfo addUser(@RequestBody @Validated User user) {
    userService.add(user);
    return ResponseInfo.success();
}

我们看看这个代码,有什么问题。 我们返回了统一的封装结果集ResponseInfo 但是后面所有的controller 都这么写不太友好。

  1. 返回内容这么不够明确具体
  2. 所有controller 这么写增加重复工作量

我们可以这么去优化:

Spirng 提供了 ResponseBodyAdvice 接口,支持在消息转换器执行转换之前,对接口的返回结果进行处理,再结合 @ControllerAdvice 注解即可轻松支持上述功能。

二、统一响应配置

准备文件:

ResponseCodeEnums.java 响应类型枚举类

ResponseInfo.java 响应数据封装类

ResponseResultHandler.java 响应信息增强类

开始

ResponseCodeEnums.java

@Getter
@AllArgsConstructor
public enum ResponseCodeEnums {
    SUCCESS(200, "success"),
    FAIL(500, "failed"),

    HTTP_STATUS_200(200, "ok"),
    HTTP_STATUS_400(400, "request error"),
    HTTP_STATUS_401(401, "no authentication"),
    HTTP_STATUS_403(403, "no authorities"),
    HTTP_STATUS_500(500, "server error");

    private final int code;

    private final String message;
}

ResponseInfo.java

@Data
public class ResponseInfo<T> {
    /**
     * 状态码
     */
    protected int code;

    /**
     * 响应信息
     */
    protected String message;

    /**
     * 返回数据
     */
    private T data;

    public static <T> ResponseInfo<T> success() {
        return new ResponseInfo<>();
    }

    public static <T> ResponseInfo<T> success(T data) {
        return new ResponseInfo<>(data);
    }

    public static <T> ResponseInfo<T> fail(String message) {
        return new ResponseInfo<>(ResponseCodeEnums.FAIL.getCode(), message);
    }

    public ResponseInfo() {
        this.code = ResponseCodeEnums.SUCCESS.getCode();
        this.message = ResponseCodeEnums.SUCCESS.getMessage();
    }

    public ResponseInfo(ResponseCodeEnums statusEnums) {
        this.code = statusEnums.getCode();
        this.message = statusEnums.getMessage();
    }

    /**
     * 若没有数据返回,可以人为指定状态码和提示信息
     */
    public ResponseInfo(int code, String msg) {
        this.code = code;
        this.message = msg;
    }

    /**
     * 有数据返回时,状态码为200,默认提示信息为“操作成功!”
     */
    public ResponseInfo(T data) {
        this.data = data;
        this.code = ResponseCodeEnums.SUCCESS.getCode();
        this.message = ResponseCodeEnums.SUCCESS.getMessage();
    }

    /**
     * 有数据返回,状态码为 200,人为指定提示信息
     */
    public ResponseInfo(T data, String msg) {
        this.data = data;
        this.code = ResponseCodeEnums.SUCCESS.getCode();
        this.message = msg;
    }
}

ResponseResultHandler.java

@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        log.info("supports:{}", returnType.getDeclaringClass().getName());
        /**
         * 排除swagger-ui请求返回数据增强
         */
        return !returnType.getDeclaringClass().getName().contains("springfox");
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 如果是ResponseInfo数据类型就直接返回
        if (body instanceof ResponseInfo) {
            return body;
        }
        // 如果是空,则返回成功
        else if (body == null) {
            return ResponseInfo.success();
        } 
        // 如果是异常类型就直接返回
        else if (body instanceof Exception) {
            return body;
        } 
        // 如果是String类型则直接返回String类型
        else if (body instanceof String) {
            return body;
        } 
        // 返回封装后的数据
        else {
            return ResponseInfo.success(body);
        }
    }
}

测试

@RestController
public class TestController {

    @GetMapping("/test")
    public void hello(){}
}

结果

{
  "code": 200,
  "message": "success",
  "data": null
}

OK,成功地响应了结果,到此配置完成,下面开始配置统一异常处理。

三、全局异常配置

在日常开发中,我们通过try...catch的形式捕获异常,并进行处理。在SpringBoot中,我们可以通过RestControllerAdvice注解来定义全局异常处理,这样就无需每处都try...catch了。

示例:

@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {

  /**
   * 参数格式异常处理
   */
  @ExceptionHandler({IllegalArgumentException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ResponseInfo<String> badRequestException(IllegalArgumentException ex) {
    log.error("参数格式不合法:{}", ex.getMessage());
    return new ResponseInfo<>(HttpStatus.BAD_REQUEST.value() + "", "参数格式不符!");
  }

  /**
   * 权限不足异常处理
   */
  @ExceptionHandler({AccessDeniedException.class})
  @ResponseStatus(HttpStatus.FORBIDDEN)
  public ResponseInfo<String> badRequestException(AccessDeniedException ex) {
    return new ResponseInfo<>(HttpStatus.FORBIDDEN.value() + "", ex.getMessage());
  }

  /**
   * 参数缺失异常处理
   */
  @ExceptionHandler({MissingServletRequestParameterException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ResponseInfo<String> badRequestException(Exception ex) {
    return new ResponseInfo<>(HttpStatus.BAD_REQUEST.value() + "", "缺少必填参数!");
  }

  /**
   * 空指针异常
   */
  @ExceptionHandler(NullPointerException.class)
  @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseInfo<String> handleTypeMismatchException(NullPointerException ex) {
    log.error("空指针异常,{}", ex.getMessage());
    return ResponseInfo.fail("空指针异常");
  }

  @ExceptionHandler(Exception.class)
  @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseInfo<String> handleUnexpectedServer(Exception ex) {
    log.error("系统异常:", ex);
    return ResponseInfo.fail("系统发生异常,请联系管理员");
  }

  /**
   * 系统异常处理
   */
  @ExceptionHandler(Throwable.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ResponseInfo<String> exception(Throwable throwable) {
    log.error("系统异常", throwable);
    return new ResponseInfo<>(HttpStatus.INTERNAL_SERVER_ERROR.value() + "系统异常,请联系管理员!");
  }
}

在上述方法中,对一些常见的异常进行了统一处理。通常情况下,根据业务需要还会定义业务异常,并对业务异常进行处理,大家可以根据自己项目中异常的使用情况进行拓展。

关于@RestControllerAdvice的几点说明:

  • @RestControllerAdvice注解包含了@Component注解,会把被注解的类作为组件交给Spring来管理。
  • @RestControllerAdvice注解包含了@ResponseBody注解,异常处理完之后给调用方输出一个JSON格式的封装数据。
  • @RestControllerAdvice注解有一个basePackages属性,该属性用来拦截哪个包中的异常信息,一般不指定,拦截项目工程中的所有异常。
  • 在方法上通过@ExceptionHandler注解来指定具体的异常,在方法中处理该异常信息,最后将结果通过统一的JSON结构体返回给调用者。

测试:

@RestController
public class TestController {

    @GetMapping("/test")
    public void hello(){
        int i = 1 / 0;
    }
}

结果:

{
  "code": 500,
  "message": "系统发生异常,请联系管理员",
  "data": null
}

可以看到统一异常处理对空指针异常进行了拦截处理,并返回了ExceptionHandlerAdvice中定义的统一报文格式。

四、总结

在使用SpringBoot或其他项目中,统一的报文格式和统一的异常处理都是必须的。本篇文章介绍了基于SpringBoot的实现,如果你的项目中采用了其他的技术栈,则可考虑对应的处理方式。同时,日常中很多类似的功能都可以统一进行处理,避免大量无效的硬编码,提高工作效率。