美文网首页程序员
SpringBoot统一数据返回和异常

SpringBoot统一数据返回和异常

作者: 西5d | 来源:发表于2020-08-24 20:15 被阅读0次

背景

我们在开发中,如果涉及到双端的交互,最好能统一调用的格式。确实很多人是这样做的,但是如果完全依赖开发者各自去维持这个规则,随着业务的增长,接口的增多,会带来额外的开发量,而且很大可能最终也没法保证完全遵守对应的规则。所以,如果能实现一个统一的门面,将数据返回和异常都封装起来,让业务开发者不关心数据格式和异常,只关心自己的业务问题,想必对开发效率会有比较好的提升。
很幸运的是依赖SpringBoot的组件,我们是可以统一封装返回或者异常的,下面做个具体的介绍。

原理简单介绍

依赖的SpringBoot组件主要是HttpMessageConverterRestControllerAdvice或者ControllerAdvice,前者是Spring的对象转换器,可以将业务数据按照需要的类型来返回,比如返回json,返回byte,返回html等。内部已有定义了如jackson,gson,string等多种的转换器,有兴趣的可以通过HttpMessageConverter的具体实现来了解。

RestControllerAdvice, ControllerAdvice是注解,可以通过实现ResponseBodyAdvice接口的beforeBodyWrite()方法来控制最终返回的数据结构和状态,也可以结合@ExceptionHandler@ResponseStatus注解来定义方法统一处理包装异常的返回结构。下面会已实际的例子来做说明。

统一的返回结构定义

首先定义一个统一的数据返回结构,包括状态码,返回信息,返回数据三个部分。

@Data
public class BaseResult<T> {
    private Integer code;
    private String msg;
    private T data;

    public static <T> BaseResult success(T data) {
        BaseResult<T> result = new BaseResult<>();
        result.setData(data);
        result.setCode(HttpStatus.OK.value());
        return result;
    }

    public static BaseResult success() {
        return success(null);
    }

    public static BaseResult fail(String msg) {
        BaseResult result = new BaseResult<>();
        result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        result.setMsg(msg);
        return result;
    }
}

数据的统一返回

我们先处理数据的返回。定义一个ControllerAdvice实现ResponseBodyAdvice接口

@RestControllerAdvice
public class CommonResultAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (null == o) {
            //这里包含特殊情况,后面会做介绍
            return BaseResult.success();
        }

        if (o instanceof BaseResult) {
            return o;
        }
        return BaseResult.success(o);
    }
}

这里说下当直接返回null,即null == o的情况,在使用默认的jackson的情况下,如果不处理,是没有contentBody返回的。文章这里使用了gson来处理。原因是在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse) 中处理的时候,jackson和gson实现不同,jackson会直接返回空的content。

要默认改成gson,可以在配置中添加

spring.http.converters.preferred-json-mapper=gson

但是直接如上使用还是会有问题,因为我们定义的基本返回对象有泛型,所以在直接返回List<T>, Map<T>等泛型的时候,默认的writeInternal()方法写入会有问题,进入下面代码的第一个分支,会抛出java.lang.ClassCastException

@Override
    protected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {
        // In Gson, toJson with a type argument will exclusively use that given type,
        // ignoring the actual type of the object... which might be more specific,
        // e.g. a subclass of the specified type which includes additional fields.
        // As a consequence, we're only passing in parameterized type declarations
        // which might contain extra generics that the object instance doesn't retain.
        if (type instanceof ParameterizedType) {
            getGson().toJson(o, type, writer);
        }
        else {
            getGson().toJson(o, writer);
        }
    }

因此,这里自定义GsonMessageConverter

    @Bean
    public GenericHttpMessageConverter<Object> httpMessageConverter() {
        return new CusHttpMessageConverter();
    }

    class CusHttpMessageConverter extends GsonHttpMessageConverter {
        @Override
        protected void writeInternal(Object o, Type type, Writer writer) throws Exception {
            //BaseResult 也是泛型
            //List<T> 也是泛型导致报错:java.lang.ClassCastException
            if (type instanceof ParameterizedType && TypeUtils.isAssignable(type, o.getClass())) {
                getGson().toJson(o, type, writer);
            } else {
                getGson().toJson(o, writer);
            }
        }

        @Override
        public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
            return true;
        }
    }

效果和验证

可以用如下的代码来进行验证,String, Object , List , null 都是支持的。


@RestController
public class TestController {

    @GetMapping("/test")
    public String test() throws Exception {
        throw new Exception("测试拦截");
    }

    @GetMapping("/str")
    public String string() {
        return "OK";
    }

    @GetMapping("/list")
    public List<Integer> list() {
        return Arrays.stream(new Integer[] {1, 2, 3, 4, 5}).collect(Collectors.toList());
    }

    @GetMapping("/fail")
    public Object fail() {
        return BaseResult.fail("错误消息");
    }

    @GetMapping("/param")
    public Object paramTest(@RequestParam(value = "str") String str) {
        return str;
    }
}

这里目前有个问题:在请求返回string的时候,默认如果请求头没有带accept: applicaion/json,返回content-Type还是text/html,需要注意下。

异常的统一返回

对于异常,如上所说使用ControllerAdviceExceptionHandler结合的方式,代码如下:

@Slf4j
@RestControllerAdvice
public class ControllerExpAdvice {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public BaseResult handleGlobalException(Exception e) {
        BaseResult result = handleBaseException(e);
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        result.setCode(status.value());
        return result;
    }
  
    
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResult handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        BaseResult<?> baseResponse = handleBaseException(e);
        baseResponse.setCode(HttpStatus.BAD_REQUEST.value());
        baseResponse.setMsg("缺失请求主体");
        return baseResponse;
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public BaseResult handleNoHandlerFoundException(NoHandlerFoundException e) {
        BaseResult<?> baseResponse = handleBaseException(e);
        HttpStatus status = HttpStatus.NOT_FOUND;
        baseResponse.setCode(status.value());
        return baseResponse;
    }
    private <T> BaseResult<T> handleBaseException(Throwable t) {
           Assert.notNull(t, "Throwable must not be null");
           log.error("Captured an exception", t);
           BaseResult<T> result = new BaseResult<>();
           result.setMsg(t.getMessage());
           return result;
       }
 }

特殊处理404

在错误处理时,对于返回404的请求,会直接返回一个少量错误信息的页面,而这里我们希望当404的时候也是返回json结构,所以这里需要加个配置,在application.properties中加如下俩行:

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

最终的效果是:

{
    "code": 404,
    "msg": "No handler found for GET /listsdgag"
}

总结

以上就是本期文章的全部内容,其实关于SpringBoot统一返回结构和异常处理相关的也比较多,缺点是很多都抄来抄去,极少有优秀的实践文章。希望这篇能给看到的大家带来帮助。

相关文章

网友评论

    本文标题:SpringBoot统一数据返回和异常

    本文链接:https://www.haomeiwen.com/subject/hqhojktx.html