springboot优雅的统一返回格式 + 全局异常处理(包括404等异常) ...

打印 上一主题 下一主题

主题 558|帖子 558|积分 1674

目录

1.自定义枚举类
  1. public enum ReturnCode {
  2.     RC200(200, "ok"),
  3.     RC400(400, "请求失败,参数错误,请检查后重试。"),
  4.     RC404(404, "未找到您请求的资源。"),
  5.     RC405(405, "请求方式错误,请检查后重试。"),
  6.     RC500(500, "操作失败,服务器繁忙或服务器错误,请稍后再试。");
  7.     // 自定义状态码
  8.     private final int code;
  9.     // 自定义描述
  10.     private final String msg;
  11.     ReturnCode(int code, String msg) {
  12.         this.code = code;
  13.         this.msg = msg;
  14.     }
  15.     public int getCode() {
  16.         return code;
  17.     }
  18.     public String getMsg() {
  19.         return msg;
  20.     }
  21. }
复制代码
该枚举类为我们和前端约定好的返回状态码和描述信息,可根据自己的需求修改状态码和描述
2.自定义统一返回格式类
  1. @Data
  2. public class R<T> {
  3.     private Integer code; //状态码
  4.     private String msg; //提示信息
  5.     private T data; //数据
  6.     private long timestamp;//接口请求时间
  7.     public R() {
  8.         this.timestamp = System.currentTimeMillis();
  9.     }
  10.     public static <T> R<T> success(T data) {
  11.         R<T> r = new R<>();
  12.         r.setCode(ReturnCode.RC200.getCode());
  13.         r.setMsg(ReturnCode.RC200.getMsg());
  14.         r.setData(data);
  15.         return r;
  16.     }
  17.     public static <T> R<T> error(int code, String msg) {
  18.         R<T> r = new R<>();
  19.         r.setCode(code);
  20.         r.setMsg(msg);
  21.         r.setData(null);
  22.         return r;
  23.     }
  24. }
复制代码
@Data注解为Lombok工具类库中的注解,提供类的get、set、equals、hashCode、canEqual、toString方法,使用时需配置Lombok,如不配置请手动生成相关方法。
我们返回的信息至少包括code、msg、data三部分,其中code是我们后端和前端约定好的状态码,msg为提示信息,data为返回的具体数据,没有返回数据则为null。除了这三部分外,你还可以定义一些其他字段,比如请求时间timestamp。
定义了统一返回类后,controller层返回数据时统一使用R.success()方法封装。
  1. @RestController
  2. @RequestMapping("/test")
  3. public class TestController {
  4.     @PostMapping("/test1")
  5.     public R<List<Student>> getStudent() {
  6.         ArrayList<Student> list = new ArrayList<>();
  7.         Student student1 = new Student();
  8.         student1.setId(1);
  9.         student1.setName("name1");
  10.         Student student2 = new Student();
  11.         student2.setId(2);
  12.         student2.setName("name2");
  13.         list.add(student1);
  14.         list.add(student2);
  15.         return R.success(list);
  16.     }
  17. }
  18. @Data
  19. class Student {
  20.     private Integer id;
  21.     private String name;
  22. }
复制代码
例如在以上代码中,我们的需求是查询学生信息,我们调用这个test1接口就返回了以下的结果:
  1. {
  2.     "code": 200,
  3.     "msg": "ok",
  4.     "data": [
  5.         {
  6.             "id": 1,
  7.             "name": "name1"
  8.         },
  9.         {
  10.             "id": 2,
  11.             "name": "name2"
  12.         }
  13.     ],
  14.     "timestamp": 1692805971309
  15. }
复制代码

到这里我们已经基本实现了统一返回格式,但是上面这种实现方式也有一个缺点,就是每次返回数据的时候都需要调用R.success()方法,非常麻烦,我们希望能够在controller层里直接返回我们实际的数据,即data字段中的内容,然后自动帮我们封装到R.success()之中,因此我们需要一种更高级的方法。
3.统一返回格式的高级实现

