通过ResponseBodyAdvice包装统一返回类型

要求

接口返回真实的业务数据对象,而且还要有统一的返回数据格式。例如,接口是这样的:

@GetMapping("/user/{id}")
public User user(@PathVariable("id") Integer id){
     
    return userService.getUser(id);
}

但是返回的数据格式是这样的

{
     
    "code": 1,
    "msg": "成功",
    "data": {
     
        "id": 1,
        "username": "user",
        "password": "e10adc3949ba59abbe56e057f20f883e",
        "email": null,
        "authorities": null,
        "enabled": true,
        "accountNonLocked": true,
        "accountNonExpired": true,
        "credentialsNonExpired": true
    }
}

好处是我们在编写接口的时候,不用每次都把数据封装到统一的数据返回类型中。直接以最真实的业务对象返回,更加清晰明了。

思路

  • 定义一个注解@ResponseResult用于在方法或者类上面标注,标识这个接口需要包装数据
  • 拦截请求、判断此请求是否被@ResponseResult注解标注
  • 实现接口ResponseBodyAdvice,然后需要用@ControllerAdvice注解激活,用于判断请求是否需要包装,如果需要,就把Controller中的返回值进行重写

具体实现

统一返回对象

@Data
public class Result implements Serializable {
     

    // 返回状态码
    private Integer code;
    // 返回消息
    private String msg;
    // 返回数据
    private Object data;

    public void setResultCode(ResultCode code){
     
        this.code = code.code();
        this.msg = code.msg();
    }
    public void setResultCode(ResultCode code,String msg){
     
        this.code = code.code();
        this.msg = msg;
    }

    /* 调用成功 */
    public static Result success(){
     
        Result result = new Result();
        result.setResultCode(ResultCode.SUCCESS);
        return result;
    }

    /* 调用成功 */
    public static Result success(Object data){
     
        Result result = new Result();
        result.setResultCode(ResultCode.SUCCESS);
        result.setData(data);
        return result;
    }

    /* 调用失败 */
    public static Result error(ResultCode code){
     
        Result result = new Result();
        result.setResultCode(code);
        return result;
    }

    /* 调用失败 */
    public static Result error(ResultCode code,Object data){
     
        Result result = new Result();
        result.setResultCode(code);
        result.setData(data);
        return result;
    }
public enum  ResultCode {
     

    /* 成功状态码 */
    SUCCESS(1,"成功"),
    /* 参数错误,1001-1999 */
    PARAM_IS_INVALID(1001,"参数无效"),
    PARAM_IS_BACK(1002,"参数为空"),
    PARAM_TYPE_BIND_ERROR(1003,"参数类型错误"),
    PARAM_NOT_COMPLETE(1004,"参数缺失"),
    /* 用户错误: 2001-2999 */
    USER_NOT_LOGIN(2001,"用户未登录,访问的路径需要验证,请登录"),
    USER_LOGIN_ERROR(2002,"账号不存在或密码错误"),
    USER_ACCOUNT_FORBIDDEN(2003,"账号已被禁用"),
    USER_NOT_EXISTS(2004,"用户不存在"),
    USER_HAS_EXISTS(2005,"用户已存在"),

    /* 系统异常异常错误 4001-4999 */
    SYSTEM_THROW_ERROR(4001,"系统内部异常");


    private Integer code;
    private String msg;

    public Integer code(){
     
        return this.code;
    }
    public String msg(){
     
        return this.msg;
    }

    ResultCode(Integer code,String msg){
     
        this.code = code;
        this.msg = msg;
    }

}

定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target({
     ElementType.TYPE , ElementType.METHOD})
@Documented
public @interface ResponseResult {
     
}

此注解仅做标注作用,所以不需要其他属性。

定义拦截器

@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
     

    // 标记名称
    private final static String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

