通过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架构设计与实战

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
统一处理返回结果时,我们可以使用一个自定义的类来封装返回结果,这个类中可以包含需要返回的数据以及一些额外的信息,比如状态码、错误信息等。在这个类中,我们可以使用 Java 中的 Optional 类型来表示可能为 null 的字段,这样就可以保留 null 字段了。 例如,在返回结果中可能包含一个 User 对象,但是这个对象有可能为 null。我们可以定义一个返回结果类 Result<T>,其中 T 代表返回的数据类型,代码如下: ```java public class Result<T> { private int code; private String message; private T data; public Result(int code, String message, T data) { this.code = code; this.message = message; this.data = data; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Optional<T> getData() { return Optional.ofNullable(data); } public void setData(T data) { this.data = data; } } ``` 在这个类中,我们使用了 Optional<T> 类型来表示可能为 null 的字段 data,在调用方获取 data 时,可以通过调用 Optional 的 get() 方法来获取 data 的值,如果 data 为 null,则会抛出 NoSuchElementException 异常,因此需要在调用 get() 方法前先判断 Optional 是否为 null。 例如,如果需要返回一个 User 对象,但是这个对象可能为 null,可以这样使用 Result 类: ```java public Result<User> getUser(int userId) { User user = userService.getUser(userId); if (user != null) { return new Result<>(200, "success", user); } else { return new Result<>(404, "not found", null); } } ``` 在调用方获取 User 对象时,可以这样使用: ```java Result<User> result = getUser(1001); if (result.getData().isPresent()) { User user = result.getData().get(); // 处理 user 对象 } else { // 处理返回结果为空的情况 } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值