美文网首页
Spring Boot Mvc 统一返回结果

Spring Boot Mvc 统一返回结果

作者: 诸葛_小亮 | 来源:发表于2021-06-15 22:38 被阅读0次

    背景

    在 spring boot 项目中,使用@RestController / @RequestMapping / @GetMapping / @PostMapping 等注解提供api的功能,但是每个Mapping返回的类型各不相同,有的是void,有的是基础类型如strping /integer,有的是dto。
    在前后端分离的项目中,返回格式不统一,使得前端处理返回结果也不能统一,会导致写很多代码。

    原始controller

    例子的代码如下

    t org.springframework.web.bind.annotation.RestController;
    
    @RestController()
    @RequestMapping
    public class NoResultWarpperController {
    
        @PostMapping("hello")
        public HelloDto hello(@RequestBody HelloCmd name){
            HelloDto result = new HelloDto();
            result.setResult("hello,"+name);
            return result;
        }
    
    
        @Data
        public class HelloCmd{
            private String name;
        }
    
        @Data
        public class HelloDto{
            private String result;
        }
    }
    
    

    测试代码如下

    
    @SpringBootTest
    @AutoConfigureMockMvc
    public class NoResultWarpperControllerTest {
    
    
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        public void testHello() throws Exception{
    
            ObjectMapper map = new ObjectMapper();
    
            NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
            cmd.setName("zhangsan");
    
            String body = map.writeValueAsString(cmd);
            MvcResult mvcResult = mockMvc.perform(
                    MockMvcRequestBuilders.post("/hello")
                        .contentType(MediaType.APPLICATION_JSON)
                    .content(body)
            ).andReturn();
    
            assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
            NoResultWarpperController.HelloDto dto = map.readValue(mvcResult.getResponse().getContentAsString(), NoResultWarpperController.HelloDto.class);
            assertThat(dto).isNotNull();
            assertThat(dto.getResult()).isEqualTo("hello,zhangsan");
    
    
        }
    
    }
    

    方式一,Controller方法统一返回类型ApiResult

    新建统一返回类

    @Data
    public class ApiResult<T> {
        private T result;
        private boolean success;
        private String errorCode;
        private String errorMessage;
        private String errorDetail;
    }
    
    

    修改上面例子的Controller, 方法返回ApiResult

    
    @RestController()
    @RequestMapping
    public class NoResultWarpperController {
    
        @PostMapping("hello")
        public ApiResult<HelloDto> hello(@RequestBody HelloCmd cmd){
            HelloDto result = new HelloDto();
            result.setResult("hello,"+ cmd.getName());
    
            return  new ApiResult<>(result);
        }
    }
    

    测试代码

    
        @Test
        public void testHello() throws Exception{
    
            ObjectMapper map = new ObjectMapper();
    
            NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
            cmd.setName("zhangsan");
    
            String body = map.writeValueAsString(cmd);
            MvcResult mvcResult = mockMvc.perform(
                    MockMvcRequestBuilders.post("/hello")
                        .contentType(MediaType.APPLICATION_JSON)
                    .content(body)
            ).andReturn();
    
            assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
            ApiResult<NoResultWarpperController.HelloDto> dto = map.readValue(mvcResult.getResponse().getContentAsString(),
                    new TypeReference<ApiResult<NoResultWarpperController.HelloDto>>(){});
            assertThat(dto).isNotNull();
            assertThat(dto.isSuccess()).isTrue();
            assertThat(dto.getResult().getResult()).isEqualTo("hello,zhangsan");
    
    
        }
    

    缺点

    每个方法统一返回ApiResult类型,但是有一个缺点,就是需要程序员自身关注这件事情,如果忘记返回了,会影响使用。

    方式二,使用拦截器

    spring mvc 提供了一个接口ResponseBodyAdvice, 用来拦截响请求响应,可以通过自定义拦截器完成统一结果返回

    定义拦截器

    
    /**
     * 通过结果返回拦截器,只拦截 @RestController 标识的类
     */
    @Slf4j
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
    @RestControllerAdvice(annotations = RestController.class)
    public class RequestResponseAdvice  implements ResponseBodyAdvice<Object> {
        @Override
        public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
            return true;
        }
    
        @SneakyThrows
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
    
            ObjectMapper mapper = new ObjectMapper();
            if (body instanceof ApiResult){
                return body;
            }
    
            // 包装 string 类型
            if(body instanceof String){
    
                return mapper.writeValueAsString(new ApiResult<>(body));
            }
    
            return new ApiResult<>(body);
        }
    }
    
    

    修改方法一的方法,去掉返回类型ApiResult

    
    @RestController()
    @RequestMapping
    public class NoResultWarpperController {
    
        @PostMapping("hello")
        public HelloDto hello(@RequestBody HelloCmd cmd){
            HelloDto result = new HelloDto();
            result.setResult("hello,"+ cmd.getName());
    
            return  result;
        }
    
    }
    
    

    测试代码不用修改,运行测试,发现测试是通过,说明通过拦截器,可以统一返回类型,并且不需要强制Controller方法返回ApiResult类型

    过滤器中指定方法不使用ApiResult

    定义注解

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

    在Controller方法或类上,添加注解@DontWrapResult, 扩展 controller 方法

    @PostMapping("helloNoWrap")
        @DontWrapResult
        public HelloDto helloNoWrap(@RequestBody HelloCmd cmd){
            HelloDto result = new HelloDto();
            result.setResult("hello,"+ cmd.getName());
    
            return  result;
        }
    

    修改拦截器,是的@DontWrapResult 注解的方法或类直接返回结果

    
        @SneakyThrows
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
    
    
            if (methodParameter.hasMethodAnnotation(DontWrapResult.class)){
                return body;
            }
    
            if (AnnotationUtils.findAnnotation(methodParameter.getDeclaringClass(),DontWrapResult.class)!=null){
                return body;
            }
    
            ObjectMapper mapper = new ObjectMapper();
            if (body instanceof ApiResult){
                return body;
            }
    
            // 包装 string 类型
            if(body instanceof String){
    
                return mapper.writeValueAsString(new ApiResult<>(body));
            }
    
            return new ApiResult<>(body);
        }
    

    添加测试代码

    
        @Test
        public void testHelloNoWrap() throws Exception{
    
            ObjectMapper map = new ObjectMapper();
    
            NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
            cmd.setName("liubei");
    
            String body = map.writeValueAsString(cmd);
            MvcResult mvcResult = mockMvc.perform(
                    MockMvcRequestBuilders.post("/helloNoWrap")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body)
            ).andReturn();
    
            assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
            NoResultWarpperController.HelloDto dto = map.readValue(mvcResult.getResponse().getContentAsString(),
                    NoResultWarpperController.HelloDto.class);
            assertThat(dto).isNotNull();
            assertThat(dto.getResult()).isEqualTo("hello,liubei");
    
    
        }
    

    异常

    统一返回类型后,全局异常也要包装到类型ApiResult

    定义友好的业务异常类UserFriendlyException

    public class UserFriendlyException  extends Exception{
    
        private int code;
    
        public int errorCode(){
            return code;
        }
    
        public UserFriendlyException(){}
    
        public UserFriendlyException(String msg){
            super(msg);
        }
    
        public UserFriendlyException(int code, String msg){
            this(msg);
            this.code= code;
        }
    
    
    }
    

    修改拦截器,进行异常拦截

    
        @ExceptionHandler(Exception.class)
        @ResponseBody
        public ApiResult<Object> exceptionHandler(
                HttpServletRequest request,
                HttpServletResponse serverHttpResponse, Exception e) {
            serverHttpResponse.setStatus(500);
            return error(500, e);
        }
    
        private ApiResult<Object> error(int code,Exception ex){
    
            ApiResult<Object> result = new ApiResult<>();
            if (ex instanceof UserFriendlyException){
                result.setErrorCode(((UserFriendlyException) ex).errorCode());
            }
            else{
                result.setErrorCode(code);
            }
            result.setSuccess(false);
            result.setErrorMessage(ex.getMessage());
            result.setResult(null);
            return result;
        }
    

    普通异常测试

    Controller 添加 除法运算

    
       @GetMapping("div")
        public Double div(){
            throw new RuntimeException("b is zero");
        }
    

    测试

    
        @Test
        public void testDiv() throws Exception {
            ObjectMapper map = new ObjectMapper();
            MvcResult mvcResult = mockMvc.perform(
                    MockMvcRequestBuilders.get("/div")
            ).andReturn();
    
            assertThat(mvcResult.getResponse().getStatus()).isEqualTo(500);
            assertThat(mvcResult.getResponse().getContentAsString()).isNotNull();
            ApiResult<Object> errorInfo = map.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<ApiResult<Object>>(){});
            assertThat(errorInfo).isNotNull();
            assertThat(errorInfo.getErrorCode()).isEqualTo(500);
            assertThat(errorInfo.getErrorMessage()).isEqualTo("b is zero");
        }
    

    友好异常

    Controller 添加 加法运算

    @GetMapping("add")
        public void add() throws UserFriendlyException {
            throw new UserFriendlyException(10000, "no method");
        }
    

    测试代码

    
        @Test
        public void testAdd() throws Exception {
            ObjectMapper map = new ObjectMapper();
            MvcResult mvcResult = mockMvc.perform(
                    MockMvcRequestBuilders.get("/add")
            ).andReturn();
    
            assertThat(mvcResult.getResponse().getStatus()).isEqualTo(500);
            assertThat(mvcResult.getResponse().getContentAsString()).isNotNull();
            ApiResult<Object> errorInfo = map.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<ApiResult<Object>>(){});
            assertThat(errorInfo).isNotNull();
            assertThat(errorInfo.getErrorCode()).isEqualTo(10000);
            assertThat(errorInfo.getErrorMessage()).isEqualTo("no method");
        }
    
    

    总结

    在spring boot项目中,让controller返回统一结果有两种实现方式:

    1. 方法代码写死返回类型,弊端是没有有效的检测机制,如果方法没有返回,会影响使用一致性
    2. 继承ResponseBodyAdvice<Object> 接口自定义拦截器,不强制要求方法返回统一类型,并且针对个性化要求,比如DontWrapResult 和异常拦截,都可以很好的支持

    相关文章

      网友评论

          本文标题:Spring Boot Mvc 统一返回结果

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