美文网首页程序员
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