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

时间:2022-11-04 15:55:12

一 前言

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

二 @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);
    }
}

说明: 实现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 "";
}

3.2 统一返回对象Result

/**
 * 统一返回数据结构
 * @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;
    }
}

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("无返回值 = ");
    }
}

3.3.1 getId() 数值返回值测试

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

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

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

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

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

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

上述测试都很顺利,当返回的值是字符串时发现报错了,并不是预期的字符串。
SpringBoot统一封装controller层返回的结果
控制台报错信息

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]

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