API接口设计(1):统一返回数据格式

1.背景

现在的项目绝大部分都采用微服务框架,并采用前后端分离的开发模式,这对前后端的数据通信,提出了要求。
后端返回给前端的数据类型可能会是:基本数据类型、String字符串、对象、数组、或者异常提示等。前端拿到后端返回的数据去展示或者给出错误提示,但前端不可能对每个接口都把这些异常提示处理一遍,比如说返回没有登录、或者一些业务异常、或者其他服务内部异常等。

这属于前后端分离的接口约定和规范问题。

2.为什么要统一返回数据格式?

建立前后端的接口约定和规范,统一数据格式,方便处理数据,前后端可以更好地交互以及通信。

3.实现

基于上面场景,那么我们要做的就是在后端返回结果前做一层统一处理,然后返回一个统一的对象。
比如返回一个Result bean,该bean有code、message、data等属性,然后前端根据返回的code做统一处理。这是一个前后端的一个约定。

Result Bean长这样子:

@Data
public class Result<T> implements Serializable {
    private Integer code;
    private String message;
    private T data;

    public Result() {
    }

    public Result(ResultCode resultCode, T data) {
        this.code = resultCode.getCode();
        this.message = resultCode.getMessage();
        this.data = data;
    }

    public void setResultCode(ResultCode resultCode) {
        this.setCode(resultCode.getCode());
        this.setMessage(resultCode.getMessage());
    }

    public static Result success() {
        Result result = new Result();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMessage(ResultCode.SUCCESS.getMessage());
        return result;
    }

    public static Result success(Object data) {
        Result result = new Result();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMessage(ResultCode.SUCCESS.getMessage());
        result.setData(data);
        return result;
    }

    public static Result fail(Integer code, String message) {
        Result result = new Result();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    public static Result fail(ResultCode resultCode) {
        Result result = new Result();
        result.setCode(resultCode.getCode());
        result.setMessage(resultCode.getMessage());
        return result;
    }

    public static Result fail(ResultCode resultCode, Object data) {
        Result result = new Result();
        result.setResultCode(resultCode);
        result.setData(data);
        return result;
    }
}

前端可以根据不同的code,进行处理: code=200,返回成功,返回数据在data上,前端可以直接使用data;code=500或其他,后端返回异常,可能是业务异常,也可能是程序异常,错误信息放在message上。如果未登录,后端返回403,这时候前端在调用后端接口返回那里根据错误码去做统一的处理,统一提示或其他。请求成功的话,可以使用返回的数据进行之后的业务处理。

3.1 定义spring拦截器,识别需要处理的接口或者类

拦截器(Interceptor) 和 Filter 过滤器一样,它俩都是面向切面编程——AOP 的具体实现,可以使用 Interceptor 来执行某些任务,例如在 Controller 处理请求之前编写日志,添加或更新配置等等。
在 Spring中,当请求发送到 Controller 时,在被Controller处理之前,它必须经过 Interceptors(0或多个)。

这一步的作用是:增加一个拦截器,识别出来哪些接口需要进行数据格式二次包装的接口。

由于注解有一定的性能损耗,基于Guava Cache来做的,我加了一层本地缓存,来优化性能。
我的实现如下:

public class ResponseResultInterceptor implements HandlerInterceptor {

    private static final String RESPONSE_RESULT_ANNOTATION = "RESPONSE_RESULT_ANNOTATION";

    @Resource
    private InterceptorLocalCacheService interceptorLocalCacheService;

    @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();

            String classKey = "class" + "|" + clazz.getName();
            String methodKey = "method" + "|" + request.getMethod() + "|" +
                    method.getDeclaringClass().getName() + "#" + method.getName();

            Object cacheOfClazz = null;
            Object cacheOfMethod = null;
            if ((cacheOfClazz = interceptorLocalCacheService.get(classKey, clazz)) != null) {
                request.setAttribute(RESPONSE_RESULT_ANNOTATION, cacheOfClazz);
            } else if ((cacheOfMethod = interceptorLocalCacheService.get(methodKey, method)) != null) {
                request.setAttribute(RESPONSE_RESULT_ANNOTATION, cacheOfMethod);
            }
        }
        return true;
    }
}

3.2 ResponseBodyAdvice 切面

ResponseBodyAdvice接口属于springMVC 和springBoot框架基础的底层切面接口;实现这个接口的类,可以修改直接作为 ResponseBody类型处理器的返回值,即进行功能增强。
利用ResponseBodyAdvice切面可以对返回结果进行加工处理,返回给调用方一个统一的响应对象。

定义一个ResultCode 注解,标注了该注解的接口,都会对返回结果进行处理。当然,相反地,也可以定义一个IgnoreResponseHandler注解,只要接口或者类标注了这个注解,那就直接返回原来的业务数据实体,不需要再次包装。

我在自己的实现里,定义了一个ResultCode 注解,凡是标注了ResultCode 注解的类或者方法,都会对返回结果进行二次包装处理。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ResultCode {
}
@Slf4j
@ControllerAdvice
public class ResponseBodyHandlerAdvice implements ResponseBodyAdvice<Object> {

    private static final String RESPONSE_RESULT_ANNOTATION = "RESPONSE_RESULT_ANNOTATION";

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {

        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();

        ResultCode responseResultAnnotation = (ResultCode) request.getAttribute(RESPONSE_RESULT_ANNOTATION);
        boolean pass = (responseResultAnnotation != null) ;
        return pass;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        log.debug("response body rewrite uri:{}", request.getURI());
        if (body instanceof ErrorResult) {
            ErrorResult result = (ErrorResult) body;
            log.error("response body rewrite fail result,uri:{}", request.getURI());
            return Result.fail(result.getCode(), result.getMessage());
        }
        if (body instanceof String) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                Result result = Result.success(body);
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                throw new APIException("API请求异常");
            }
        }
        return Result.success(body);
    }
}

3.3 接口层面,Controller可以这么写

接口的Controller的实现,如果有需要ResultCode注解也可以加载类上面,对全部方法进行处理:

@RestController
@RequestMapping("users")
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping("raw/{id}")
    public UserDTO get(@PathVariable Long id) {
        return userService.get(id);
    }

    @RequestMapping("query/{id}")
    @ResultCode
    public UserDTO query(@PathVariable Long id) {
        return userService.get(id);
    }

    @RequestMapping("{id}")
    public Result<UserDTO> getUser(@PathVariable Long id) {
        UserDTO userDTO = userService.get(id);
        return Result.success(userDTO);
    }

    @RequestMapping("exception/{id}")
    @ResultCode
    public UserDTO exception(@PathVariable Long id) {
        userService.exception(id);
        return null;
    }

}

此外,还需要进行统一的异常处理,下一篇写全局的异常处理。

(完)

发表评论

邮箱地址不会被公开。 必填项已用*标注