我们从一开始,前后端没有统一的数据格式,封装了Result、PageResult等类,统一返回的JSON格式,随后我们又发现出现异常时返回的JSON和正常响应时JSON依旧不统一,于是尝试使用用Result处理异常返回,将自定义的异常转为Result输出,最后让RestControllerAdvice做兜底处理。
为什么要统一结果封装?
前后端交互格式,现在最常用的是JSON字符串格式传递
如果没有统一结果封装的话,无论是单个对象,还是对象集合,只能用于正常返回的情况。
当不是正常情况时,我们应该怎么返回值给前端呢?
后端有参数校验时,需要将异常信息返回时
后端逻辑不严谨导致异常时
为了兼容各种可能的情况,提供更好的接口联调的体验,需要拟定一套统一的返回格式。必要时可以做统一异常处理。
常见的处理方式
至少包含的字段:
- data,用来存储实际的返回数据
- code/success,用来表示此次的请求是否成功,(前端依据这个字段来判断)
- message,无论接口因为什么原因失败,如果后端希望告知前端,都可以将必要信息存入该字段。
阿里云处理返回值:
使用了一个@CosmoController注解,返回值仍是CourseDTO,不是封装好的Result,但是前端得到的JSON是这样的:
返回的值竟然还是被封装了的。
认识ResponseBadyAdvice
public class CommonResponseDataAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
return Result.success(o);
}
}
当我们实现ResponseBodyAdvice时,需要我们重写两个方法,一个suports方法,一个beforeBodyWrite方法,第一个方法当返回是false返回值则不进行任何处理,只有当返回值为true时才会走beforeBodyWrite方法,在此方法里我们就可以把返回数据进行统一封装了。但此时是全局的Controller都会进入,造成有些数据不想封装也封装了,让我们获取不到想要的结果。
整理一下ResponseBadyAdvice接口
Spring提供一个接口,和AOP一样的,xxxAdvice用来增强的。 @ResponseBadyAdvice+@RestControllerAdvice,可以拦截返回值
通过supports方法判断是否拦截 模拟阿里云的CosmoController 有了ResponseBadyAdvice接口,我们就很容易想到:只要在beforeBadyWrite中对返回值o进行统一结果封装,就能达到@CosmoController的效果。
怎么实现注解方式的处理返回值封装呢?
定义一个CosmoController注解
在实现了ResponseBadyAdvice的实现类中的supports方法中判断是否使用了@CosmoController注解,使用了的进行封装处理。
定义@CosmoController注解
SpringBoot只会读取自己定义的Controller、RestController注解,以及Bean 实例化,及返回值处理等,我们自定义的注解SpringBoot肯定是不会帮我们读取处理的。
观察RestController注解是怎样的:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
除了元注解以外,还有@Controller和@ResponseBody注解
SpringBoot准确来说 会读取@Controller和@ResponseBody注解,所以为了使SpringBoot能够读取到自己,把Spring能读取到的注解作为父注解。
所以我们可以模仿@RestController注解,我们直接把@RestController注解套上(好处): @RestController的功能都继承了
可以拓展自己注解的功能
使用注解对返回结果进行统一结果封装
@Documented
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@RestController
public @interface CosmoController {
}
@RestControllerAdvice
public class CommonResponseDataAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return methodParameter.getDeclaringClass().isAnnotationPresent(CosmoController.class);
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
return Result.success(o);
}
}
此注解功能为,如果使用了此注解则对返回值进行统一结果封装。 此时已经完成基本的要求了。
优化
上面的代码还是不够健壮,有些情况没考虑到:
如果此时Controller的返回值已经用Result封装过了,此时就会造成重复嵌套
不够细颗粒度,如果某些方法就是不需要封装,就会显得画蛇添足了
如参数校验失败、统一异常处理等情况怎么办呢
为了避免重复嵌套,我们可以在beforeBodyWrite调用时里判断并处理,也可以通过 methodParameter中的returnValue判断。
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
return o instanceof Result ? o :Result.success(o);
}
如果希望个别方法中的返回值不需要进行返回值统一结果封装,又不想类中没有需要封装的方法加上注解,我们必须给不需要封装的方法加一个标记,即创建一个忽略封装的注解。
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreCosmoResult {
}
public boolean supports(MethodParameter methodParameter, Class aClass) {
// 标注了CosmoController注解 且类与方法上没有IgnoreCosmoResult注解的方法才进行返回值包装
return methodParameter.getDeclaringClass().isAnnotationPresent(CosmoController.class) &&
!methodParameter.getDeclaringClass().isAnnotationPresent(IgnoreCosmoResult.class) &&
!methodParameter.getMethod().isAnnotationPresent(IgnoreCosmoResult.class);
}
如果参数校验错误,处理方式大概有两种
转为自定义异常抛出,用全局异常处理器兜底,异常会被@RestControllerAdvice捕获走全局异常处理逻辑
在当前方法用Result.error封装信息返回,及时传到了处理返回值封装时,此时也有判断,如果封装了Result,直接返回。