logo头像

不破不立

SpringBoot统一包装接口返回值

SpringBoot对于Http请求做了两种处理,一种是返回视图,一种是返回JSON。都使用的是HTTP统一的状态码来对返回状态进行表述。

很多公司都喜欢包装一种自己的返回值,使用自己定义的业务状态码来处理;今天我们就来简单的统一包装一下API的返回值。

记得看总结!!!记得看总结!!!记得看总结!!!

大致的流程:

  1. 定义返回值类型Result
  2. 定义响应状态码枚举ResultCode
  3. 定义返回值工具类ResultUtils
  4. 定义返回值注解@ResponseResult
  5. 定义处理器拦截器ResponseInterceptor
  6. 定义返回值包装处理器ResponseHandler
  7. 注册处理器拦截器和String返回类型转换器
  8. 定义全局异常处理器AppExceptionHandler

1. 定义返回值类型Result

首先,我们需要设定好我们的返回值类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Result<T> implements Serializable {

/**
* 时间戳
*/
private Date timestamp;
/**
* 状态码
*/
private Integer code;
/**
* 状态码描述
*/
private String message;
/**
* 数据
*/
private T data;
}

上面就是大多数公司会采用的方式,可能有些会去掉时间戳,有些会增加请求路径path,有些会增加异常时的详细信息errorMessages等等。

2. 定义响应状态码枚举ResultCode

然后,我们就需要定义好我们状态码枚举,像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum ResultCode {


/** 成功状态码 1000 */
SUCCESS(1000,"成功"),
APP_EXCEPTION(2000,"系统错误"),
/** 参数相关状态码 2100-2199 */
PARAM_IS_INVALID(2100, "参数无效"),
PARAM_IS_BLANK(2101,"参数为空"),
PARAM_NOT_COMPLETE(2102,"参数不完整"),
PARAM_TYPE_BIND_ERROR(2101,"参数类型错误"),
;
private Integer code;
private String message;
}

像上面举的参数相关,可能还有用户相关,业务相关等等。

3.定义返回值工具类ResultUtils

有了上述两个,我们其实可以直接在Controller方法返回值处通过new Result方式来直接使用包装返回类型了。

但是,当我们的接口越来越多时,我们会发现Controller方法一直在做重复的new Result操作,而且这个操作与业务无关,正常来说与业务无关的操作频繁出现会让开发者为难,同时代码也是千篇一律,没有重要关注点了。

因此,我们需要一个工具类来统一去掉new Result的方式,ResultUtils登场了。

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
public class ResultUtils {
static final String RESPONSE_RESULT = "RESPONSE_RESULT";
private ResultUtils(){}

public static Result success() {
Result r = new Result();
r.setResponseCode(ResultCode.SUCCESS);
return r;
}

public static Result success(Object data) {
Result r = success();
r.setData(data);
return r;
}

public static Result failure(ResultCode resultCode) {
Result r = new Result();
r.setResponseCode(resultCode);
return r;
}

public static Result failure(ResultCode resultCode, Object data) {
Result r = new Result();
r.setResponseCode(resultCode);
r.setData(data);
return r;
}
}

这样,每次我们在Controller返回方法处调用ResultUtils.success或者failure就可以了。

但是仔细一看,我们会发现其实这样也是换汤不换药,归根结底还是没有解决重复大量的与业务无关的Result代码。

这时候,就需要使用注解来处理了,我们可以定义一个类注解,或者方法注解,来统一处理这一类Controller,让这些重复的操作在注解中完成就好了,我们继续关注我们的业务就ok了。所以ResponseResult注解来了。

4. 定义返回值注解@ResponseResult

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ResponseResult {

}

仔细一看,这个注解也没啥,就是表明在运行时生效,可以标注在类和方法体上。就这么简单?当然不,我们还需要对注解进行处理。

5. 定义处理器拦截器ResponseInterceptor

我们定义一个HandlerInterceptor,重写他的preHandler方法,用来拦截所有的处理器方法(即Controller方法),来判断这些方法以及它所在类是否标注@ResponseResult注解,如果标注了,那么就请求头上打上一个tag,用于后续包装返回结果做标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class ResponseInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
if (clazz.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(ResultUtils.RESPONSE_RESULT, clazz.getAnnotation(ResponseResult.class));
} else if (method.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(ResultUtils.RESPONSE_RESULT, method.getAnnotation(ResponseResult.class));
}
}
return true;
}
}

