SpringBoot之阿里云返回值处理技巧 | 小册免费学

975 阅读5分钟

我们从一开始,前后端没有统一的数据格式,封装了Result、PageResult等类,统一返回的JSON格式,随后我们又发现出现异常时返回的JSON和正常响应时JSON依旧不统一,于是尝试使用用Result处理异常返回,将自定义的异常转为Result输出,最后让RestControllerAdvice做兜底处理。

为什么要统一结果封装?

前后端交互格式,现在最常用的是JSON字符串格式传递

如果没有统一结果封装的话,无论是单个对象,还是对象集合,只能用于正常返回的情况。

当不是正常情况时,我们应该怎么返回值给前端呢?

后端有参数校验时,需要将异常信息返回时

后端逻辑不严谨导致异常时

为了兼容各种可能的情况,提供更好的接口联调的体验,需要拟定一套统一的返回格式。必要时可以做统一异常处理。

常见的处理方式

至少包含的字段:

  • data,用来存储实际的返回数据
  • code/success,用来表示此次的请求是否成功,(前端依据这个字段来判断)
  • message,无论接口因为什么原因失败,如果后端希望告知前端,都可以将必要信息存入该字段。

阿里云处理返回值:

1.png 使用了一个@CosmoController注解,返回值仍是CourseDTO,不是封装好的Result,但是前端得到的JSON是这样的:

2.png 返回的值竟然还是被封装了的。

认识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的效果。

3.png

怎么实现注解方式的处理返回值封装呢?

定义一个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,直接返回。

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情