SpringBoot如何统一处理返回结果和异常情况

 更新时间:2024年05月17日 14:55:17   作者:sw-code  
这篇文章主要介绍了SpringBoot如何统一处理返回结果和异常情况问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

原因

在springboot项目里我们希望接口返回的数据包含至少三个属性:

  • code:请求接口的返回码,成功或者异常等返回编码,例如定义请求成功。
  • message:请求接口的描述,也就是对返回编码的描述。
  • data:请求接口成功,返回的结果。
{
  "code":20000,
  "message":"成功",
  "data":{
    "info":"测试成功"
  }
}

开发环境

  • 工具:IDEA
  • SpringBoot版本:2.2.2.RELEASE

依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.62</version>
</dependency>
<!-- Spring web -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
<!-- swagger3 -->
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-boot-starter</artifactId>
  <version>3.0.0</version>
</dependency>

创建 SpringBoot 工程#

新建 SpringBoot 项目common_utils,包名com.spring.utils

返回结果统一

创建code枚举

在com.spring.utils中创建 pojo 包,并添加枚举ResultCode

public enum ResultCode {

  /* 成功状态码 */
  SUCCESS(20000, "成功"),
  /* 参数错误 */
  PARAM_IS_INVALID(1001, "参数无效"),
  PARAM_IS_BLANK(1002, "参数为空"),
  PARAM_TYPE_BIND_ERROR(1003, "参数类型错误"),
  PARAM_NOT_COMPLETE(1004, "参数缺失"),
  /* 用户错误 2001-2999*/
  USER_NOTLOGGED_IN(2001, "用户未登录"),
  USER_LOGIN_ERROR(2002, "账号不存在或密码错误"),
  SYSTEM_ERROR(10000, "系统异常,请稍后重试");

  private Integer code;
  private String message;

  private ResultCode(Integer code, String message) {
    this.code = code;
    this.message = message;
  }

  public Integer code() {
    return this.code;
  }
  public String message() {
    return this.message;
  }
}

**可根据项目自定义,结果返回码

创建返回结果实体

在 pojo 包中添加返回结果类R**

@Data
@ApiModel(value = "返回结果实体类", description = "结果实体类")
public class R implements Serializable {

  private static final long serialVersionUID = 1L;

  @ApiModelProperty(value = "返回码")
  private Integer code;

  @ApiModelProperty(value = "返回消息")
  private String message;

  @ApiModelProperty(value = "返回数据")
  private Object data;

  private R() {

  }

  public R(ResultCode resultCode, Object data) {
    this.code = resultCode.code();
    this.message = resultCode.message();
    this.data = data;
  }

  private void setResultCode(ResultCode resultCode) {
    this.code = resultCode.code();
    this.message = resultCode.message();
  }

  // 返回成功
  public static R success() {
    R result = new R();
    result.setResultCode(ResultCode.SUCCESS);
    return result;
  }
  // 返回成功
  public static R success(Object data) {
    R result = new R();
    result.setResultCode(ResultCode.SUCCESS);
    result.setData(data);
    return result;
  }

  // 返回失败
  public static R fail(Integer code, String message) {
    R result = new R();
    result.setCode(code);
    result.setMessage(message);
    return result;
  }
  // 返回失败
  public static R fail(ResultCode resultCode) {
    R result = new R();
    result.setResultCode(resultCode);
    return result;
  }
}

自定义一个注解

新建包annotation,并添加ResponseResult注解类 

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ResponseResult {

}

定义拦截器

新建包interceptor,并添加ResponseResultInterceptorJava类

@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
  //标记名称
  public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {
    // TODO Auto-generated method stub
    if (handler instanceof HandlerMethod) {
      final HandlerMethod handlerMethod = (HandlerMethod) handler;
      final Class<?> clazz = handlerMethod.getBeanType();
      final Method method = handlerMethod.getMethod();
      // 判断是否在类对象上添加了注解
      if (clazz.isAnnotationPresent(ResponseResult.class)) {
        // 设置此请求返回体,需要包装,往下传递,在ResponseBodyAdvice接口进行判断
        request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
      } else if (method.isAnnotationPresent(ResponseResult.class)) {
        request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));
      }
    }
    return true;
  }
}

用于拦截请求,判断 Controller 是否添加了@ResponseResult注解

注册拦截器

新建包config,并添加WebAppConfig配置类

@Configuration
public class WebAppConfig implements WebMvcConfigurer {

    // SpringMVC 需要手动添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        ResponseResultInterceptor interceptor = new ResponseResultInterceptor();
        registry.addInterceptor(interceptor);
        WebMvcConfigurer.super.addInterceptors(registry);
    }
}

方法返回值拦截处理器

新建包handler,并添加ResponseResultHandler配置类,实现ResponseBodyAdvice重写两个方法

