• 字节跳动面试官:SpringBoot统一接口返回和全局异常处理怎么玩?


    现在大多数公司项目框架,基本都是属于前后端分离模式,这种模式会涉及到一个前后端对接问题,无论是对前端或者是后台服务,维护一套完善且规范的接口是非常有必要的,这样不仅能够提高对接效率,也可以让我的代码看起来更加简洁优雅。

    修改前后最大的区别是我们不用在每个接口单独捕获异常,也不用在每个接口都要组装一遍返回参数,可以参考下面这张对比图:

    一、SpringBoot不使用统一返回格式

    默认情况下,SpringBoot会有如下三种返回情况。

    1.1 字符串

    1. @GetMapping("/getUserName")
    2. public String getUserName(){
    3. return "HuaGe";
    4. }

    调用接口返回结果:

    HuaGe

    1.2 实体类

    1. @GetMapping("/getUserName")
    2. public User getUserName(){
    3. return new User("HuaGe",18,"男");
    4. }

    调用接口返回结果:

    1. {
    2. "name": "HuaGe",
    3. "age": "18",
    4. "性别": "男",
    5. }

    1.3异常返回

    1. @GetMapping("/getUserName")
    2. public static String getUserName(){
    3. HashMap hashMap = Maps.newHashMap();
    4. return hashMap.get(0).toString();
    5. }

    模拟一个空指针异常,在不做任何异常处理的情况下,可以看下SpringBoot的默认返回结果:

    1. {
    2. "timestamp": "2021-08-09T06:56:41.524+00:00",
    3. "status": 500,
    4. "error": "Internal Server Error",
    5. "path": "/sysUser/getUserName"
    6. }

    对于上面这几种情况,如果整个项目没有定义统一的返回格式,五个后台开发人员定义五种返回格式,这样不仅代码臃肿,前后端对接效率低,而且还会有一些意向不到的情况发生,比如前端直接显示异常详情等,这给用户的体验是非常差的。

    二、基础玩法

    项目中最常见到的是封装一个工具类,类中定义需要返回的字段信息,把需要返回前端的接口信息,通过该类进行封装,这样就可以解决返回格式不统一的现象了。

    2.1 参数说明

    • code: 状态码,后台可以维护一套统一的状态码;
    • message: 描述信息,接口调用成功/失败的提示信息;
    • data: 返回数据。

    2.2 流程说明

    • 新建Result类
    1. public class Result<T> {
    2. private int code;
    3. private String message;
    4. private T data;
    5. public Result() {}
    6. public Result(int code, String message) {
    7. this.code = code;
    8. this.message = message;
    9. }
    10. /**
    11. * 成功
    12. */
    13. public static <T> Result<T> success(T data) {
    14. Result<T> result = new Result<T>();
    15. result.setCode(ResultMsgEnum.SUCCESS.getCode());
    16. result.setMessage(ResultMsgEnum.SUCCESS.getMessage());
    17. result.setData(data);
    18. return result;
    19. }
    20. /**
    21. * 失败
    22. */
    23. public static <T> Result<T> error(int code, String message) {
    24. return new Result(code, message);
    25. }
    26. }
    • 定义返回状态码
    1. public enum ResultMsgEnum {
    2. SUCCESS(0, "成功"),
    3. FAIL(-1, "失败"),
    4. AUTH_ERROR(502, "授权失败!"),
    5. SERVER_BUSY(503, "服务器正忙,请稍后再试!"),
    6. DATABASE_OPERATION_FAILED(504, "数据库操作失败");
    7. private int code;
    8. private String message;
    9. ResultMsgEnum(int code, String message) {
    10. this.code = code;
    11. this.message = message;
    12. }
    13. public int getCode() {
    14. return this.code;
    15. }
    16. public String getMessage() {
    17. return this.message;
    18. }
    19. }
    • 使用方式

    上面两步定义了数据返回格式和状态码,接下来就要看下在接口中如何使用了。

    1. @GetMapping("/getUserName")
    2. public Result getUserName(){
    3. return Result.success("huage");
    4. }

    调用结果如下,可以看到是我们在Result中定义的参数类型。

    1. {
    2. "code": 0,
    3. "message": "成功",
    4. "data": "huage"
    5. }

    这样写虽然能够满足日常需求,而且我相信很多小伙伴也是这么用的,但是如果我们有大量的接口,然后在每一个接口中都使用Result.success来包装返回信息,会新增很多重复代码,显得不够优雅,甚至都不好意思拿出去显摆。 肯定会有一种方式能够再一次提高代码逼格,实现最优解。

    三、进阶用法

    基本用法学会后,接下来看点究极版本,主要用到如下两个知识点,用法简单,无论是拿出来教学妹,还是指点小姐姐,都是必备技能。

    3.1 类介绍

    3.1类介绍

    3.1 类介绍

    • ResponseBodyAdvice: 该接口是SpringMVC 4.1提供的,它允许在 执行 @ResponseBody后自定义返回数据,用来封装统一数据格式返回;
    • @RestControllerAdvice: 该注解是对Controller进行增强的,可以全局捕获抛出的异常。

    3.2 用法说明

    • 新建ResponseAdvice类;
    • 实现ResponseBodyAdvice接口,实现supports、beforeBodyWrite方法;
    • 该类用于统一封装controller中接口的返回结果。
    1. @RestControllerAdvice
    2. public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    3. @Autowired
    4. private ObjectMapper objectMapper;
    5. /**
    6. * 是否开启功能 true:是
    7. */
    8. @Override
    9. public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
    10. return true;
    11. }
    12. /**
    13. * 处理返回结果
    14. */
    15. @Override
    16. public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
    17. //处理字符串类型数据
    18. if(o instanceof String){
    19. try {
    20. return objectMapper.writeValueAsString(Result.success(o));
    21. } catch (JsonProcessingException e) {
    22. e.printStackTrace();
    23. }
    24. }
    25. return Result.success(o);
    26. }
    27. }

    我们可以通过getUserName接口测试一下,会发现和直接使用Result返回的结果是一致的。

    不过,细心的小伙伴们肯定注意到了,在ResponseAdvice我们全部使用了Result.success(o)来处理结果,对于error类型的结果未做处理。我们来看下,发生异常情况时,返回结果是什么样呢?继续使用上面HashMap空指针异常的代码,测试结果如下:

    1. {
    2. "code": 0,
    3. "message": "成功",
    4. "data": {
    5. "timestamp": "2021-08-09T09:33:26.805+00:00",
    6. "status": 405,
    7. "error": "Method Not Allowed",
    8. "path": "/sysUser/getUserName"
    9. }
    10. }

    虽然格式上没有毛病,但是在code、data字段的具体数据上是不友好或不正确的。不处理好这些事情,会严重影响自己在前端妹妹心中的高大形象的,这是决不能容忍的。

    3.3 全局异常处理器

    以前我们遇到异常时,第一时间想到的应该是try..catch..finnal吧,不过这种方式会导致大量代码重复,维护困难,逻辑臃肿等问题,这不是我们想要的结果。

    今天我们要用的全局异常处理方式,用起来是比较简单的。首先新增一个类,增加@RestControllerAdvice注解,该注解的作用花哥上面已经介绍过,就不再唠叨了。

    1. @RestControllerAdvice
    2. public class CustomerExceptionHandler {
    3. }

    如果我们有想要拦截的异常类型,就新增一个方法,使用@ExceptionHandler注解修饰,注解参数为目标异常类型。

    例如:controller中接口发生Exception异常时,就会进入到Execption方法中进行捕获,将杂乱的异常信息,转换成指定格式后交给ResponseAdvice方法进行统一格式封装并返回给前端小伙伴。

    1. @RestControllerAdvice
    2. @Slf4j
    3. public class CustomerExceptionHandler {
    4. @ExceptionHandler(AuthException.class)
    5. public String ErrorHandler(AuthorizationException e) {
    6. log.error("没有通过权限验证!", e);
    7. return "没有通过权限验证!";
    8. }
    9. @ExceptionHandler(Exception.class)
    10. public Result Execption(Exception e) {
    11. log.error("未知异常!", e);
    12. return Result.error(ResultMsgEnum.SERVER_BUSY.getCode(),ResultMsgEnum.SERVER_BUSY.getMessage());
    13. }
    14. }

    再次调用接口getUserName查看返回结果,会发现还是有一些问题,因为我们在CustomerExceptionHandler中已经将接口返回结果封装成Result类型,而代码执行到统一结果返回类ResponseAdvice时,又会结果再次封装,就出现了如下问题。

    1. {
    2. "code": 0,
    3. "message": "成功",
    4. "data": {
    5. "code": 503,
    6. "message": "服务器正忙,请稍后再试!",
    7. "data": null
    8. }
    9. }

    3.4 统一返回结果处理类最终版

    解决上述问题非常简单,只要在beforeBodyWrite中增加一条判断即可。

    1. @RestControllerAdvice
    2. public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    3. @Autowired
    4. private ObjectMapper objectMapper;
    5. /**
    6. * 是否开启功能 true:开启
    7. */
    8. @Override
    9. public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
    10. return true;
    11. }
    12. /**
    13. * 处理返回结果
    14. */
    15. @Override
    16. public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
    17. //处理字符串类型数据
    18. if(o instanceof String){
    19. try {
    20. return objectMapper.writeValueAsString(Result.success(o));
    21. } catch (JsonProcessingException e) {
    22. e.printStackTrace();
    23. }
    24. }
    25. //返回类型是否已经封装
    26. if(o instanceof Result){
    27. return o;
    28. }
    29. return Result.success(o);
    30. }
    31. }

    至此,本章的任务就全部讲完,上述代码可以直接引用,不需要其他的配置项,非常推荐引用到自己的项目中。

    四、总结

    本章讲解的内容不是很多,主要就两个类的配置,即处理统一返回结果的类ResponseAdvice和异常处理类CustomerExceptionHandler。全文围绕RestControllerAdvice、ResponseBodyAdvice的使用,通过一步步的迭代,最终构建一套通用的代码代码返回格式。本文属于实用知识分享,不知道小伙伴们还有其他好用的统一处理方案嘛,如果有更好的方案,可以分享出来大家一块学习。

     

  • 相关阅读:
    模型推荐丨政务大数据项目案例模型分享
    4.9 nodejs操作多种数据库
    spring创建bean的三种方式
    2015架构案例(五十一)
    Linux I/O 原理和 Zero-copy 技术全面分析
    TYPE-C HUB(拓展坞)乐得瑞LDR6023A快充方案介绍
    2018年五一杯数学建模C题江苏省本科教育质量综合评价解题全过程文档及程序
    轻量级的VsCode为何越用越大?为什么吃了我C盘10G?如何无痛清理VsCode缓存?手把手教你为C盘瘦身
    Dolphin Scheduler 2.x版本部署篇
    DataEase安装升级
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/125391392