Spring Boot统一结果响应和异常处理

在开发中,我们会经常使用公司或其他平台的各种接口,每个接口的功能各不相同,但是它们的响应结果基本上是一致的,都会包含code(响应码)、msg(错误信息)、data(真实数据)这三部分,有些公司会使用Map来返回这些信息,但是需要编写重复性的代码,不太优雅,而Spring为我们提供了能够简化代码编写的功能,下面就来尝试一下吧

项目准备

  1. 创建一个SpringBoot项目并引入spring-boot-starter-web依赖,分别创建controller和service

    • UserController.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      @RequestMapping(value = "/user")
      @RestController
      public class UserController {

      @Resource
      private UserService userService;

      @GetMapping(value = "/id/{id}")
      public UserDO getById(@PathVariable(value = "id") Long id) {
      UserDO user = userService.getById(id);

      return user;
      }
      }
    • UserService.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Service
      public class UserService {

      @Resource
      private UserMapper userMapper;

      public UserDO getById(Long id) {
      return userMapper.getById(id);
      }

      }
    • UserMapper.xml省略(可以直接硬编码返回,省略数据库配置)

  2. 测试controller返回结果

统一结果响应

  1. 创建CommonResponseCommonResponseAdvice

    • CommonResponse.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      @Getter
      @Setter
      @ToString
      public class CommonResponse<T> {

      /**
      * 响应是否成功:
      * true: 成功
      * false: 失败
      */
      private Boolean success;

      /**
      * 错误码
      */
      private int errCode;

      /**
      * 错误信息
      */
      private String errMsg;

      /**
      * 响应数据
      */
      private T data;

      public CommonResponse(Boolean success) {
      this.success = success;
      }

      public CommonResponse(Boolean success, T data) {
      this.success = success;
      this.data = data;
      }

      public CommonResponse(Boolean success, int errCode, String errMsg) {
      this.success = success;
      this.errCode = errCode;
      this.errMsg = errMsg;
      }

      /**
      *
      * @desc 响应成功无返回数据
      * @author codecho
      * @date 2021-12-05 15:22:07
      */
      public static <T> CommonResponse<T> success() {
      return new CommonResponse<>(true);
      }

      /**
      *
      * @desc 响应成功并返回数据
      * @author codecho
      * @date 2021-12-05 15:22:28
      * @param data 响应数据
      */
      public static <T> CommonResponse<T> success(T data) {
      return new CommonResponse<>(true, data);
      }

      /**
      *
      * @desc 响应失败,无错误码,有错误信息
      * @author codecho
      * @date 2021-12-05 17:37:39
      * @param errMsg 错误信息
      */
      public static<T> CommonResponse<T> fail(String errMsg) {
      return new CommonResponse<>(false, -1, errMsg);
      }

      /**
      *
      * @desc 响应失败,有错误码和错误信息
      * @author codecho
      * @date 2021-12-05 15:25:27
      * @param errCode 错误码
      * @param errMsg 错误信息
      */
      public static <T> CommonResponse<T> fail(int errCode, String errMsg) {
      return new CommonResponse<>(false, errCode, errMsg);
      }

      }
    • CommonResponseAdvice.java

      supports方法返回true表示响应结果需要进行重写

      beforeBodyWrite方法对响应结果进行封装

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @RestControllerAdvice(basePackages = {"com.codecho.base.controller"})
      public class CommonResponseAdvice implements ResponseBodyAdvice {

      @Override
      public boolean supports(MethodParameter returnType, Class converterType) {
      return true;
      }

      @Override
      public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
      return CommonResponse.success(body);
      }
      }
  2. 重新运行程序,再次测试controller返回结果