注意这里需要将其标注为@Component,作为一个Spring的Bean放入Spring的容器中,后续会用到。

6. 定义返回值包装处理器ResponseHandler

上面再处理器拦截器中打了一个标记,下面是不是该处理这个标记了呢。

我们定义一个ResponseBodyAdvice返回结果包装处理器,重写supports和beforeBodyWrite方法。
在supports方法中,我们来判断http请求是否有携带上面的标记,如果有携带,则返回true,表示该请求需要对返回值进行包装,要求它进入beforeBodyWrite方法中处理返回值。如果不携带就直接跳过该处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (sra == null) {
return false;
}
HttpServletRequest request = sra.getRequest();
ResponseResult responseResult = (ResponseResult) request.getAttribute(ResultUtils.RESPONSE_RESULT);
return responseResult != null;
}

@Override
public Object beforeBodyWrite(Object resultBody, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (resultBody instanceof Result) {
return resultBody;
}
return ResultUtils.success(resultBody);
}
}

注意,该方法需要标注注解@ControllerAdvice。

还需要注意的是我在beforeBodyWrite方法中有一段代码:

1
2
3
if (resultBody instanceof Result) {
return resultBody;
}

这里主要是处理后续的全局异常返回值的。剧透一下,是因为异常处理器返回值本身已经处理为Result,因此便不需要重复包装。

7. 注册处理器拦截器和String返回类型转换器(WebMvcConfigurer)

到这,基本上上完成了,但是还需要注意的是,我们需要将上面的处理器拦截器注册到WebMVC 的InterceptorRegistry中,并设置它的拦截规则。

1
2
3
4
5
6
7
8
9
10
@Configuration
public class ApplicationConfigure implements WebMvcConfigurer {
@Autowired
private ResponseInterceptor responseInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(responseInterceptor).addPathPatterns("/**");
}
}

是不是到这就ok了呢?

这里基本上是的,但是,我们可以尝试一下在@RestController标注Controller方法中,返回一个String类型的值,就会出现一个异常:Result can not cast to String(大致意思是这样)。
这里我们需要对String类型返回值处理一下,用Json类型转换器就转换一下就可以了。

如下完整的代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class ApplicationConfigure implements WebMvcConfigurer {

@Autowired
private ResponseInterceptor responseInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(responseInterceptor).addPathPatterns("/**");
}

@Bean
public HttpMessageConverters messageConverters() {
return new HttpMessageConverters(new FastJsonHttpMessageConverter());
}
}

ok了么?没有,正常结果返回是ok了,那么异常返回呢?

8. 定义全局异常处理器APPExceptionHandler

上面我们在第6步提到了异常返回的时候的处理,这里便讲的是这个。

这里我们定义全局异常处理器,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestControllerAdvice
public class AppExceptionHandler {

@ExceptionHandler(BindException.class)
public Result handlerBindException(BindException e) {
return ResultUtils.failure(ResultCode.PARAM_TYPE_BIND_ERROR);
}


@ExceptionHandler(ParamException.class)
public Result handlerParamException(ParamException e){
return ResultUtils.failure(ResultCode.PARAM_IS_INVALID);
}


@ExceptionHandler(AppException.class)
public Result handlerAppException(AppException e) {
return ResultUtils.failure(ResultCode.APP_EXCEPTION);
}
}

上述代码中的AppException,ParamException这些都是需要自定义的,继承自RuntimeException即可,就不贴出来了。

总结

上面一个流程走下来,大家对于API统一返回值包装也是有个大致的熟悉了,国内很多公司都采用了这一套,也确实比较形象。

那么我们思考一个问题,SpringBoot不是自定义了一套JSON返回类型吗,我们真的还需要多次一举的再次包装么?
我之前也没意识到,只是觉得既然大家都这么做了,应该是有道理的。直到我看到这篇文章下的评论(重点是评论,不是文章,当然,文章也不错)

https://segmentfault.com/a/1190000017908482?utm_source=tag-newest

评论内容为:

gh6497 · 8月14日

能不能尊重下http协议。全世界只有中国程序员还在包装接口返回值, 强行增加代码量.

哎呀,我去,我一看,怎么好像很有道理的样子;然后仔细一想,确实有那么些道理;然后就动手试了试,发现人家确实说的极有道理。

大家可以去动手看看,用用rest的一些工具试试看,然后想想是不是这样。

当然,包装接口在国内确实常见,也一定是有它的道理的。

看公司、个人的取舍吧。

还是得多思考,不能人云亦云啊。

支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励