• SpringBoot统一封装controller层返回的结果


    一 前言

    目前前后端分离的项目中,我们在controller层会统一格式封装结果给前端。如果我们在每个方法中手动封装Result,无疑是增加了额外的工作量。
    手动封装Result
    那么有没有一种方式我们只返回它相应的数据。对于结果可以自动帮我们封装,且还可以对某个方法或者某个类下所有方法封装或者不封装。
    答案是肯定的。利用@RestControllerAdviceResponseBodyAdviceResult返回对象统一拦截处理。

    二 @RestControllerAdvice注解和 ResponseBodyAdvice接口说明

    @RestControllerAdvice:是一个组合注解,包含@ControllerAdvice@ResponseBody

    • @ControllerAdvice 捕获controller层中的方法做进一步加强。@ControllerAdvice三种使用场景
    • @ResponseBody 将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML
      @responseBody的使用

    ResponseBodyAdvice :允许在@ResponseBody或ResponseEntity控制器方法执行之后,但在使用HttpMessageConverter编写body之前定制响应。

    简单理解:ResponseBodyAdvice接口是在controller层方法执行之后,在response返回给前端数据之前对reponse的数据进行处理,可以对数据进行统一的处理,从而可以使返回数据格式一致。

    三 具体实现

    源码地址

    3.1 统一返回数据格式代码

    3.1.1 ResponseResult

    @RestControllerAdvice
    public class ResponseResult implements ResponseBodyAdvice<Object> {
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            if (returnType.getDeclaringClass().isAnnotationPresent(ResponseNotIntercept.class)) {
                //若在类中加了@ResponseNotIntercept 则该类中的方法不用做统一的拦截
                return false;
            }
            if (returnType.getMethod().isAnnotationPresent(ResponseNotIntercept.class)) {
                //若方法上加了@ResponseNotIntercept 则该方法不用做统一的拦截
                return false;
            }
            return true;
        }
        
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                      Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                      ServerHttpRequest request, ServerHttpResponse response) {
            if (body instanceof Result) {
                // 提供一定的灵活度,如果body已经被包装了,就不进行包装
                return body;
            }
            if (body instanceof String) {
                //解决返回值为字符串时,不能正常包装
                return JSON.toJSONString(Result.success(body));
            }
            return Result.success(body);
        }
    }
    
    • 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

    说明: 实现ResponseBodyAdvice接口 需要重写supports,beforeBodyWrite方法;
    supports: 是否支持给定的控制器方法返回类型和选定的HttpMessageConverter类型;若不支持则就不会对数据进行做统一处理,就像上面代码,若加了@ResponseNotIntercept注解,则不会进行拦截(@ResponseNotIntercept是自己自定义的一个注解)

    • 参数:
      returnType:返回类型;
      converterType:选择的转换器类型
    • 返回:若返回结果为true,则调用beforeBodyWrite方法

    beforeBodyWrite: 在选择HttpMessageConverter之后以及在调用其write方法之前调用。

    • 参数:
      body:你传入的数据;
      returnType:controller层方法返回的类型;
      selectedContentType :通过内容协商选择的内容类型;
      selectedConverterType:选择要写入响应的转换器类型;
      request/reponse:当前请求和响应;
    • 返回:传入的数据或修改的(可能是新的)实例。

    3.1.2 ResponseNotIntercept

    自定义不被拦截注解,灵活控制对某个方法不封装Result

    /**
     * 返回放行注解
     * 在类和方法上使用此注解表示不会在ResponseResult类中进一步封装返回值,直接返回原生值
     *
     * @author xlwang55
     */
    @Target({ElementType.METHOD, ElementType.TYPE})  //可以在字段、方法
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ResponseNotIntercept {
        String value() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.1.3 CommonExceptionHandler

    统一异常拦截,系统中出现的异常一致会往上抛 最终抛到controller层,被CommonExceptionHandler拦截,此类中针对不同的异常做了不同的返回消息体的处理,如果是未识别的异常统一封装为系统异常。使用原理可以学习下面连接
    SpringBoot项目异常的统一处理

    
    import com.wxl52d41.exception.BusinessException;
    import com.wxl52d41.exception.ForbiddenException;
    import com.wxl52d41.result.Result;
    import com.wxl52d41.result.ResultEnum;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.validation.BindException;
    import org.springframework.validation.ObjectError;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.MissingServletRequestParameterException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
    
    import javax.servlet.ServletException;
    import javax.validation.ConstraintViolationException;
    import java.util.stream.Collectors;
    
    /**
     * 统一拦截异常
     *
     * @author xlwang
     */
    @RestControllerAdvice
    @Slf4j
    public class CommonExceptionHandler {
    
        /**
         * 捕获 自定 异常
         */
        @ExceptionHandler({BusinessException.class})
        public Result<?> handleBusinessException(BusinessException ex) {
            log.error(ex.getMessage(), ex);
            return Result.failed(ex.getCode(),ex.getMessage());
        }
    
        /**
         * 参数缺失异常
         * 说明:参数为必填时,若入参中无此参数则会报MissingServletRequestParameterException
         */
        @ExceptionHandler({MissingServletRequestParameterException.class})
        public Result<?> handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
            log.error(ex.getMessage(), ex);
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
    
        /**
         * 参数值校验异常
         * {@code @PathVariable} 和 {@code @RequestParam} 参数值校验不通过时抛出的异常处理
         */
        @ExceptionHandler({ConstraintViolationException.class})
        public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
            log.error(ex.getMessage(), ex);
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
    
        /**
         * 参数值类型异常
         * 说明: 定义Integer类型,输入的为String,会出现 MethodArgumentTypeMismatchException异常
         */
        @ExceptionHandler(MethodArgumentTypeMismatchException.class)
        public Result<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
            log.error(ex.getMessage(), ex);
            String message = "参数:" + ex.getName() + " 类型错误";
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), message);
        }
    
        /**
         * {@code @RequestBody} 参数校验不通过时抛出的异常处理
         */
        @ExceptionHandler({MethodArgumentNotValidException.class})
        public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
            log.error(ex.getMessage(), ex);
            String msg = ex.getBindingResult().getFieldErrors().stream()
                    .map(f -> f == null ? "null" : f.getField() + ": " + f.getDefaultMessage())
                    .collect(Collectors.joining(", "));
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
        }
    
        @ExceptionHandler(BindException.class)
        public Result<?> handleBindException(BindException ex) {
            log.error(ex.getMessage(), ex);
            return Result.failed(HttpStatus.BAD_REQUEST.value(),
                    ex.getAllErrors().stream()
                            .map(ObjectError::getDefaultMessage)
                            .collect(Collectors.joining("; "))
            );
        }
    
        /**
         * 捕获 {@code ForbiddenException} 异常
         */
        @ExceptionHandler({ForbiddenException.class})
        public Result<?> handleForbiddenException(ForbiddenException ex) {
            log.error(ex.getMessage(), ex);
            return Result.failed(ResultEnum.FORBIDDEN);
        }
    
    
        /**
         * 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
         */
        @ExceptionHandler({Exception.class})
        public Result<?> handle(Exception ex) {
            log.error(ex.getMessage(), ex);
            return Result.failed(ResultEnum.COMMON_FAILED);
        }
    
        /**
         * 处理已知的系统异常
         */
        @ExceptionHandler({ServletException.class})
        public Result<?> handle1(Exception ex) {
            log.error(ex.getMessage(), ex);
            return Result.failed(ex.getMessage());
        }
    
    }
    
    
    • 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
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120

    3.2 统一返回对象Result

    3.2 .1Result

    /**
     * 统一返回数据结构
     * @author xlwang
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Result<T> {
        private Integer status;
        private String message;
        private T data;
    
        public static <T> Result<T> success(T data) {
            return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
        }
    
        public static <T> Result<T> success(String message, T data) {
            return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
        }
    
        public static Result<?> failed() {
            return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
        }
    
        public static Result<?> failed(String message) {
            return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
        }
    
        public static Result<?> failed(IResult errorResult) {
            return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
        }
    
        public static Result<?> failed(Integer code, String message) {
            return new Result<>(code, message, null);
        }
    
        public static <T> Result<T> instance(Integer code, String message, T data) {
            Result<T> result = new Result<>();
            result.setStatus(code);
            result.setMessage(message);
            result.setData(data);
            return result;
        }
    }
    
    • 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

    3.2 .2 IResult接口

    **
     * 定义返回数据结构
     * @author xlwang
     */
    public interface IResult {
        /**
         * 获取状态码
         *
         * @return 状态码
         */
        Integer getCode();
    
        /**
         * 获取消息体
         *
         * @return 消息体
         */
        String getMessage();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    3.2 .3ResultEnum枚举

    /**
     * 常用结果的枚举
     */
    public enum ResultEnum implements IResult {
        SUCCESS(200, "成功"),
        VALIDATE_FAILED(400, "参数错误"),
        COMMON_FAILED(500, "系统错误"),
        FORBIDDEN(2004, "没有权限访问资源");
    
        private Integer code;
        private String message;
    
        //省略get、set方法和构造方法
    
        @Override
        public Integer getCode() {
            return code;
        }
    
        public void setCode(Integer code) {
            this.code = code;
        }
    
        @Override
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    
        ResultEnum(Integer code, String message) {
            this.code = code;
            this.message = message;
        }
    
    }
    
    
    • 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

    3.3 controller层方法测试

    /**
     * 用来测试统一返回的功能
     */
    @RestController
    @RequestMapping("/CommentRest")
    public class CommentRestController {
    
        /**
         *  数值返回值测试,是否能正常封装返回体
         */
        @GetMapping("getId")
        public Integer getId() {
            return 1;
        }
        /**
         * 对象返回值测试,是否能正常封装返回体
         */
        @GetMapping("getOne")
        public TestVO getOne() {
            TestVO testVO = new TestVO("1","测试标题","无内容","小明");
            return testVO;
        }
        /**
         * 字符串返回值测试
         */
        @DeleteMapping("delete")
        public String delete() {
            return "删除成功";
        }
    
        /**
         * 无返回值测试
         */
        @PutMapping("save")
        @ResponseNotIntercept
        public void save() {
            System.out.println("无返回值 = ");
        }
    }
    
    
    • 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

    3.3.1 getId() 数值返回值测试

    在这里插入图片描述

    3.3.2 getOne()对象返回值测试结果

    在这里插入图片描述

    3.3.2 save()无返回值测试结果

    由于添加了自定义的@ResponseNotIntercept注解,测试时不会封装返回值
    无返回值测试结果
    我们将注解注释后再测试一次
    在这里插入图片描述

    3.3.2 delete()字符串返回值测试结果(注意此处为重点)

    上述测试都很顺利,当返回的值是字符串时发现报错了,并不是预期的字符串。
    在这里插入图片描述
    控制台报错信息

    2022-11-01 19:44:26.962 ERROR 36416 --- [nio-9098-exec-2] c.w.advice.ControllerExceptionHandler    : com.wxl52d41.result.Result cannot be cast to java.lang.String
    
    java.lang.ClassCastException: com.wxl52d41.result.Result cannot be cast to java.lang.String
    	at org.springframework.http.converter.StringHttpMessageConverter.addDefaultHeaders(StringHttpMessageConverter.java:44) ~[spring-web-5.3.22.jar:5.3.22]
    
    • 1
    • 2
    • 3
    • 4

    通过错误日志分析说,Result 这个类不能够转换成String。我明明返回的是字符串怎么就是报这个错了呢,其他怎么就没有问题。一顿百度,最终debug发现了端倪。
    SpringBoot统一返回处理出现cannot be cast to java.lang.String异常
    解决方案添加如下代码
    在这里插入图片描述
    再次测试,成功返回在这里插入图片描述

  • 相关阅读:
    Pytorch 多卡并行(3)—— 使用 DDP 加速 minGPT 训练
    vue 导出二进制流文件
    正则校验的多种方式
    计算机组成原理·考点知识点整理
    JAVA大学生过程培养信息系统计算机毕业设计Mybatis+系统+数据库+调试部署
    深度学习推理框架
    springBoot 条件注解
    SpringBoot+uniapp+uview打造H5+小程序+APP入门学习的聊天小项目
    DevExpress WinForms TreeMap组件,用嵌套矩形可视化复杂分层数据
    【机器学习基础】机器学习概述
  • 原文地址:https://blog.csdn.net/weixin_43811057/article/details/127638674