统一异常处理

  1. 当程序出现异常时,controller返回的结果并不是我们希望获得的

  2. 创建ResponseStatusEnum、CommonExceptionCommonExceptionHandler

    • ResponseStatusEnum.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      public enum ResponseStatusEnum {

      /**
      * 错误
      */
      ERROR_500(500, "服务器未知错误"),
      ERROR_400(400, "错误请求"),
      USER_NOT_FOUND(233, "用户不存在");

      private int code;

      private String msg;

      ResponseStatusEnum(int code, String msg) {
      this.code = code;
      this.msg = msg;
      }

      public int getCode() {
      return code;
      }

      public String getMsg() {
      return msg;
      }
      }
    • CommonException.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      public class CommonException extends RuntimeException {

      private static final long serialVersionUID = 6124960120588564481L;

      @Getter
      private int code;

      @Getter
      private ResponseStatusEnum statusEnum;

      public CommonException(String message) {
      super(message);
      }

      public CommonException(int code, String message) {
      super(message);
      this.code = code;
      }

      public CommonException(ResponseStatusEnum statusEnum) {
      super(statusEnum.getMsg());
      this.code = statusEnum.getCode();
      this.statusEnum = statusEnum;
      }

      }
    • CommonExceptionHandler.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      @Slf4j
      @RestControllerAdvice(basePackages = {"com.codecho.base.controller"})
      public class CommonExceptionHandler {

      /**
      * @desc 处理基础异常
      * @author codecho
      * @date 2022-11-23 15:59:48
      * @param ex 基础异常
      */
      @ExceptionHandler(value = {CommonException.class})
      public CommonResponse exceptionHandler(CommonException ex) {
      // 输出日志
      log.error("CommonException: {}", ex);

      return CommonResponse.fail(ex.getCode(), ex.getMessage());

      }

      // ...处理自定义或常见的异常

      /**
      * @desc 处理其他异常
      * @author codecho
      * @date 2022-11-23 16:00:05
      * @param ex 其他异常
      */
      @ExceptionHandler(value = {Exception.class})
      public CommonResponse exceptionHandler(Exception ex) {
      // 输出日志
      log.error("Exception: {}", ex);

      return CommonResponse.fail(ex.getMessage());
      }

      }
  3. 修改CommonResponseAdvice

    • CommonResponseAdvice.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      @RestControllerAdvice(basePackages = {"com.codecho.base.controller"})
      public class CommonResponseAdvice implements ResponseBodyAdvice {

      private static final String COMMON_RESPONSE = "CommonResponse";

      @Override
      public boolean supports(MethodParameter returnType, Class converterType) {
      return true;
      }

      @Override
      public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
      // 配置统一异常处理或controller返回类型为CommonResponse,直接返回,不需要再次封装
      if (Objects.nonNull(body) && COMMON_RESPONSE.equals(body.getClass().getSimpleName())) {
      return body;
      }

      return CommonResponse.success(body);
      }
      }
  4. 修改UserController的getById方法

    • UserController.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @GetMapping(value = "/{id}")
      public UserDO getById(@PathVariable(value = "id") Long id) {
      UserDO user = userService.getById(id);
      if (null == user) {
      throw new CommonException(ResponseStatusEnum.USER_NOT_FOUND);
      }

      return user;
      }
  5. 重新运行程序,测试controller返回结果

补充

有时候不是所有的接口都必须返回统一的格式,某些情况下我们想要自定义controller的返回,但是配置统一结果响应后,它是对注解@RestControllerAdvice的属性basePackages的包和其子包生效的。如果想要在某些使其不生效,可以考虑使用自定义注解和重写ResponseBodyAdvice接口的supports方法来实现

  1. 创建自定义注解@MyResponse

    • MyResponse.java

      1
      2
      3
      4
      5
      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface MyResponse {

      }
  2. 修改CommonResponseAdvicesupports方法

    • CommonResponseAdvice.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Override
      public boolean supports(MethodParameter returnType, Class converterType) {
      // 如果方法上有@MyResponse注解,返回false,不需要设置统一响应结果
      if (returnType.hasMethodAnnotation(MyResponse.class)) {
      return false;
      }

      return true;
      }
  3. UserController中创建一个带有@MyResponse注解的请求

    • UserController.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @MyResponse
      @GetMapping(value = "/mock")
      public UserDO mockUser() {
      UserDO user = new UserDO();
      user.setUsername("july");
      user.setMobilePhone("18756989090");
      user.setUserState(0);

      return user;
      }
  4. 重新运行程序,测试新的请求

    可以看到,添加自定义注解后,统一结果响应未生效