    /**
     * 【 拦截标注了@ResponseResult注解的请求 】
     * @Author zwx
     * @Date 2020/7/28 09:28
     **/
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     
        // handle是拦截的当前请求的方法的全类名+参数+抛出的异常
        // 请求的方法
        if (handler instanceof HandlerMethod){
     
            final HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 所在类
            final Class<?> clazz = handlerMethod.getBeanType();
            final Method method = handlerMethod.getMethod();
            // 判断类上是否加了@ResponseResult注解
            if (clazz.isAnnotationPresent(ResponseResult.class)){
     
                //设置次请求返回体,需要包装,向下传递,在ResponseBodyAdvice接口进行判断
                request.setAttribute(RESPONSE_RESULT_ANN,clazz.getAnnotation(ResponseResult.class));
            }
            // 如果类上没有,则再判断方法上
            else if (method.isAnnotationPresent(ResponseResult.class)){
     
                //设置次请求返回体,需要包装,向下传递,在ResponseBodyAdvice接口进行判断
                request.setAttribute(RESPONSE_RESULT_ANN,method.getAnnotation(ResponseResult.class));
            }
        }
        return true;
    }
    // intercept 的其他两个接口不涉及逻辑,此处省略
    ... ... 
}

主要就是利用反射API来获取此请求接口的全方法名,以及类名。然后判断如果此方法标注在类上,那么类中的所有接口都应该包装返回值。如果类上没有标注注解,再判断方法上是否标注了注解。如果标注了注解的话,就在请求中设置attribute,仅用作标记用,具体的还需要在ResponseBodyAdvice类中进行处理。

因为我们到时候注册拦截器的时候是拦截的所有的请求,然后再根据请求的类型来进行判断,handler instanceof HandlerMethod,如果是一个web接口方法,则走进if判断中。如果不是,直接放行请求。

注册拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
     

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
     
        //拦截所有请求
        registry.addInterceptor(new ResponseResultInterceptor()).addPathPatterns("/**");
    }
}

此处拦截所有请求,或者可以根据自己的业务需要来具体安排。

包装返回值

此代码核心思想,就是获取此请求,是否需要返回值包装,设置一个属性标记。重写返回体

@ControllerAdvice
public class ResponseResultAdvice implements ResponseBodyAdvice<Object> {
     

    // 标记名称
    private final static String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
     
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        // 判断请求是否有包装标记
        ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
        return responseResultAnn != null;
    }
}

这是实现ResponseBodyAdvice接口必须要重写的两个方法之一,用来判断是否需要包装返回值。如果需要就包装返回值,然后方法返回值true,就会执行包装的方法beforeBodyWrite(),如果不需要包装就直接返回false,不用重写beforeBodyWrite()

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
     
        System.out.println("============开始包装返回类型");
        System.out.println(body);
        // 判断body是否是异常类型
        if ( body instanceof Result){
     
            return body;
        }
        return Result.success(body);
    }

包装返回值的核心代码,直接将原本的返回值封装进Result就可以。

这里的判断body instanceof Result的作用是,如果需要包装返回值的接口发生了异常,我们需要对异常进行处理。

这里我采用了全局异常处理@ControllerAdvice+@ExceptionHandler来处理发生的异常,然后将异常信息包装进Result对象。

之所以这么判断,是因为当接口出现异常之后,会先调用被@ExceptionHandler注解标注的方法ProcessException(),来捕获指定的异常,这样的话异常信息就会被封装进Result了。那之后才会调用包装返回值方法beforeBodyWrite(),这里就需要判断body的类型了,因为如果出现异常,那么此时body的类型是在ProcessException()中封装之后返回的Result对象,那么此时并不需要再次封装,直接返回就好了。而如果没有出现异常,那么此时body就是实际的业务对象了,此时就需要来封装返回值。

@ExceptionHandler( value = Exception.class)
@ResponseBody
public Object ProcessException(Exception e){
     
    return Result.error(ResultCode.SYSTEM_THROW_ERROR,e.getMessage());
}

参考博客:
Java生鲜电商平台-统一格式返回的API架构设计与实战

你可能感兴趣的:(Spring,拦截器,java,spring)