import org.springframework.web.method.HandlerMethod;
/**
 * 使用 @ControllerAdvice & ResponseBodyAdvice 
 * 拦截Controller方法默认返回参数,统一处理返回值/响应体
 */
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
   
   // 标记名称
   public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

   
   // 判断是否要执行 beforeBodyWrite 方法,true为执行,false不执行,有注解标记的时候处理返回值
   @Override
   public boolean supports(MethodParameter arg0, Class<? extends HttpMessageConverter<?>> arg1) {
      ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      HttpServletRequest request = sra.getRequest();
      // 判断请求是否有包装标记
      ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
      return responseResultAnn == null ? false : true;
   }
   
   // 对返回值做包装处理,如果属于异常结果,则需要再包装
   @Override
   public Object beforeBodyWrite(Object body, MethodParameter arg1, MediaType arg2,
                          Class<? extends HttpMessageConverter<?>> arg3, ServerHttpRequest arg4, ServerHttpResponse arg5) {
      if (body instanceof R) {
         return (R) body;
      }
      return R.success(body);
   }
}

实现ResponseBodyAdvice重写两个方法

添加@ControllerAdvice注解

测试#

新建包controller,并添加TestController测试类

@RestController
@ResponseResult
public class TestController {

  @GetMapping("/test")
  public Map<String, Object> test() {
    HashMap<String, Object> data = new HashMap<>();
    data.put("info", "测试成功");
    return data;
  }
}

添加@ResponseResult注解

启动项目,在默认端口: 8080

浏览器访问地址:localhost:8080/test

{"code":20000,"message":"成功","data":{"info":"测试成功"}}

总结

1、创建code枚举和返回结果实体类

2、自定义一个注解@ResponseResult

3、定义拦截器,拦截请求,判断Controller上是否添加了@ResponseResult注解。如果添加了注解在request中添加注解标记,往下传递

4、添加@ControllerAdvice注解 ,实现ResponseBodyAdvice接口,并重写两个方法,通过判断request中是否有注解标记,如果有就往下执行,进一步包装。没有就直接返回,不需包装。

问题

1、如果要返回错误结果,这种方法显然不方便

@GetMapping("/fail")
public R error() {
  int res = 0; // 查询结果数
  if( res == 0 ) {
    return R.fail(10001, "没有数据");
  }
  return R.success(res);
}

2、我们需要对错误和异常进行进一步的封装

封装错误和异常结果#

创建错误结果实体#

在包pojo中添加ErrorResult实体类

/**
 * 异常结果包装类
 * @author sw-code
 *
 */
public class ErrorResult {

  private Integer code;

  private String message;

  private String exception;

  public Integer getCode() {
    return code;
  }

  public void setCode(Integer code) {
    this.code = code;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }

  public String getException() {
    return exception;
  }

  public void setException(String exception) {
    this.exception = exception;
  }

  public static ErrorResult fail(ResultCode resultCode, Throwable e, String message) {
    ErrorResult errorResult = ErrorResult.fail(resultCode, e);
    errorResult.setMessage(message);
    return errorResult;
  }

  public static ErrorResult fail(ResultCode resultCode, Throwable e) {
    ErrorResult errorResult = new ErrorResult();
    errorResult.setCode(resultCode.code());
    errorResult.setMessage(resultCode.message());
    errorResult.setException(e.getClass().getName());
    return errorResult;
  }
  public static ErrorResult fail(Integer code, String message) {
    ErrorResult errorResult = new ErrorResult();
    errorResult.setCode(code);
    errorResult.setMessage(message);
    return errorResult;
  }    
}

自定义异常类

在包pojo中添加BizException实体类,继承RuntimeException

@Data
public class BizException extends RuntimeException {

  /**
     * 错误码
     */
  private Integer code;

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

  public BizException() {
    super();
  }

  public BizException(ResultCode resultCode) {
    super(resultCode.message());
    this.code = resultCode.code();
    this.message = resultCode.message();
  }

  public BizException(ResultCode resultCode, Throwable cause) {
    super(resultCode.message(), cause);
    this.code = resultCode.code();
    this.message = resultCode.message();
  }

  public BizException(String message) {
    super(message);
    this.setCode(-1);
    this.message = message;
  }

  public BizException(Integer code, String message) {
    super(message);
    this.code = code;
    this.message = message;
  }

  public BizException(Integer code, String message, Throwable cause) {
    super(message, cause);
    this.code = code;
    this.message = message;
  }

  @Override
  public synchronized Throwable fillInStackTrace() {
    return this;
  }
}

全局异常处理类

在包handler中添加GlobalExceptionHandler,添加@RestControllerAdvice注解