我们需要利用springboot的ResponseBodyAdvice类来实现这个功能,ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体
  1. /**
  2. * 拦截controller返回值,封装后统一返回格式
  3. */
  4. @RestControllerAdvice
  5. public class ResponseAdvice implements ResponseBodyAdvice<Object> {
  6.     @Autowired
  7.     private ObjectMapper objectMapper;
  8.     @Override
  9.     public boolean supports(MethodParameter returnType, Class converterType) {
  10.         return true;
  11.     }
  12.     @SneakyThrows
  13.     @Override
  14.     public Object beforeBodyWrite(Object o, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
  15.         //如果Controller返回String的话,SpringBoot不会帮我们自动封装而直接返回,因此我们需要手动转换成json。
  16.         if (o instanceof String) {
  17.             return objectMapper.writeValueAsString(R.success(o));
  18.         }
  19.         //如果返回的结果是R对象,即已经封装好的,直接返回即可。
  20.         //如果不进行这个判断,后面进行全局异常处理时会出现错误
  21.         if (o instanceof R) {
  22.             return o;
  23.         }
  24.         return R.success(o);
  25.     }
  26. }
复制代码
@RestControllerAdvice是@RestController注解的增强,可以实现三个方面的功能:

  • 全局异常处理
  • 全局数据绑定
  • 全局数据预处理
经过上面的处理后,我们就不需要在controller层使用R.success()进行封装了,直接返回原始数据,springboot就会帮我们自动封装。
  1. @RestController
  2. @RequestMapping("/test")
  3. public class TestController {
  4.     @PostMapping("/test1")
  5.     public List<Student> getStudent() {
  6.         ArrayList<Student> list = new ArrayList<>();
  7.         Student student1 = new Student();
  8.         student1.setId(1);
  9.         student1.setName("name1");
  10.         Student student2 = new Student();
  11.         student2.setId(2);
  12.         student2.setName("name2");
  13.         list.add(student1);
  14.         list.add(student2);
  15.         return list;
  16.     }
  17. }
  18. @Data
  19. class Student {
  20.     private Integer id;
  21.     private String name;
  22. }
复制代码
此时我们调用接口返回的数据依然是自定义统一返回格式的json数据
  1. {
  2.     "code": 200,
  3.     "msg": "ok",
  4.     "data": [
  5.         {
  6.             "id": 1,
  7.             "name": "name1"
  8.         },
  9.         {
  10.             "id": 2,
  11.             "name": "name2"
  12.         }
  13.     ],
  14.     "timestamp": 1692805971325
  15. }
复制代码
需要注意的是,即使我们controller层的接口返回类型是void,ResponseBodyAdvice类依然会帮我们自动封装,其中data字段为null。返回的格式如下:
  1. {
  2.     "code": 200,
  3.     "msg": "ok",
  4.     "data": null,
  5.     "timestamp": 1692805971336
  6. }
复制代码
4.全局异常处理

如果我们不做统一异常处理,当后端出现异常时,返回的数据就变成了下面这样:
后端接口:
  1. @RestController
  2. @RequestMapping("/test")
  3. public class TestController {
  4.     @PostMapping("/test1")
  5.     public String getStudent() {
  6.         int i = 1/0;
  7.         return "hello";
  8.     }
  9. }
复制代码
返回json:
  1. {
  2.     "code": 200,
  3.     "msg": "ok",
  4.     "data": {
  5.         "timestamp": "2023-08-23T16:13:57.818+00:00",
  6.         "status": 500,
  7.         "error": "Internal Server Error",
  8.         "path": "/test/test1"
  9.     },
  10.     "timestamp": 1692807237832
  11. }
复制代码
code返回了200,又在data中显示500错误,这显然不是我们想要的结果,我们想要的结果应该时code返回500,data返回null。解决的方式有很多,你可以通过try catch的方式来捕获,但是我们并不知道什么时候会出现异常,而且手动写try catch并不方便。因此我们需要进行全局异常处理 。
  1. @Slf4j
  2. @RestControllerAdvice
  3. @ResponseBody
  4. public class RestExceptionHandler {
  5.     /**
  6.      * 处理异常
  7.      *
  8.      * @param e otherException
  9.      * @return
  10.      */
  11.     @ExceptionHandler(Exception.class)
  12.     public R<String> exception(Exception e) {
  13.         log.error("异常 exception = {}", e.getMessage(), e);
  14.         return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
  15.     }
  16. }
