Spring Boot 接口统一返回对象

3,941 阅读4分钟

resultDemo,接口统一返回对象

如果接口返回的内容格式不规范,会对前端开发人员造成困扰,现在让我们来对接口做一些优化。

实现内容

  • 创建统一返回的对象Result
  • 使用注解让实现方式变得更加优雅
  • 结合全局异常捕获

创建统一返回的对象Result

package com.cc.api;

import java.io.Serializable;

/**
 * 统一返回对象
 * @author cc
 * @date 2021-07-12 10:10
 */
public class Result<T> implements Serializable {
    // 自定义状态码
    private Integer code;
    // 提示内容,如果接口出错,则存放异常信息
    private String msg;
    // 返回数据体
    private T data;
    // 接口成功检测。拓展字段,前台可用该接口判断接口是否正常,或者通过code状态码
    private boolean success;
    private static final long serialVersionUID = 1L;

    public Result() {}

    public Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    /**
     * 请求成功返回
     * public和返回值间的<T>指定的这是一个泛型方法,这样才可以在方法内使用T类型的变量
     * @author cc
     * @date 2021-07-12 10:11
     */
    public static <T> Result<T> success() {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null);
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
    }

    /**
     * 请求失败返回
     * @param msg:
     * @author cc
     * @date 2021-07-12 10:11
     */
    public static <T> Result<T> failed(String msg) {
        return new Result<>(ResultCode.FAILED.getCode(), msg, null);
    }

    public static <T> Result<T> failed(String msg, T data) {
        return new Result<>(ResultCode.FAILED.getCode(), msg, data);
    }

    public static <T> Result<T> failed(ResultCode errorCode) {
        return new Result<>(errorCode.getCode(), errorCode.getMsg(), null);
    }

    public static <T> Result<T> failed(ResultCode errorCode, T data) {
        return new Result<>(errorCode.getCode(), errorCode.getMsg(), data);
    }

    ...

    public boolean isSuccess() {
        return this.code == ResultCode.SUCCESS.getCode();
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    @Override
    public String toString() {
        return "Result{" + "code=" + code +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                ", success=" + success +
                '}';
    }
}
自定义状态码表
package com.cc.api;

/**
 * APi返回的状态码表
 * @author cc
 * @date 2021-07-12 8:58
 */
public enum ResultCode {
    /**
     * api状态码管理
     * @author cc
     * @date 2021-07-12 10:00
     */
    SUCCESS(10000, "请求成功"),
    FAILED(10001, "操作失败"),
    TOKEN_FAILED(10002, "token失效"),

    NONE(99999, "无");

    private int code;
    private String msg;

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

用到我们的接口上:

package com.cc.controller;

import com.cc.api.Result;
import com.cc.api.ResultCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/a")
public class TestController {
    @GetMapping("/t1")
    public Result t1() {
        return Result.success("请求成功,返回数据");
    }

    @GetMapping("/t2")
    public Result t2() {
        return Result.failed("请求失败,这里是错误信息");
    }

    // 返回自定义的错误
    @GetMapping("/t3")
    public Result t3() {
        return Result.failed(ResultCode.TOKEN_FAILED);
    }
}

返回内容:

{"code":10000,"msg":"请求成功","data":"请求成功,返回数据","success":true}

使用注解让实现方式变得更加优雅

虽然我们已经初步实现功能,但是现在需要我们将每一个接口的返回对象改成Result,这有两个问题,一是如果我们的接口已经有很多,那么改动量比较大,二是我们很难分清这个接口本来应该返回什么类型的值,这样反而造成了维护的不清晰,不能忍受。

所以要保持原来接口的代码,然后通过给接口类添加注解,来实现返回内容的统一。

我们要创建三个类:

  • ResponseResult,注解类
  • ResponseResultAdvice,返回结果处理类
  • ResponseResultInterceptor,拦截器

ResponseResult:

package com.cc.response;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 需要统一对返回值进行封装的注解
 * @author cc
 * @date 2021-07-12 10:50
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseResult {
}

ResponseResultAdvice:

package com.cc.response;

import com.cc.api.Result;
import com.cc.api.ResultCode;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * 统一封装返回结果
 * @author cc
 * @date 2021-07-12 10:53
 */
@ControllerAdvice
public class ResponseResultAdvice implements ResponseBodyAdvice<Object> {
    /**
     * 标记名称
     * @author cc
     * @date 2021-07-12 10:53
     */
    public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