/**
 * 全局异常处理类
 * @RestControllerAdvice(@ControllerAdvice),拦截异常并统一处理
 * @author sw-code
 *
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

  /**
     * 处理自定义的业务异常
     * @param e    异常对象
     * @param request    request
     * @return    错误结果
     */
  @ExceptionHandler(BizException.class)
  public ErrorResult bizExceptionHandler(BizException e, HttpServletRequest request) {
    log.error("发生业务异常!原因是: {}", e.getMessage());
    return ErrorResult.fail(e.getCode(), e.getMessage());
  }

  // 拦截抛出的异常,@ResponseStatus:用来改变响应状态码
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Throwable.class)
  public ErrorResult handlerThrowable(Throwable e, HttpServletRequest request) {
    log.error("发生未知异常!原因是: ", e);
    ErrorResult error = ErrorResult.fail(ResultCode.SYSTEM_ERROR, e);
    return error;
  }

  // 参数校验异常
  @ExceptionHandler(BindException.class)
  public ErrorResult handleBindExcpetion(BindException e, HttpServletRequest request) {
    log.error("发生参数校验异常!原因是:",e);
    ErrorResult error = ErrorResult.fail(ResultCode.PARAM_IS_INVALID, e, e.getAllErrors().get(0).getDefaultMessage());
    return error;
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
    log.error("发生参数校验异常!原因是:",e);
    ErrorResult error = ErrorResult.fail(ResultCode.PARAM_IS_INVALID,e,e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
    return error;
  }    
}

添加注解@RestControllerAdvice(@ControllerAdvice),拦截异常并统一处理

修改方法返回值拦截处理器#

将错误和异常结果也进行统一封装

/**
 * 使用 @ControllerAdvice & ResponseBodyAdvice 
 * 拦截Controller方法默认返回参数,统一处理返回值/响应体
 */
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

  // 标记名称
  public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";


  // 判断是否要执行 beforeBodyWrite 方法,true为执行,false不执行,有注解标记的时候处理返回值
  @Override
  public boolean supports(MethodParameter arg0, Class<? extends HttpMessageConverter<?>> arg1) {
    ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = sra.getRequest();
    // 判断请求是否有包装标记
    ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
    return responseResultAnn == null ? false : true;
  }


  // 对返回值做包装处理,如果属于异常结果,则需要再包装
  @Override
  public Object beforeBodyWrite(Object body, MethodParameter arg1, MediaType arg2,
                                Class<? extends HttpMessageConverter<?>> arg3, ServerHttpRequest arg4, ServerHttpResponse arg5) {
    if (body instanceof ErrorResult) {
      ErrorResult error = (ErrorResult) body;
      return R.fail(error.getCode(), error.getMessage());
    } else if (body instanceof R) {
      return (R) body;
    }
    return R.success(body);
  }
}
测试#
Copy 
@GetMapping("/fail")
public Integer error() {
  int res = 0; // 查询结果数
  if( res == 0 ) {
    throw new BizException("没有数据");
  }
  return res;
}

返回结果

{"code":-1,"message":"没有数据","data":null}

我们无需担心返回类型,如果需要返回错误提示信息,可以直接抛出自定义异常(BizException),并添加自定义错误信息。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not found Error处理)

    MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not f

    这篇文章主要介绍了MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not found)的Error处理方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • Java设计模式之状态模式State Pattern详解

    Java设计模式之状态模式State Pattern详解

    这篇文章主要介绍了Java设计模式之状态模式State Pattern,状态模式允许一个对象在其内部状态改变的时候改变其行为。这个对象看上去就像是改变了它的类一样
    2022-11-11
  • java中Servlet程序下载文件实例详解

    java中Servlet程序下载文件实例详解

    在本篇文章里小编给大家整理的是一篇关于java中Servlet程序下载文件实例内容,有兴趣的朋友们可以学习参考下。
    2021-02-02
  • 详谈jpa中表的@OneToMany等关联关系

    详谈jpa中表的@OneToMany等关联关系

    这篇文章主要介绍了详谈jpa中表的@OneToMany等关联关系,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • SpringCloud2020版本配置与环境搭建教程详解

    SpringCloud2020版本配置与环境搭建教程详解

    这篇文章主要介绍了SpringCloud2020版本配置与环境搭建教程详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • 如何解决struts2日期类型转换

    如何解决struts2日期类型转换

    在Struts2中,一般的内置类型都会在由系统自动进行转换.但是Date类型好像有时候行,有时候不行。比如我这次,本地运行正常,一上测试服务器就不行了。这个转换类是从网上看的:
    2013-03-03
  • java区分绝对路径和相对路径的方法

    java区分绝对路径和相对路径的方法

    这篇文章主要介绍了java区分绝对路径和相对路径的方法,实例分析了java针对路径操作的相关技巧,需要的朋友可以参考下
    2015-04-04
  • Java类加载器ClassLoader源码层面分析讲解

    Java类加载器ClassLoader源码层面分析讲解

    ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了
    2022-09-09
  • springboot-assembly自定义打包全过程

    springboot-assembly自定义打包全过程

    这篇文章主要介绍了springboot-assembly自定义打包全过程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • SpringBoot Security权限控制自定义failureHandler实例

    SpringBoot Security权限控制自定义failureHandler实例

    这篇文章主要为大家介绍了SpringBoot Security权限控制自定义failureHandler实例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11

最新评论