复制代码
说明:

  • @RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器
  • @ExceptionHandler,统一处理某一类异常,比如要获取空指针异常可以@ExceptionHandler(NullPointerException.class)
除此之外,你还可以使用@ResponseStatus来指定客户端收到的http状态码,如@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)则客户端收到的http状态码为500。如果不指定,则默认返回200。在这里我们并没有指定,因此我们的请求返回的http状态码全部是200,当出现异常时,我们可以修改统一返回格式中code的状态码,来表明具体情况。
具体效果如下:
接口:
  1. @RestController
  2. @RequestMapping("/test")
  3. public class TestController {
  4.     @PostMapping("/test1")
  5.     public void test() {
  6.         int i = 1/0; //发生除0异常
  7.     }
  8. }
复制代码
返回json:
  1. {
  2.     "code": 500,
  3.     "msg": "操作失败,服务器繁忙或服务器错误,请稍后再试。",
  4.     "data": null,
  5.     "timestamp": 1692808061062
  6. }
复制代码
基本上实现了我们的需求。
5.更优雅的全局异常处理

在上面的全局异常处理中,我们直接捕获了Exception.class,无论什么异常都统一处理,但实际上我们需要根据不同的异常进行不同的处理,如空指针异常可能是前端传参错误,以及我们的自定义异常等。
自定义异常如下:
  1. @Getter
  2. @Setter
  3. public class BusinessException extends RuntimeException {
  4.     private int code;
  5.     private String msg;
  6.     public BusinessException() {
  7.     }
  8.     public BusinessException(ReturnCode returnCode) {
  9.         this(returnCode.getCode(),returnCode.getMsg());
  10.     }
  11.     public BusinessException(int code, String msg) {
  12.         super(msg);
  13.         this.code = code;
  14.         this.msg = msg;
  15.     }
  16. }
复制代码
注:@Getter和@Setter分别提供了get和set方法,同样需要Lombok依赖。
我们在全局异常处理中可以使用@ExceptionHandler指定异常类型,分别处理不同的异常
  1. @Slf4j
  2. @RestControllerAdvice
  3. @ResponseBody
  4. public class RestExceptionHandler {
  5.     /**
  6.      * 处理自定义异常
  7.      *
  8.      * @param e BusinessException
  9.      * @return
  10.      */
  11.     @ExceptionHandler(BusinessException.class)
  12.     public R<String> businessException(BusinessException e) {
  13.         log.error("业务异常 code={}, BusinessException = {}", e.getCode(), e.getMessage(), e);
  14.         return R.error(e.getCode(), e.getMsg());
  15.     }
  16.     /**
  17.      * 处理空指针的异常
  18.      *
  19.      * @param e NullPointerException
  20.      * @return
  21.      * @description 空指针异常定义为前端传参错误,返回400
  22.      */
  23.     @ExceptionHandler(NullPointerException.class)
  24.     public R<String> nullPointerException(NullPointerException e) {
  25.         log.error("空指针异常 NullPointerException ", e);
  26.         return R.error(ReturnCode.RC400.getCode(), ReturnCode.RC400.getMsg());
  27.     }
  28.     /**
  29.      * 处理其他异常
  30.      *
  31.      * @param e otherException
  32.      * @return
  33.      */
  34.     @ExceptionHandler(Exception.class)
  35.     public R<String> exception(Exception e) {
  36.         log.error("未知异常 exception = {}", e.getMessage(), e);
  37.         return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
  38.     }
  39. }
复制代码
需要注意的是一个异常只会被捕获一次,比如空指针异常,只会被第二个方法捕获,处理之后不会再被最后一个方法捕获。当上面两个方法都没有捕获到指定异常时,最后一个方法指定了@ExceptionHandler(Exception.class)就可以捕获到所有的异常,相当于if  elseif  else语句
分别测试自定义异常、空指针异常以及其他异常:

  • 自定义异常
    接口:
  1. @RestController
  2. @RequestMapping("/test")
  3. public class TestController {
  4.     @PostMapping("/test1")
  5.     public void test() {
  6.         throw new BusinessException(ReturnCode.RC500.getCode(),"发生异常");
  7.     }
  8. }
