Spring Boot如何优雅实现结果统一封装和异常统一处理

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 当下基于`Spring Boot`框架开发的系统几乎都是前后端分离的,也都是基于`RESTFUL`风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示

1.概述

当下基于Spring Boot框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示

Restful风格是什么?

RESTFUL(英文:Representational State Transfer,简称REST)可译为"表现层状态转化”,是一种网络应用程序的设计风格和开发方式,是资源定位和资源操作的一种风格。不是标准也不是协议。基于HTTP可以使用 XML 格式定义或 JSON 格式定义。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,使用JSON格式的REST风格的API具有简单、易读、易用的特点。Restful风格最大的特点为:资源、统一接口、URI和无状态。

对于我们Web开发人员而言,restful风格简单来说就是使用一个url地址表示一个唯一的资源。然后把原来的请求参数加入到请求资源地址中。把原来请求的增,删,改,查操作路径标识,改为使用HTTP协议中请求方式GET、POST、PUT、DELETE表示。

传统的方式是:http://127.0.0.1:8080/shepherd/user/add 表示新增用户的接口,需要在路径上加以增删改查标识,如果我们要修改:那么路径是:http://127.0.0.1:8080/shepherd/user/update

但是我们基于restful风格就比较优雅:http://127.0.0.1:8080/shepherd/user,增删改查都可以用这个路径,使用请求方法来进行区别即可,如`post`代表新增,`put`代表修改等。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

2.返回结果统一封装

定义一个统一的标准返回格式,有助于后端接口开发的规范性和通用性,同时也提高了前后端联调的效率,前端通过接收同一返回结构体进行相应映射处理,不用担心每个接口返回的格式都不一样而做一一适配了。

2.1 定义返回统一结构体

@Data
public class ResponseVO<T> implements Serializable {
   

    private Integer code;

    private String msg;

    private T data;

    public ResponseVO() {
   

    }

    public ResponseVO(Integer code, String msg) {
   
        this.code = code;
        this.msg = msg;
    }

    public ResponseVO(Integer code, T data) {
   
        this.code = code;
        this.data = data;
    }

    public ResponseVO(Integer code, String msg, T data) {
   
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    private ResponseVO(ResponseStatusEnum resultStatus, T data) {
   
        this.code = resultStatus.getCode();
        this.msg = resultStatus.getMsg();
        this.data = data;
    }

    /**
     * 业务成功返回业务代码和描述信息
     */
    public static ResponseVO<Void> success() {
   
        return new ResponseVO<Void>(ResponseStatusEnum.SUCCESS, null);
    }

    /**
     * 业务成功返回业务代码,描述和返回的参数
     */
    public static <T> ResponseVO<T> success(T data) {
   
        return new ResponseVO<T>(ResponseStatusEnum.SUCCESS, data);
    }

    /**
     * 业务成功返回业务代码,描述和返回的参数
     */
    public static <T> ResponseVO<T> success(ResponseStatusEnum resultStatus, T data) {
   
        if (resultStatus == null) {
   
            return success(data);
        }
        return new ResponseVO<T>(resultStatus, data);
    }

    /**
     * 业务异常返回业务代码和描述信息
     */
    public static <T> ResponseVO<T> failure() {
   
        return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
    }

    /**
     * 业务异常返回业务代码,描述和返回的参数
     */
    public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus) {
   
        return failure(resultStatus, null);
    }

    /**
     * 业务异常返回业务代码,描述和返回的参数
     */
    public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus, T data) {
   
        if (resultStatus == null) {
   
            return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
        }
        return new ResponseVO<T>(resultStatus, data);
    }

    public static <T> ResponseVO<T> failure(Integer code, String msg) {
   
        return new ResponseVO<T>(code, msg);
    }
}

这里包含了三个字段信息:

  1. code 状态值:由后端统一定义各种返回结果的状态码, 比如说code=200代表接口调用成功
  2. msg 描述:本次接口调用的结果描述,比如说后端抛出的业务异常信息就在这里体现
  3. data 数据:本次返回的数据,泛型类型意味着可以支持任意类型的返回数据

成功返回如下:

{
   
  "code": 200,
  "msg": "OK",
  "data": {
   
    "id": 123,
    "name": "shepherd"
  }
}

业务异常返回如下:

{
   
  "code": 400,
  "msg": "当前用户不存在"
}

按照上面成功返回的示例我们接口定义如下:

    @GetMapping("/test/user")
    public ResponseVO<User> testUser() {
   
        User user = new User();
        user.setId(123l);
        user.setName("shepherd");
        return ResponseVO.success(user);
    }

可以看到接口方法返回类型为ResponseVO<User>,然后通过ResponseVO.success()对返回结果进行包装后返回给前端。这就意味着写一个接口都需要调用ResultData.success()这行代码对结果进行包装,有点重复劳动不够优雅的感觉。还有一种情况,有些项目服务前期为了赶时间开发时没有返回统一结构,等项目上线了有时间之后按照规范需要对后端接口返回结构进行统一,这时候如果复杂的系统已经有成百上千的接口了,如果一个个地像上面说的那样把接口返回类型改为ResponseVO<T>,再用ResponseVO.success()进行结果包装,工作量不小,也比较繁琐。

2.2 高级优雅实现统一结果封装

为了解决上面阐述的问题,我们借助于Spring Boot提供的ResponseBodyAdvice进行了高级实现。

ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。我们在分享 Spring Boot如何对接口参数进行加解密就有提到过这个类进行返回结果参数的加密。

先来看下ResponseBodyAdvice的源码

public interface ResponseBodyAdvice<T> {
   
  /**
  * 是否支持advice功能
  * true 支持,false 不支持
  */
    boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);

   /**
  * 对返回的数据进行处理
  */
    @Nullable
    T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}

