统一封装controller层返回的结果
一 前言
目前前后端分离的项目中,我们在controller层会统一格式封装结果给前端。如果我们在每个方法中手动封装Result,无疑是增加了额外的工作量。
那么有没有一种方式我们只返回它相应的数据。对于结果可以自动帮我们封装,且还可以对某个方法或者某个类下所有方法封装或者不封装。
答案是肯定的。利用@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() 数值返回值测试
3.3.2 getOne()对象返回值测试结果
3.3.2 save()无返回值测试结果
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]
通过错误日志分析说,Result 这个类不能够转换成String。我明明返回的是字符串怎么就是报这个错了呢,其他怎么就没有问题。一顿百度,最终debug发现了端倪。
SpringBoot统一返回处理出现cannot be cast to java.lang.String异常
解决方案添加如下代码
再次测试,成功返回