复制代码
​        返回json:
  1. {
  2.     "code": 500,
  3.     "msg": "发生异常",
  4.     "data": null,
  5.     "timestamp": 1692809118244
  6. }
复制代码

  • 空指针异常:
    接口:
    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4.     @PostMapping("/test1")
    5.     public void test(int id, String name) {
    6.         System.out.println(id + name);
    7.         boolean equals = name.equals("11");
    8.     }
    9. }
    复制代码
    请求:

    返回json:
    1. {
    2.     "code": 400,
    3.     "msg": "请求失败,参数错误,请检查后重试。",
    4.     "data": null,
    5.     "timestamp": 1692809456917
    6. }
    复制代码
  • 其他异常:
    接口:
    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4.     @PostMapping("/test1")
    5.     public void test() {
    6.        throw new RuntimeException("发生异常");
    7.     }
    8. }
    复制代码
    返回json:
    1. {
    2.     "code": 500,
    3.     "msg": "操作失败,服务器繁忙或服务器错误,请稍后再试。",
    4.     "data": null,
    5.     "timestamp": 1692809730234
    6. }
    复制代码
6.处理404错误

即使我们配置了全局异常处理,当出现404 not found等4xx错误时,依然会出现意外情况:

返回json:
  1. {
  2.     "code": 200,
  3.     "msg": "ok",
  4.     "data": {
  5.         "timestamp": "2023-08-23T17:01:15.102+00:00",
  6.         "status": 404,
  7.         "error": "Not Found",
  8.         "path": "/test/nullapi"
  9.     },
  10.     "timestamp": 1692810075116
  11. }
复制代码
我们可以看到发生404错误时控制台并没有报异常,原因是404错误并不属于异常,全局异常处理自然不会去捕获并处理。因此我们的解决方法是当出现4xx错误时,让springboot直接报异常,这样我们的全局异常处理就可以捕获到。
在application.yml配置文件增加以下配置项:
  1. #  当HTTP状态码为4xx时直接抛出异常
  2. spring:
  3.   mvc:
  4.     throw-exception-if-no-handler-found: true
  5.   #  关闭默认的静态资源路径映射
  6.   web:
  7.     resources:
  8.       add-mappings: false
复制代码
现在当我们再次请求一个不存在的接口是,控制台会报NoHandlerFoundException异常,然后被全局异常处理捕获到并统一返回
返回json:
  1. {
  2.     "code": 500,
  3.     "msg": "操作失败,服务器繁忙或服务器错误,请稍后再试。",
  4.     "data": null,
  5.     "timestamp": 1692810621545
  6. }
复制代码

当发生404错误时,http的状态码依然是200,同时code返回的是500,这不利于用户或者前端人员的理解,因此我们可以在全局异常处理中单独对NoHandlerFoundException异常进行处理。
  1. /**
  2.      * 处理404异常
  3.      *
  4.      * @param e NoHandlerFoundException
  5.      * @return
  6.      */
  7.     @ExceptionHandler(NoHandlerFoundException.class)
  8.     @ResponseStatus(HttpStatus.NOT_FOUND)//指定http状态码为404
  9.     public R<String> noHandlerFoundException(HttpServletRequest req, Exception e) {
  10.         log.error("404异常 NoHandlerFoundException, method = {}, path = {} ", req.getMethod(), req.getServletPath(), e);
  11.         return R.error(ReturnCode.RC404.getCode(), ReturnCode.RC404.getMsg());
  12.     }
复制代码
在上面中,我们使用@ExceptionHandler(NoHandlerFoundException.class)单独捕获处理404异常,同时使用@ResponseStatus(HttpStatus.NOT_FOUND)指定http返回码为404,我们统一返回格式中code也设置为404
现在当我们再次发生404异常时,返回json如下:
  1. {
  2.     "code": 404,
  3.     "msg": "未找到您请求的资源。",
  4.     "data": null,
  5.     "timestamp": 1692811047868
  6. }
复制代码

控制台日志:

同理我们还可以为405错误进行配置,405错误对应的异常为HttpRequestMethodNotSupportedException
  1. /**
  2.      * 处理请求方式错误(405)异常
  3.      *
  4.      * @param e HttpRequestMethodNotSupportedException
  5.      * @return
  6.      */
  7.     @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
  8.     @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)//指定http状态码为405
  9.     public R<String> HttpRequestMethodNotSupportedException(HttpServletRequest req, Exception e) {
  10.         log.error("请求方式错误(405)异常 HttpRequestMethodNotSupportedException, method = {}, path = {}", req.getMethod(), req.getServletPath(), e);
  11.         return R.error(ReturnCode.RC405.getCode(), ReturnCode.RC405.getMsg());
  12.     }
复制代码
返回json:
  1. {
  2.     "code": 405,
  3.     "msg": "请求方式错误,请检查后重试。",
  4.     "data": null,
  5.     "timestamp": 1692811288226
  6. }
复制代码

控制台日志:

全局异常处理RestExceptionHandler类完整代码如下:
  1. package com.tuuli.config;
  2. import com.tuuli.common.BusinessException;
  3. import com.tuuli.common.R;
  4. import com.tuuli.common.ReturnCode;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.web.HttpRequestMethodNotSupportedException;
  8. import org.springframework.web.bind.annotation.ExceptionHandler;
  9. import org.springframework.web.bind.annotation.ResponseBody;
  10. import org.springframework.web.bind.annotation.ResponseStatus;
  11. import org.springframework.web.bind.annotation.RestControllerAdvice;
  12. import org.springframework.web.servlet.NoHandlerFoundException;
  13. import javax.servlet.http.HttpServletRequest;
  14. /**
  15. * 全局异常处理
  16. */
  17. @Slf4j
  18. @RestControllerAdvice
  19. @ResponseBody
  20. public class RestExceptionHandler {
  21.     /**
  22.      * 处理自定义异常
  23.      *
  24.      * @param e BusinessException
  25.      * @return
  26.      */
  27.     @ExceptionHandler(BusinessException.class)
  28.     public R<String> businessException(BusinessException e) {
  29.         log.error("业务异常 code={}, BusinessException = {}", e.getCode(), e.getMessage(), e);
  30.         return R.error(e.getCode(), e.getMsg());
  31.     }
  32.     /**
  33.      * 处理空指针的异常
  34.      *
  35.      * @param e NullPointerException
  36.      * @return
  37.      * @description 空指针异常定义为前端传参错误,返回400
  38.      */
  39.     @ExceptionHandler(value = NullPointerException.class)
  40.     public R<String> nullPointerException(NullPointerException e) {
  41.         log.error("空指针异常 NullPointerException ", e);
  42.         return R.error(ReturnCode.RC400.getCode(), ReturnCode.RC400.getMsg());
  43.     }
  44.     /**
  45.      * 处理404异常
  46.      *
  47.      * @param e NoHandlerFoundException
  48.      * @return
  49.      */
  50.     @ExceptionHandler(NoHandlerFoundException.class)
  51.     @ResponseStatus(HttpStatus.NOT_FOUND)
  52.     public R<String> noHandlerFoundException(HttpServletRequest req, Exception e) {
  53.         log.error("404异常 NoHandlerFoundException, method = {}, path = {} ", req.getMethod(), req.getServletPath(), e);
  54.         return R.error(ReturnCode.RC404.getCode(), ReturnCode.RC404.getMsg());
  55.     }
  56.     /**
  57.      * 处理请求方式错误(405)异常
  58.      *
  59.      * @param e HttpRequestMethodNotSupportedException
  60.      * @return
  61.      */
  62.     @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
  63.     @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
  64.     public R<String> HttpRequestMethodNotSupportedException(HttpServletRequest req, Exception e) {
  65.         log.error("请求方式错误(405)异常 HttpRequestMethodNotSupportedException, method = {}, path = {}", req.getMethod(), req.getServletPath(), e);
  66.         return R.error(ReturnCode.RC405.getCode(), ReturnCode.RC405.getMsg());
  67.     }
  68.     /**
  69.      * 处理其他异常
  70.      *
  71.      * @param e otherException
  72.      * @return
  73.      */
  74.     @ExceptionHandler(Exception.class)
  75.     public R<String> exception(Exception e) {
  76.         log.error("未知异常 exception = {}", e.getMessage(), e);
  77.         return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
  78.     }
  79. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

圆咕噜咕噜

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表