所以我们编写一个具体实现类即可:

@RestControllerAdvice
@Slf4j
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
   
    @Resource
    private ObjectMapper objectMapper;

    private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;

    /**
     * 判断类或者方法是否使用了 @ResponseResultBody
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
   
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
    }

    /**
     * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
   
        //如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
        // 当body都为null时,下面的if判断条件都不满足,如果接口返回类似为String,会报错com.shepherd.fast.global.ResponseVO cannot be cast to java.lang.String
        Class<?> returnClass = returnType.getMethod().getReturnType();
        if (body instanceof String || Objects.equals(returnClass, String.class)) {
   
            String value = objectMapper.writeValueAsString(ResponseVO.success(body));
            return value;
        }
        // 防止重复包裹的问题出现
        if (body instanceof ResponseVO) {
   
            return body;
        }
        return ResponseVO.success(body);
    }

}

这里使用到一个自定义注解@ResponseResultBody:

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

}

注入bean

    @Bean
    public ResponseResultBodyAdvice responseResultBodyAdvice() {
   
        return new ResponseResultBodyAdvice();
    }

从上面我们自己定义实现类ResponseResultBodyAdvice#supports()可以看到,只要我们的Controller类或者方法上使用了ResponseResultBody注解,就会执行方法#beforeBodyWrite(),使用ResponseVO对结果进行包装统一返回。

实现类上使用了RestControllerAdvice注解,@RestControllerAdvice是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法,该注解特点如下:

1.通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。

2.注解了@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。

3.@RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。

4.@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。

5.@InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。

6.@ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对

实现类ResponseResultBodyAdvice使用该注解就是满足上面的第3种情况,将拦截作用在所有注解了@RequestMapping的控制器的方法进行判断是否使用了注解@ResponseResultBody,从而对接口结构进行统一ResponseVO包装。

3.全局异常统一处理

使用统一返回结果时,还有一种情况,就是程序由于运行时异常导致报错的结果,有些异常我们可能无法提前预知,不能正常走到我们return的ResponseVO对象返回,因此,我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回给控制层。

使用上面的@ControllerAdvice@ExceptionHandler进行全局异常统一处理:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
   

    /**
     * 全局异常处理
     * @param e
     * @return
     */
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    public ResponseVO exceptionHandler(Exception e){
   
        // 处理业务异常
        if (e instanceof BizException) {
   
            BizException bizException = (BizException) e;
            if (bizException.getCode() == null) {
   
                bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());
            }
            return ResponseVO.failure(bizException.getCode(), bizException.getMessage());
        } else if (e instanceof MethodArgumentNotValidException) {
   
            // 参数检验异常
            MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
            Map<String, String> map = new HashMap<>();
            BindingResult result = methodArgumentNotValidException.getBindingResult();
            result.getFieldErrors().forEach((item)->{
   
                String message = item.getDefaultMessage();
                String field = item.getField();
                map.put(field, message);
            });
            log.error("数据校验出现错误:", e);
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);
        } else if (e instanceof HttpRequestMethodNotSupportedException) {
   
            log.error("请求方法错误:", e);
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");
        } else if (e instanceof MissingServletRequestParameterException) {
   
            log.error("请求参数缺失:", e);
            MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());
        } else if (e instanceof MethodArgumentTypeMismatchException) {
   
            log.error("请求参数类型错误:", e);
            MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());
        } else if (e instanceof NoHandlerFoundException) {
   
            NoHandlerFoundException ex = (NoHandlerFoundException) e;
            log.error("请求地址不存在:", e);
            return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());
        } else {
   
            //如果是系统的异常,比如空指针这些异常
            log.error("【系统异常】", e);
            return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());
        }
    }

}

注入bean

    @Bean
    public GlobalExceptionHandler globalExceptionHandler() {
        return new GlobalExceptionHandler();
    }

通过以上步骤就可以对异常进行全局异常统一处理,这样做的好处不仅是可以对未知异常进行处理之后按照统一结构返回给前端,同时还能对异常处理之后进行error级别的日志输出,这样才能结合logback,log4j2等日志框架写入到日志文件中,以便后续查看异常错误日志排查追踪问题,否则异常信息不会被记录在error日志文件中。

4.总结

基于以上全部内容,我们讲述了如何优雅实现返回结果统一封装和全局异常统一处理,这样可以规范后端接口输出,同时也增强了项目服务的健壮性,可以说这两个统一处理事当下项目服务的必须要求,所以我们得了解一下哦。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
Dubbo Java 应用服务中间件
深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案
本文深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案。通过检查GraalVM版本兼容性、配置反射列表、使用代理类、检查配置文件、禁用不支持的功能、查看日志文件、使用GraalVM诊断工具和调整GraalVM配置等步骤,帮助开发者快速定位并解决问题,确保服务的正常运行。
49 1
|
1月前
|
Java
springboot将list封装成csv文件
springboot将list封装成csv文件
35 4
|
2月前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
2月前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
324 10
|
2月前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
172 6
|
4月前
|
前端开发 小程序 Java
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
本文详细介绍了如何在SpringBoot项目中统一处理接口返回结果及全局异常。首先,通过封装`ResponseResult`类,实现了接口返回结果的规范化,包括状态码、状态信息、返回信息和数据等字段,提供了多种成功和失败的返回方法。其次,利用`@RestControllerAdvice`和`@ExceptionHandler`注解配置全局异常处理,捕获并友好地处理各种异常信息。
1945 0
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
|
4月前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
54 0
Spring高手之路22——AOP切面类的封装与解析
|
2月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
232 2
|
6天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
47 14
|
29天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
48 1
SpringBoot入门(7)- 配置热部署devtools工具