    /**
     * 判断请求,是否有包装标记,没有就直接返回,不需要重写返回体
     * @author cc
     * @date 2021-07-12 10:53
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        ResponseResult responseResult = (ResponseResult)request.getAttribute(RESPONSE_RESULT_ANN);
        // 是否要执行beforeBodyWrite方法
        return responseResult != null;
    }

    /**
     * 对response处理的执行方法
     * @author cc
     * @date 2021-07-12 10:53
     */
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        /**
         * 不能直接包装String因为当返回值为String的时候,beforeBodyWrite会使用StringHttpMessageConverter转换器,
         * 所以在返回Result的时候会报cannot be cast to java.lang.String错误,解决方式有三种:
         * 1. 在接口层将String打包成Result,这个需要跟诸位开发者协商一致,比较麻烦
         * 2. 将Result对象转换成字符串返回,这样在前端显示的也是字符串,不妥(实现代码:ObjectMapper om = new ObjectMapper(); return om.writeValueAsString(Result.success(o));)
         * 3. 在实现了WebMvcConfigurer的配置类上,修改转换器的执行顺序,让Json比String更早被处理(具体实现在WebMvcConfig类中)
         */

        // 如果已经是Result类,就不再进行封装
        if (o instanceof Result) {
            return o;
        }
        // 如果是列表类
        if (o instanceof List) {
            // do something
        }
        // 如果是整型
        if (o instanceof Integer) {
            // do something
        }
        return Result.success(o);
    }
}

ResponseResultInterceptor:

package com.cc.response;

import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * 返回结果拦截器
 * @author cc
 * @date 2021-07-12 10:55
 */
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
    /**
     * 标记名称
     * @author cc
     * @date 2021-07-12 10:55
     */
    public  static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

    @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)) {
                // 设置此请求体,添加标记,向下传递,在ResponseResultAdvice进行判断
                request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
//                System.out.println("类上有ResponseResult注解");
            } else if(method.isAnnotationPresent(ResponseResult.class)) {
                request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));
//                System.out.println("方法上有ResponseResult注解");
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

为了使其生效,我们把它加到配置类中:

WebMvcConfig:

package com.cc.config;

import com.cc.response.ResponseResultInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;

/**
 * web mvc的配置类
 * @author cc
 * @date 2021-07-12 10:29
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    private final ResponseResultInterceptor responseResultInterceptor;

    public WebMvcConfig(ResponseResultInterceptor responseResultInterceptor) {
        this.responseResultInterceptor = responseResultInterceptor;
    }

    /**
     * 统一返回格式 Result
     * @author cc
     * @date 2021-07-12 10:30
     */
    // 结合接口版本管理后,添加过滤器的代码要放到requestMappingHandlerMapping
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(responseResultInterceptor).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 将json处理器的执行顺序提前,避免Result因为返回String的时候,被string处理器执行导致报错
        converters.add(0, new MappingJackson2HttpMessageConverter());
    }
}

然后在需要的地方加上这个注解即可,不需要改动原有接口代码:

package com.cc.controller;

import com.cc.response.ResponseResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@ResponseResult
@RestController
@RequestMapping("/b")
public class Test2Controller {
    @GetMapping("/t1")
    public String t1() {
        return "hello java";
    }

    @GetMapping("/t2")
    public Integer t2() {
        return 1;
    }
}

返回结果:

{"code":10000,"msg":"请求成功","data":"hello java","success":true}

结合全局异常捕获

接口正常执行的时候没有问题,但是当出现异常时Result就不生效了,所以结合全局异常捕获,可以让系统在程序出错的时候仍然返回规范的结果给前端:

GlobalExceptionHandler:

package com.cc.exception;

import com.cc.api.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常捕获,出现异常时以Result的形式返回
 * @author cc
 * @date 2021/06/16 15:10
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public Result<Object> catchException(Exception e) {
        todoErrorLogger(e, null);
        e.printStackTrace();
        return Result.failed(e.getMessage());
    }

    private void todoErrorLogger(Exception e, String customErrorMsg) {
        StringBuilder sb = new StringBuilder();
        sb.append(e.getClass().getName()).append(":");
        if (customErrorMsg != null) {
            sb.append(customErrorMsg);
        } else {
            sb.append(e.getMessage());
        }
        StackTraceElement[] elements = e.getStackTrace();
        if(elements.length > 0){
            StackTraceElement element = elements[0];
            sb.append("##function:").append(element.getClassName()).append("-").append(element.getMethodName()).append("-").append(element.getLineNumber());
        }
        e.printStackTrace();
    }
}